OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace / tests / query / EinsteinUTest.cxxtest

300 lines · cxxtest ref:
/*
 * 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__);
}