OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace / tests / query / ExecutionOutputUTest.cxxtest

586 lines · cxxtest ref:
/*
 * tests/query/ExecutionOutputUTest.cxxtest
 *
 * Copyright (C) 2009 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/util/Logger.h>
#include <opencog/atoms/base/Link.h>
#include <opencog/atoms/core/NumberNode.h>
#include <opencog/atoms/pattern/QueryLink.h>
#include <opencog/atoms/value/LinkValue.h>
#include <opencog/atoms/value/QueueValue.h>
#include <opencog/atoms/value/StringValue.h>
#include <opencog/atoms/value/FloatValue.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/guile/SchemeEval.h>
#include <opencog/query/Implicator.h>

#include "imply.h"

using namespace opencog;

class ExecutionOutputUTest :  public CxxTest::TestSuite
{
private:
	AtomSpacePtr as;
	SchemeEval* eval;

public:

	ExecutionOutputUTest(void) : as(createAtomSpace()), eval(SchemeEval::get_scheme_evaluator(as))
	{
		logger().set_level(Logger::DEBUG);
		logger().set_print_to_stdout_flag(true);

		eval->eval("(add-to-load-path \"" PROJECT_SOURCE_DIR "\")");

	}

	~ExecutionOutputUTest()
	{
		// 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_exec(void);
	void test_varsub(void);
	void test_query_exec(void);
	void test_clear_box_single_argument(void);
	void test_clear_box_single_argument_link(void);
	void test_value(void);
	void test_recursion(void);
	void test_function(void);
	void test_lambda(void);
	void test_lambda_get(void);
	void test_variable_schema(void);
	void test_exec_variable(void);
};

#define an as->add_node
#define al as->add_link
#define getarity(hand) hand->get_arity()
#define getlink(hand,pos) hand->getOutgoingAtom(pos)

void ExecutionOutputUTest::tearDown(void)
{
	as->clear();
}
void ExecutionOutputUTest::setUp(void)
{
}

/*
 * preposition reasoning test.
 *
 * Expect to get the following result, given the above data:
 *
 *    EvaluationLink
 *       DefinedLinguisticRelationshipNode "capital_of"
 *       ListLink
 *          WordNode "capital"
 *          DefinedLinguisticRelationshipNode "of"
 */
void ExecutionOutputUTest::test_exec(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	/*
	 * Create an implication link that will be tested.
	 * This sets up the following structures:
	 *  # IF _obj(be, $var1) ^
	 *       $prep($var1, $var2)
	 *      THEN %ExecutionOutputLink("make-it", $var1, $prep)
	 */
	Handle clauses =
		al(AND_LINK,
			al(EDGE_LINK,
				an(PREDICATE_NODE, "_obj"),
				al(LIST_LINK,
					an(CONCEPT_NODE, "be"),
					an(VARIABLE_NODE, "$var1")
				)
			),
			al(EDGE_LINK,
				an(VARIABLE_NODE, "$prep"),
				al(LIST_LINK,
					an(VARIABLE_NODE, "$var1"),
					an(VARIABLE_NODE, "$var2")
				)
			)
		);
	Handle implicand =
		al(EXECUTION_OUTPUT_LINK,
			an(GROUNDED_SCHEMA_NODE, "scm:make-verb-prep"),
			al(LIST_LINK,
				an(VARIABLE_NODE, "$var1"),
				an(VARIABLE_NODE, "$prep")
			)
		);

	Handle hcapital;
	// Create data on which the above pattern should match
	al(EDGE_LINK,
		an(PREDICATE_NODE, "_obj"),
		al(LIST_LINK,
			an(CONCEPT_NODE, "be"),
			hcapital = an(CONCEPT_NODE, "capital")));

	Handle hprep;
	al(EDGE_LINK,
		hprep = an(PREDICATE_NODE, "of"),
		al(LIST_LINK,
			an(CONCEPT_NODE, "capital"),
			an(CONCEPT_NODE, "Spain")));

	const char * str =
	"(define (make-verb-prep verb prep) \n"
	"	(Edge \n"
	"		(Concept \n"
	"			(string-append (cog-name verb) \"_\" (cog-name prep))) \n"
	"		(List\n"
	"			(Concept (cog-name verb)) \n"
	"			(Predicate (cog-name prep)))))\n";
	eval->eval(str);

	// Result should be a ListLink
	Handle result = imply(as, clauses, implicand);

	// There should be only one solution: the Berlin one.
	// The Madrid graph should be rejected because of the
	// NOT definite link.
	TSM_ASSERT_EQUALS("wrong number of solutions found", 1, getarity(result));

	// Then, evaluationLink, as above.
	Handle heval = getlink(result, 0);
	Handle vcap_of = getlink(heval, 0);
	Handle hlist = getlink(heval, 1);
	Handle vcapital = getlink(hlist, 0);
	Handle vprep = getlink(hlist, 1);

	TSM_ASSERT_EQUALS("incorrect capital", hcapital, vcapital);
	TSM_ASSERT_EQUALS("incorrect prep", hprep, vprep);

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

/*
 * Test execution of variables.
 * During pattern matching, variables can bind to ExecutionOutputLinks
 * If they are then repeated in the implcand, the ExecutionOutputLink
 * must be executed there, with the resulting answer returned.
 */
void ExecutionOutputUTest::test_varsub(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	const char * str = "(define (retarg x) x)\n";
	eval->eval(str);

	Handle resolution;
	// Raw data
	al(CONTEXT_LINK,
		an(CONCEPT_NODE, "situation"),
		al(EXECUTION_OUTPUT_LINK,
			an(GROUNDED_SCHEMA_NODE, "scm: retarg"),
			al(LIST_LINK,
				resolution = an(CONCEPT_NODE, "resolution")
			)
		)
	);

	Handle deliverance;
	al(CONTEXT_LINK,
		an(CONCEPT_NODE, "predicament"),
		al(EXECUTION_OUTPUT_LINK,
			an(GROUNDED_SCHEMA_NODE, "scm: retarg"),
			al(LIST_LINK,
				deliverance = an(CONCEPT_NODE, "deliverance")
			)
		)
	);

	Handle situ =
	al(QUERY_LINK,
	   al(CONTEXT_LINK,
	      an(CONCEPT_NODE, "situation"),
	      an(VARIABLE_NODE, "$action")
	      ),
	   an(VARIABLE_NODE, "$action")
	);
	QueryLinkPtr situation(QueryLinkCast(situ));

	Handle pred =
	al(QUERY_LINK,
	   al(CONTEXT_LINK,
	      an(CONCEPT_NODE, "predicament"),
	      an(VARIABLE_NODE, "$action")
	      ),
	   an(VARIABLE_NODE, "$action")
	);
	QueryLinkPtr predicament(QueryLinkCast(pred));

	// Now perform the search.
	QueueValuePtr qvp(createQueueValue());
	ContainerValuePtr cvp(qvp);
	qvp->close();
	Implicator simpl(as.get(), cvp);
	simpl.satisfy(situation);

	// The result_list contains a list of the grounded expressions.
	qvp->open();

	// If it's empty, it will hang ...
	TSM_ASSERT("no results returned", 0 != qvp->size());

	Handle res = HandleCast(qvp->remove());
	printf("res is %s\n", res->to_short_string().c_str());
	TSM_ASSERT_EQUALS("incorrect resolution", res, resolution);

	// Now perform the search.
	qvp->close();
	Implicator pimpl(as.get(), cvp);
	pimpl.satisfy(predicament);

	// The result_list contains a list of the grounded expressions.
	qvp->open();
	Handle del = HandleCast(qvp->remove());
	printf("del is %s\n", del->to_short_string().c_str());
	TSM_ASSERT_EQUALS("incorrect delivarance", del, deliverance);

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

/*
 * Query ExecutionOutputLink atoms, with varying schemas.
 */
void ExecutionOutputUTest::test_query_exec(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	eval->eval("(load-from-path \"tests/query/exec.scm\")");

	Handle exec_query = eval->eval_h("exec-query");
	Handle results = HandleCast(exec_query->execute(as.get()));
	TS_ASSERT_EQUALS(results->get_arity(), 4);

	exec_query = eval->eval_h("quote-exec-query");
	results = HandleCast(exec_query->execute(as.get()));
	TS_ASSERT_EQUALS(results->get_arity(), 3);

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

/**
 * Execute clear box with single argument
 */
void ExecutionOutputUTest::test_clear_box_single_argument(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	Handle X = an(VARIABLE_NODE, "$X"),
		PXX = al(PLUS_LINK, X, X),
		twice = al(LAMBDA_LINK, PXX),
		N21 = an(NUMBER_NODE, "21"),
		twice21 = al(EXECUTION_OUTPUT_LINK, twice, N21);

	Handle expected = an(NUMBER_NODE, "42");
	Handle result = HandleCast(twice21->execute(as.get()));
	result = as->add_atom(result);

	TS_ASSERT_EQUALS(expected, result);

	// should work with variable inside the ListLink too

	Handle N21_LST = al(LIST_LINK, N21);
	Handle twice21_lst = al(EXECUTION_OUTPUT_LINK, twice, N21_LST);
	Handle result_lst = HandleCast(twice21_lst->execute(as.get()));
	result_lst = as->add_atom(result_lst);

	TS_ASSERT_EQUALS(expected, result_lst);

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


/**
 * Execute clear box with single argument (a link different than list)
 */
void ExecutionOutputUTest::test_clear_box_single_argument_link(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	Handle AB =
		al(INHERITANCE_LINK,
			an(CONCEPT_NODE, "A"),
			an(CONCEPT_NODE, "B"));

	Handle identity =
		al(LAMBDA_LINK,
			an(VARIABLE_NODE, "$X"),
			an(VARIABLE_NODE, "$X"));

	Handle exec =
		al(EXECUTION_OUTPUT_LINK, identity, AB);

	Handle expected = AB;
	Handle result = HandleCast(exec->execute(as.get()));

	TS_ASSERT_EQUALS(expected, result);

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

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

	eval->eval("(load-from-path \"tests/query/exec-gsn.scm\")");

	ValuePtr result = eval->eval_v("(cog-execute! exec-query-handle)");
	TS_ASSERT(nameserver().isA(result->get_type(), LINK_VALUE));
	ValueSeq vals = LinkValueCast(result)->value();
	TS_ASSERT_EQUALS(vals.size(), 3);
	for (const ValuePtr& va : vals)
		TS_ASSERT_EQUALS(va->get_type(), UNORDERED_LINK);

	//------------
	result = eval->eval_v("(cog-execute! exec-query-link-value)");
	TS_ASSERT(nameserver().isA(result->get_type(), LINK_VALUE));
	vals = LinkValueCast(result)->value();
	TS_ASSERT_EQUALS(vals.size(), 3);
	for (const ValuePtr& va : vals)
		TS_ASSERT_EQUALS(va->get_type(), LINK_VALUE);

	//------------
	result = eval->eval_v("(cog-execute! exec-query-string-value)");
	TS_ASSERT(nameserver().isA(result->get_type(), LINK_VALUE));
	vals = LinkValueCast(result)->value();
	TS_ASSERT_EQUALS(vals.size(), 3);
	for (const ValuePtr& va : vals)
		TS_ASSERT_EQUALS(va->get_type(), STRING_VALUE);

	//------------
	// The ret-float-value function returns identical FloatValue(0.6, 0.3)
	// for all matches. With proper content-based comparison, these should
	// deduplicate to 1 unique result (even though there were 3 matches).
	// This is expected behavior - see commit 658c9688a "One more bug-fix
	// for grouping" which explicitly documents result collapsing.
	result = eval->eval_v("(cog-execute! exec-query-float-value)");
	TS_ASSERT(nameserver().isA(result->get_type(), LINK_VALUE));
	vals = LinkValueCast(result)->value();
	TS_ASSERT_EQUALS(vals.size(), 1);  // 3 matches collapse to 1 unique result
	for (const ValuePtr& va : vals)
	{
		TS_ASSERT_EQUALS(va->get_type(), FLOAT_VALUE);
		FloatValuePtr fvp = FloatValueCast(va);
		const std::vector<double>& fvals = fvp->value();
		TS_ASSERT_EQUALS(fvals.size(), 2);
		TS_ASSERT_DELTA(fvals[0], 0.6, 1.0e-8);
		TS_ASSERT_DELTA(fvals[1], 0.3, 1.0e-8);
	}

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

/**
 * Execute defined function
 */
void ExecutionOutputUTest::test_function(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	al(DEFINE_LINK,
		an(DEFINED_PROCEDURE_NODE, "remainder"),
		al(LAMBDA_LINK,
			an(VARIABLE_NODE, "X"),
			al(MINUS_LINK,
				an(VARIABLE_NODE, "X"),
				al(FLOOR_LINK,
					an(VARIABLE_NODE, "X")))));

	Handle exout =
	al(EXECUTION_OUTPUT_LINK, {
		an(DEFINED_PROCEDURE_NODE, "remainder"),
		an(NUMBER_NODE, "2.1 4.2 5.3 2.4 6.5 7.6 0.7 -0.2 -8.1")});

	ValuePtr result = exout->execute(as.get());
	printf("Got: %s\n", result->to_string().c_str());

	NumberNodePtr nnp = NumberNodeCast(result);
	const std::vector<double>& val = nnp->value();
	TS_ASSERT_EQUALS(val.size(), 9);

	// The atoms won't be the same, due to rounding errors.
	for (int i=0; i<9; i++)
	{
		TS_ASSERT(fabs(val[i]-0.1*(i+1))< 1.0e-9);
	}

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

// This tests recursion of ExecutionOutputLinks, on a classic algo:
// the factorial algo.  This test does not actually test the pattern
// matcher, and so does not really belong in this directory...
void ExecutionOutputUTest::test_recursion(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	eval->eval("(load-from-path \"tests/query/exec-factorial.scm\")");

	Handle result = eval->eval_h(
		"(cog-execute! (ExecutionOutput"
		"     (DefinedSchema \"factorial\") (Number 5)))");

	TS_ASSERT_EQUALS(result->get_type(), NUMBER_NODE);
	double ans = NumberNodeCast(result)->get_value();

	TS_ASSERT_DELTA(ans, 120.0, 1.0e-6);

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

// This tests reconstructing a lambda link with a matched variable
// declaration and body
void ExecutionOutputUTest::test_lambda(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	eval->eval("(load-from-path \"tests/query/exec-lambda.scm\")");
	Handle bl = eval->eval_h("bl");

	ValuePtr result = bl->execute(as.get());
	Handle lamb = al(LAMBDA_LINK, {
	                  an(VARIABLE_NODE, "$X"),
	                  al(EDGE_LINK, {
	                     an(PREDICATE_NODE, "P"),
	                     an(CONCEPT_NODE, "A")})});
	ValuePtr expect = createLinkValue(ValueSeq({lamb}));

	logger().debug() << "result = " << oc_to_string(result);
	logger().debug() << "expect = " << oc_to_string(expect);

	TS_ASSERT(*expect == *result);

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

// This tests one of the demo examples that users are told to run.
void ExecutionOutputUTest::test_lambda_get(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	eval->eval("(load-from-path \"tests/query/exec-lambda-get.scm\")");
	Handle exo = eval->eval_h("exo");

	Handle result = as->add_atom(HandleCast(exo->execute(as.get())));
	Handle expect = al(SET_LINK, {an(NUMBER_NODE, "0.6")});

	logger().debug() << "result = " << oc_to_string(result);
	logger().debug() << "expect = " << oc_to_string(expect);

	TS_ASSERT_EQUALS(expect, result);

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

void ExecutionOutputUTest::test_variable_schema(void)
{
	al(DEFINE_LINK, {
		an(DEFINED_SCHEMA_NODE, "sum"),
		al(LAMBDA_LINK, {
			al(VARIABLE_LIST, {
				an(VARIABLE_NODE, "x"),
				an(VARIABLE_NODE, "y")}),
			al(PLUS_LINK, {
				an(VARIABLE_NODE, "x"),
				an(VARIABLE_NODE, "y")})
		})});

	al(DEFINE_LINK, {
		an(DEFINED_SCHEMA_NODE, "mul"),
		al(LAMBDA_LINK, {
			al(VARIABLE_LIST, {
				an(VARIABLE_NODE, "x"),
				an(VARIABLE_NODE, "y")}),
			al(TIMES_LINK, {
				an(VARIABLE_NODE, "x"),
				an(VARIABLE_NODE, "y")})
		})});

	al(INHERITANCE_LINK, {
		an(DEFINED_SCHEMA_NODE, "mul"),
		an(CONCEPT_NODE, "current-schema")
	});


	Handle get_link =
	al(COLLECTION_OF_LINK, {
	al(QUERY_LINK, {
		al(TYPED_VARIABLE_LINK, {
			an(VARIABLE_NODE, "$SCHEMA"), an(TYPE_NODE, "DefinedSchemaNode")}),
		al(AND_LINK, {
			al(INHERITANCE_LINK, {
				an(VARIABLE_NODE, "$SCHEMA"),
				an(CONCEPT_NODE, "current-schema")
		    })
		}),
		al(EXECUTION_OUTPUT_LINK, {
			an(VARIABLE_NODE, "$SCHEMA"),
			al(LIST_LINK, {an(NUMBER_NODE, "6"), an(NUMBER_NODE, "7")})
		})
	})});
	ValuePtr result = get_link->execute(as.get());
	TS_ASSERT_EQUALS(result, al(SET_LINK, {an(NUMBER_NODE, "42")}));
}

void ExecutionOutputUTest::test_exec_variable(void)
{
	al(DEFINE_LINK, {
		an(DEFINED_SCHEMA_NODE, "x*y"),
		al(LAMBDA_LINK, {
			al(VARIABLE_LIST, {
				an(VARIABLE_NODE, "x"),
				an(VARIABLE_NODE, "y")}),
			al(TIMES_LINK, {
				an(VARIABLE_NODE, "x"),
				an(VARIABLE_NODE, "y")})
		})});
	Handle bind_link =
	al(COLLECTION_OF_LINK, {
	al(QUERY_LINK, {
		al(TYPED_VARIABLE_LINK, {
			an(VARIABLE_NODE, "x"), an(TYPE_NODE, "DefinedSchemaNode")}),
		al(AND_LINK, {al(PRESENT_LINK, {an(VARIABLE_NODE, "x")})}),
		al(EXECUTION_OUTPUT_LINK, {
			an(VARIABLE_NODE, "x"),
			al(LIST_LINK, {an(NUMBER_NODE, "6"), an(NUMBER_NODE, "7")})
		})
	})});
	ValuePtr result = bind_link->execute(as.get());

	TS_ASSERT_EQUALS(result, al(SET_LINK, {an(NUMBER_NODE, "42")}));
}