OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace / tests / query / PatternUTest.cxxtest

484 lines · cxxtest ref:
/*
 * tests/query/PatternUTest.cxxtest
 *
 * Copyright (C) 2009,2011 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 <cxxtest/TestSuite.h>

#include <opencog/atoms/base/Node.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/query/TermMatchMixin.h>
#include <opencog/query/InitiateSearchMixin.h>
#include <opencog/query/SatisfyMixin.h>
#include <opencog/atoms/pattern/PatternLink.h>
#include <opencog/util/Logger.h>

#include "imply.h"
#include "test-types.h"

using namespace opencog;

#define al as->add_link
#define an as->add_node

// This implements the most basic, simplest test of the pattern matcher.
class PatternUTest: public CxxTest::TestSuite
{
	private:
		AtomSpacePtr as;

		// These handles are documented below.
		Handle hconst_1;
		Handle hconst_2;
		Handle hconst_3;
		Handle hsoln_a;
		Handle hsoln_b;
		Handle hsoln_c;

		Handle hpred_1a;
		Handle hpred_1b;
		Handle hpred_1c;

		Handle hpred_2a;
		Handle hpred_2b;
		Handle hpred_2c;

		Handle hitem_1;
		Handle hitem_2;
		Handle hitem_3;
		Handle hlist;
		Handle hprnode;
		Handle hoc;

		Handle hshort_list;
		Handle hshort_prnode;
		Handle hshort_oc;

		Handle heval;
		Handle hshort_eval;

	public:

		PatternUTest(void)
		{
			logger().set_level(Logger::DEBUG);
			logger().set_print_to_stdout_flag(true);
#include "test-types.cc"
			as = createAtomSpace();
		}

		~PatternUTest()
		{
			// 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_simple_link(void);
		void test_two_links(void);
		void test_eval(void);
		void test_implication(void);
};

class PMCB :
	public InitiateSearchMixin,
	public TermMatchMixin,
	public SatisfyMixin
{
	public:
		PMCB(AtomSpace* as) :
			InitiateSearchMixin(as), TermMatchMixin(as), found(0) {}

		virtual void set_pattern(const Variables& vars,
		                         const Pattern& pat)
		{
			InitiateSearchMixin::set_pattern(vars, pat);
			TermMatchMixin::set_pattern(vars, pat);
		}

		virtual bool propose_grounding(const HandleMap &var_soln,
		                               const HandleMap &pred_soln);

		int found;
		HandleMap preds;
		HandleMap vars;
};

bool PMCB::propose_grounding(const HandleMap &var_soln,
                             const HandleMap &pred_soln)
{
	found ++;
	preds = pred_soln;
	vars = var_soln;
	return false;
}

/**
 * Run the matcher against the indicated variables and clauses,
 * using the indicated callback.
 */
static inline void match(PatternMatchCallback& pmcb,
                         const HandleSet &vars,
                         const HandleSeq &clauses)
{
	// Convert HandleSet vars to HandleSeq for VariableList
	HandleSeq varseq(vars.begin(), vars.end());
	Handle hvars = createLink(std::move(varseq), VARIABLE_LIST);

	// Convert HandleSeq clauses to body Handle
	Handle hbody;
	if (clauses.size() == 1) {
		hbody = clauses[0];
	} else {
		hbody = createLink(clauses, AND_LINK);
	}

	PatternLinkPtr slp(createPatternLink(hvars, hbody));
	pmcb.satisfy(slp);
}

#define getlink(hand,pos) hand->getOutgoingAtom(pos)

/*
 * This function sets up the following structures:
 *
 *    ConceptNode "some const concept node 1"       ;; hconst_1
 *    ConceptNode "some const concept node 2"       ;; hconst_2
 *    ConceptNode "some const concept node 3"       ;; hconst_3
 *    ConceptNode "desired solution concept node a" ;; hsoln_a
 *    ConceptNode "desired solution concept node b" ;; hsoln_b
 *    ConceptNode "desired solution concept node c" ;; hsoln_c
 *    ConceptNode "incorrect solution concept node"
 *
 *    InheritanceLink   ;; hpred_1a
 *       ConceptNode "some const concept node 1"
 *       ConceptNode "desired solution concept node a"
 *
 *    ListLink     ;; hpred_1b
 *       ConceptNode "some const concept node 1"
 *       ConceptNode "desired solution concept node b"
 *
 *    ListLink     ;; hpred_1c
 *       ConceptNode "some const concept node 1"
 *       ConceptNode "desired solution concept node c"
 *
 *    AssociativeLink
 *       ConceptNode "some const concept node"
 *       ConceptNode "incorrect solution concept node"
 *
 *    EvaluationLink                                ;; hoc
 *       PredicateNode "relationship blaupunkt"     ;; hprnode
 *       ListLink                                   ;; hlist
 *          ConceptNode "concept kanjiru 1"         ;; hitem_1
 *          ConceptNode "concept kangaeru 2"        ;; hitem_2
 *          ConceptNode "concept nihongo 3"         ;; hitem_3
 *
 *    EvaluationLink                                ;; hshort_oc
 *       PredicateNode "relationship weltanshaung"  ;; hshort_prnode
 *       ListLink                                   ;; hshort_list
 *          ConceptNode "concept kanjiru 1"         ;; hitem_1
 *          ConceptNode "concept kangaeru 2"        ;; hitem_2
 */
void PatternUTest::tearDown(void)
{
}
void PatternUTest::setUp(void)
{
	// create a gnarly graph
	hconst_1 = an(CONCEPT_NODE, "some const concept node 1");
	hconst_2 = an(CONCEPT_NODE, "some const concept node 2");
	hconst_3 = an(CONCEPT_NODE, "some const concept node 3");
	hsoln_a = an(CONCEPT_NODE, "desired solution concept node a");
	hsoln_b = an(CONCEPT_NODE, "desired solution concept node b");
	hsoln_c = an(CONCEPT_NODE, "desired solution concept node c");

	hpred_1a = al(INHERITANCE_LINK, hconst_1, hsoln_a);
	hpred_1b = al(LIST_LINK, hconst_1, hsoln_b);
	hpred_1c = al(LIST_LINK, hconst_1, hsoln_c);

	hpred_2a = al(INHERITANCE_LINK, hconst_2, hsoln_a);
	hpred_2b = al(LIST_LINK, hconst_2, hsoln_b);
	hpred_2c = al(LIST_LINK, hconst_2, hsoln_c);

	// create a confounding link, like above, but different
	Handle hwrong = an(CONCEPT_NODE, "incorrect solution concept node");

	al(SIMILARITY_LINK, hconst_1, hwrong);

	// Create a standard-format opencog predicate
	hitem_1 = an(CONCEPT_NODE, "concept kanjiru 1");
	hitem_2 = an(CONCEPT_NODE, "concept kangaeru 2");
	hitem_3 = an(CONCEPT_NODE, "concept nihongo 3");

	hlist = al(LIST_LINK, hitem_1, hitem_2, hitem_3);

	hprnode = an(PREDICATE_NODE, "relationship blaupunkt");

	hoc = al(EVALUATION_LINK, hprnode, hlist);

	// Same as above, but with only two items in the list.
	hshort_list = al(LIST_LINK, hitem_1, hitem_2);

	hshort_prnode = an(PREDICATE_NODE, "relationship weltanshaung");

	hshort_oc = al(EVALUATION_LINK, hshort_prnode, hshort_list);
}

void PatternUTest::test_simple_link(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	// Define the pattern to match against.
	Handle hvar = an(VARIABLE_NODE, "variable node");
	Handle he = al(INHERITANCE_LINK, hconst_1, hvar);

	HandleSet vars;
	vars.insert(hvar);

	HandleSeq preds;
	preds.push_back(he);

	PMCB pmcb(as.get());
	match(pmcb, vars, preds);

	// Should find only one solution.
	TSM_ASSERT_EQUALS("wrong number of solutions", pmcb.found, 1);
	TSM_ASSERT("incorrect solution", hsoln_a == pmcb.vars[hvar]);
	TSM_ASSERT("incorrect predicate", hpred_1a == pmcb.preds[he]);

	// Remove garbage, just in case ...
	as->extract_atom(he);
	as->extract_atom(hvar);
	logger().debug("END TEST: %s", __FUNCTION__);
}

/*
 * This test creates the following pattern, for matching:
 *
 *    AndList
 *       ListLink
 *          ConceptNode "some const concept node 1"
 *          VariableNode "variable node x"
 *       ListLink
 *          ConceptNode "some const concept node 2"
 *          VariableNode "variable node x"
 *
 *    ListLink
 *       VariableNode "variable node x"
 *
 * This is used to try to match hypergraphs in the atomspace, defined
 * above. Two answers are expected, since "desired solution concept
 * node b" and "c" can be paired to "x".
 */

void PatternUTest::test_two_links(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	// Define the pattern to match against.
	Handle hx = an(VARIABLE_NODE, "variable node x");

	Handle hl1 = al(LIST_LINK, hconst_1, hx);
	Handle hl2 = al(LIST_LINK, hconst_2, hx);

	Handle han = al(AND_LINK, hl1, hl2);

	Handle hv = al(VARIABLE_LIST, hx);

	HandleSet vars;
	vars.insert(hx);

	HandleSeq preds;
	preds.push_back(hl1);
	preds.push_back(hl2);

	PMCB pmcb(as.get());
	match(pmcb, vars, preds);

	// Should find two solutions:
	// var_x = soln_b
	// var_x = soln_c
	TSM_ASSERT_EQUALS("wrong number of solutions", pmcb.found, 2);
	// TSM_ASSERT("incorrect solution", hsoln_b == pmcb.vars[hx]);
	// TSM_ASSERT("incorrect predicate", hpred_b == pmcb.preds[hlx]);

	// Test the non-std::vector interface also.
	pmcb.found = 0;
	PatternLinkPtr sl(createPatternLink(hv, han));
	pmcb.satisfy(sl);
	TSM_ASSERT_EQUALS("wrong number of solutions", pmcb.found, 2);

	logger().debug("END TEST: %s", __FUNCTION__);
}

/*
 * This test case creates the following pattern-matching and variable
 * structures:
 *
 *    EvaluationLink                                ;; heval
 *       VariableNode "3-tuple relationship variable"
 *       ListLink
 *          ConceptNode "concept kanjiru 1"         ;; hitem_1
 *          VariableNode "concept variable"
 *          ConceptNode "concept nihongo 3"         ;; hitem_3
 *
 *    EvaluationLink                                ;; hshort_eval
 *       VariableNode "2-tuple relationship variable"
 *       ListLink
 *          ConceptNode "concept kanjiru 1"         ;; hitem_1
 *          VariableNode "concept variable"
 *
 */
void PatternUTest::test_eval(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	// Create a standard-format opencog predicate
	Handle h1 = an(CONCEPT_NODE, "concept kanjiru 1");
	Handle h2 = an(VARIABLE_NODE, "concept variable");
	Handle h3 = an(CONCEPT_NODE, "concept nihongo 3");

	Handle hl = al(LIST_LINK, h1, h2, h3);

	Handle hprn = an(VARIABLE_NODE, "3-tuple relationship variable");

	heval = al(EVALUATION_LINK, hprn, hl);

	HandleSet vars;
	vars.insert(hprn);
	vars.insert(h2);

	HandleSeq preds;
	preds.push_back(heval);

	PMCB pmcb(as.get());
	match(pmcb, vars, preds);

	// Should find one solution
	TSM_ASSERT_EQUALS("wrong number of solutions", pmcb.found, 1);
	TSM_ASSERT("incorrect solution", hitem_2 == pmcb.vars[h2]);
	TSM_ASSERT("incorrect pr-node", hprnode == pmcb.vars[hprn]);
	TSM_ASSERT("incorrect predicate", hoc == pmcb.preds[heval]);

	// ----------------------------------------
	logger().debug("BEGIN second subtest: %s", __FUNCTION__);
	// Do it again, but with a truncated list
	Handle hls = al(LIST_LINK, h1, h2);

	Handle hprns = an(VARIABLE_NODE, "2-tuple relationship variable");

	hshort_eval = al(EVALUATION_LINK, hprns, hls);

	HandleSet svars;
	svars.insert(hprns);
	svars.insert(h2);

	HandleSeq spreds;
	spreds.push_back(hshort_eval);

	pmcb.found = 0; // reset the counter !!
	match(pmcb, svars, spreds);

	// Should find one solution
	TSM_ASSERT_EQUALS("wrong number of solutions", pmcb.found, 1);
	TSM_ASSERT("incorrect solution", hitem_2 == pmcb.vars[h2]);
	TSM_ASSERT("incorrect pr-node", hshort_prnode == pmcb.vars[hprns]);
	TSM_ASSERT("incorrect predicate", hshort_oc == pmcb.preds[hshort_eval]);

	// ----------------------------------------------
	logger().debug("BEGIN third subtest: %s", __FUNCTION__);
	// And do it again, now matching two clauses
	// The first clause was set up above; tack on the second one.
	preds.push_back(hshort_eval);
	vars.insert(hprns);
	pmcb.found = 0; // reset the counter !!
	match(pmcb, vars, preds);
	TSM_ASSERT_EQUALS("wrong number of solutions", pmcb.found, 1);
	TSM_ASSERT("incorrect solution", hitem_2 == pmcb.vars[h2]);
	TSM_ASSERT("incorrect pr-node", hprnode == pmcb.vars[hprn]);
	TSM_ASSERT("incorrect pr-node", hshort_prnode == pmcb.vars[hprns]);
	TSM_ASSERT("incorrect predicate", hoc == pmcb.preds[heval]);
	TSM_ASSERT("incorrect predicate", hshort_oc == pmcb.preds[hshort_eval]);

	logger().debug("END TEST: %s", __FUNCTION__);
}

/*
 * This creates the following implication structure
 *
 *    ImplicationLink
 *       AndLink
 *           EvaluationLink  ;; heval, as given above ...
 *           EvaluationLink  ;; hshorteval, as given above ...
 *       ParseLink
 *           VariableNode "concept variable"
 *
 * After evaluation, it is expected that the atom-space will
 * contain the following:
 *
 *    ParseLink
 *       ConceptNode "concept kangaeru 2"        ;; hitem_2
 *
 * Since that is what the variable is expected to bind to.
 */
void PatternUTest::test_implication(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	Handle han = al(AND_LINK, heval, hshort_eval);

	Handle h2 = an(VARIABLE_NODE, "concept variable");

	Handle hpa = al(PARSE_LINK, h2);

	imply(as.get(), han, hpa);

	// Remove the parse link, so that the other one is easier to find.
	as->extract_atom(hpa);

	// At this point, the AtomSpace should contain a structure
	// that looks like the below. Look for it.
	//
	//   ParseLink
	//      ConceptNode "concept kangaeru 2"        ;; hitem_2
	//
	// Be sure to explicitly check that we got hitem_2 as the result

	HandleSeq ret;
	as->get_handles_by_type(ret, PARSE_LINK);

	TSM_ASSERT_EQUALS("incorrect implication", ret.size(), 1);
	if (0 < ret.size())
	{
		TSM_ASSERT("null pointer", as->is_valid_handle(ret[0]));

		Handle solution = getlink(ret[0],0);
		TSM_ASSERT("incorrect solution", hitem_2 == solution);
	}

	logger().debug("END TEST: %s", __FUNCTION__);
}

#undef al
#undef an