/*
* tests/query/EinsteinUTest.cxxtest
*
* Copyright (C) 2009, 2013 Linas Vepstas <linasvepstas@gmail.com>
* All Rights Reserved
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License v3 as
* published by the Free Software Foundation and including the exceptions
* at http://opencog.org/wiki/Licenses
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, write to:
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/guile/SchemeEval.h>
#include <opencog/util/Logger.h>
#include "test-types.h"
using namespace opencog;
class EinsteinPuzzle : public CxxTest::TestSuite
{
private:
AtomSpacePtr as;
SchemeEval* eval;
public:
EinsteinPuzzle(void)
{
logger().set_level(Logger::DEBUG);
logger().set_print_to_stdout_flag(true);
#include "test-types.cc"
as = createAtomSpace();
eval = SchemeEval::get_scheme_evaluator(as);
eval->eval("(add-to-load-path \"" PROJECT_SOURCE_DIR "\")");
eval->eval("(load-from-path \"tests/query/test_types.scm\")");
}
~EinsteinPuzzle()
{
// Erase the log file if no assertions failed.
if (!CxxTest::TestTracker::tracker().suiteFailed())
std::remove(logger().get_filename().c_str());
}
void setUp(void);
void tearDown(void) { }
void test_trivial_deduct(void);
void test_trivial_keep(void);
void test_full(void);
};
/*
* Clear the atomspace, load various atom types.
*/
#define an as->add_node
#define al as->add_link
#define getarity(hand) hand->get_arity()
#define getlink(hand,pos) hand->getOutgoingAtom(pos)
void EinsteinPuzzle::setUp(void)
{
as->clear();
}
/* ========================================================================
* Trivial deductive test -- test the is-same-person rule
*/
void EinsteinPuzzle::test_trivial_deduct(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
eval->eval("(load-from-path \"tests/query/deduct-trivial.scm\")");
eval->eval("(load-from-path \"tests/query/deduct-rules.scm\")");
// Create an implication link that will be tested.
Handle puzzle_rules = eval->apply("is-same-rule", Handle::UNDEFINED);
// Make sure the scheme file actually loaded!
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != puzzle_rules);
// Result should be a ListLink w/ four solutions
// Why four? Obviously, (1,1), (1,2), (2,1), (2,2)
//
// Note that we explicitly filter sameness to avoid double
// counting, as otherwise, there would be six solutions. Why six?
// Obviously, four above, but also, the reversed grounding matches:
// (1,1) and (2,2), via predicates Nationality and KeepsPet.
Handle puzzle_solution = HandleCast(puzzle_rules->execute(as.get()));
logger().debug("trivial puzzle solution is %s\n",
oc_to_string(puzzle_solution).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 4,
getarity(puzzle_solution));
logger().debug("END TEST: %s", __FUNCTION__);
}
/* ========================================================================
* Trivial deductive test -- print out who keeps what.
*/
void EinsteinPuzzle::test_trivial_keep(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
eval->eval("(load-from-path \"tests/query/deduct-trivial.scm\")");
eval->eval("(load-from-path \"tests/query/deduct-rules.scm\")");
eval->eval("(load-from-path \"tests/query/deduct-keep.scm\")");
// Create an implication link that will be tested.
Handle same_rule = eval->apply("is-same-rule", Handle::UNDEFINED);
Handle trans_rule = eval->apply("transitive-rule", Handle::UNDEFINED);
Handle print_rule = eval->apply("print-ownership", Handle::UNDEFINED);
// Make sure the scheme file actually loaded!
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != same_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != trans_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != print_rule);
// First, deduce if two seemingly different people are the same ...
same_rule->execute(as.get());
// Next, deduce that the same properties apply to each
trans_rule->execute(as.get());
// Result should be a ListLink w/ two solutions
//
// Why two? Because person 1 and 2 are the same person, so both
// solutions get listed.
Handle puzzle_solution = HandleCast(print_rule->execute(as.get()));
logger().debug("trivial puzzle keepers are %s\n",
oc_to_string(puzzle_solution).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 2,
getarity(puzzle_solution));
logger().debug("END TEST: %s", __FUNCTION__);
}
/* ========================================================================
* Full puzzle test -- Try to solve the entire puzzle.
* The algorithm used below is simple forward-chaining. Start by reading
* a number of facts about the people, and their various attributes. One
* goal is to keep the facts as close as possible to natural-English
* language assertions as possile. Then, to these facts, apply a number
* of different deductive rules. These rules try to be "natural" as well,
* rather than forcing an "un-natural" 5x5 solution grid onto the problem
* (Since we don't a-priori know that its really a 5x5 problem ... yet).
*
* At this time, there are not enough deductive rules to solve the problem;
* we just scratch the surface. But, for a unit test, this is enough, for now.
* I'd like to expand this sometime later ...
*/
void EinsteinPuzzle::test_full(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
eval->eval("(load-from-path \"tests/query/deduct-einstein.scm\")");
eval->eval("(load-from-path \"tests/query/deduct-rules.scm\")");
// Create an implication link that will be tested.
Handle same_rule = eval->apply("is-same-rule", Handle::UNDEFINED);
Handle trans_rule = eval->apply("transitive-rule", Handle::UNDEFINED);
Handle ntran_rule = eval->apply("transitive-not-rule", Handle::UNDEFINED);
Handle faddr_rule = eval->apply("first-addr-rule", Handle::UNDEFINED);
Handle neigh_rule = eval->apply("neighbor-rule", Handle::UNDEFINED);
Handle nesym_rule = eval->apply("neighbor-symmetry-rule", Handle::UNDEFINED);
Handle nattr_rule = eval->apply("neighbor-not-attr-rule", Handle::UNDEFINED);
Handle distc_rule = eval->apply("distinct-attr-rule", Handle::UNDEFINED);
Handle elimi_rule = eval->apply("by-elimination-rule", Handle::UNDEFINED);
Handle solun_rule = eval->apply("found-solution-rule", Handle::UNDEFINED);
// Make sure the scheme file actually loaded!
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != same_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != trans_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != ntran_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != faddr_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != neigh_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != nesym_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != nattr_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != distc_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != elimi_rule);
TSM_ASSERT("Failed to load test data", Handle::UNDEFINED != solun_rule);
// First, deduce if two seemingly different people are the same ...
// We should be able to immediately deduce that:
// person10 == person15 (blend)
// person14 == person9 (norweigan)
// dunhill smoker(rule 12) == person7
Handle same = HandleCast(same_rule->execute(as.get()));
logger().debug("After first same-rule\n%s\n",
oc_to_string(same).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 6, getarity(same));
// Next, propagate this knowledge transitively
Handle trans = HandleCast(trans_rule->execute(as.get()));
logger().debug("After first transitive-rule\n%s\n",
oc_to_string(trans).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 2, getarity(trans));
// -----------
// Next, make a deduction about the house on the end.
// This combines facts 9+14: blue house has address 101. This is just a single result.
Handle faddr = HandleCast(faddr_rule->execute(as.get()));
logger().debug("After first-address-rule\n%s\n",
oc_to_string(faddr).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 1, getarity(faddr));
// Neighbor relations are symmetric. Propagate this symmetry.
// The original facts were's stated symmetrically; we make up for that now:
// symmetrize rules 10, 12, 14, 15
Handle neigh = HandleCast(nesym_rule->execute(as.get()));
logger().debug("After neighbor-symmetry-rule\n%s\n",
oc_to_string(neigh).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 4, getarity(neigh));
// Deduce new neighbors.
// The above made a deduction about address 102, and fact 8 has
// address 103 in it.
neigh = HandleCast(neigh_rule->execute(as.get()));
logger().debug("After first neighbor-rule\n%s\n",
oc_to_string(neigh).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 2, getarity(neigh));
// Neighbor relations are symmetric. Propagate this symmetry.
// This symmetrizes the findings from the rule application immediately above.
neigh = HandleCast(nesym_rule->execute(as.get()));
logger().debug("After second neighbor-symmetry-rule\n%s\n",
oc_to_string(neigh).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 2, getarity(neigh));
// -----------
// If some attribute holds for a neighbor, it cannot hold for a person.
Handle nattr = HandleCast(nattr_rule->execute(as.get()));
logger().debug("After first neighbor-not-attr rule\n%s\n",
oc_to_string(nattr).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 17, getarity(nattr));
// Next, propagate this knowledge transitively
Handle ntran = HandleCast(ntran_rule->execute(as.get()));
logger().debug("After first transitive-not-rule\n%s\n",
oc_to_string(ntran).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 3, getarity(ntran));
// -----------
// Two distinct people cannot have a common attribute
Handle distc = HandleCast(distc_rule->execute(as.get()));
logger().debug() << "After first distinct-attr rule\n"
<< oc_to_string(distc).c_str();
// XXX virtual link matcher does not yet do 'not' clauses ...
// TSM_ASSERT_EQUALS("wrong number of solutions found", 234, getarity(distc));
TSM_ASSERT_EQUALS("wrong number of solutions found", 222, getarity(distc));
// -----------
// By-elimination rule
// If a person doesn't smoke/drink/live-in four items, they must do the fifth.
Handle elimi = HandleCast(elimi_rule->execute(as.get()));
logger().debug() << "After elimination\n" << oc_to_string(elimi);
TSM_ASSERT_EQUALS("wrong number of solutions found", 29, getarity(elimi));
// -----------
// Print partial results.
Handle prt_stuff = eval->apply("print-results", Handle::UNDEFINED);
Handle stuff = HandleCast(prt_stuff->execute(as.get()));
logger().debug() << "stuff\n" << oc_to_string(stuff);
// -----------
// Result should be a ListLink w/ five solutions
// Right now, we accept zero solutions, because above was not yet
// enough to solve the pouzzle.
Handle puzzle_solution = HandleCast(solun_rule->execute(as.get()));
logger().debug("puzzle solution is\n%s\n",
oc_to_string(puzzle_solution).c_str());
TSM_ASSERT_EQUALS("wrong number of solutions found", 0,
getarity(puzzle_solution));
logger().debug("END TEST: %s", __FUNCTION__);
}