diff --git a/examples/atomspace/formulas.scm b/examples/atomspace/formulas.scm new file mode 100644 index 0000000000..cfaeaa7415 --- /dev/null +++ b/examples/atomspace/formulas.scm @@ -0,0 +1,182 @@ +; +; formulas.scm -- Declaring formulas that compute truth values. +; +; The rule engine, PLN and other subsystems need to be able to compute +; and alter truth values. If those formulas are known a-priori, then +; thay can be hard-coded into GroundedPredicateNodes. However, it is +; possible that those formulas are not yet known: that they will be +; learned using some learning algorithm, for example, MOSES, or maybe +; the Pattern Miner, or some neural network. +; +; In this case, the formulas need to be placed where they can be +; accessed during computation: in the AtomSpace. The example below +; shows how formulas can be declared in the AtomSpace, but in such a +; way that they can also be evaluated to yeild an actual truth value, +; which is then attached to some Atom. +; +(use-modules (opencog) (opencog exec)) + +; The StrengthOfLink returns a single floating-point number, +; the strength of a TruthValue. +(Concept "A" (stv 0.8 1.0)) +(cog-execute! (StrengthOf (Concept "A"))) + +; The demo needs at least one more Atom. +(Concept "B" (stv 0.6 0.9)) + +; Multiply the strength of the TV's of two atoms. +(cog-execute! + (Times (StrengthOf (Concept "A")) (StrengthOf (Concept "B")))) + +; Create a SimpleTruthValue with a non-trivial formula: +; It will be the TV := (1-sA*sB, cA*cB) where sA and sB are strenghts +; and cA, cB are confidence values. The PredicateFormulaLink assembles +; two floating-point values, and create a SimpleTruthValue out of them. +; +(cog-evaluate! + (PredicateFormula + (Minus + (Number 1) + (Times (StrengthOf (Concept "A")) (StrengthOf (Concept "B")))) + (Times (ConfidenceOf (Concept "A")) (ConfidenceOf (Concept "B"))))) + +; The values do not need to be formulas; they can be hard-coded numbers. +(cog-evaluate! + (PredicateFormula (Number 0.7) (Number 0.314))) + +; The below computes a truth value, and attaches it to the +; EvaluationLink. Let's demo this in three parts: first define it, +; then evaluate it, then look at it. +; +(define my-ev-link + (Evaluation + (PredicateFormula (Number 0.7) (Number 0.314)) + (List + (Concept "A") + (Concept "B")))) + +; Evaluate ... +(cog-evaluate! my-ev-link) + +; Print. +(display my-ev-link) + +; More typically, one wishes to have a formula in the abstract, +; with variables in it, so that one can apply it in any one of +; a number of different situations. In the below, the variables +; are automatically reduced with the Atoms in the ListLink, and +; then the formula is evaluated to obtain a TruthValue. +(cog-evaluate! + (Evaluation + ; Compute TV = (1-sA*sB, cA*cB) + (PredicateFormula + (Minus + (Number 1) + (Times + (StrengthOf (Variable "$X")) + (StrengthOf (Variable "$Y")))) + (Times + (ConfidenceOf (Variable "$X")) + (ConfidenceOf (Variable "$Y")))) + (List + (Concept "A") + (Concept "B")))) + +; Optionally, you can wrap formulas with LambdaLinks. This doesn't +; really change anything; formulas work fine without LambdaLinks. +(cog-evaluate! + (Evaluation + ; Compute TV = (1-sA*sB, cA*cB) + (PredicateFormula + (Lambda (Minus + (Number 1) + (Times + (StrengthOf (Variable "$X")) + (StrengthOf (Variable "$Y"))))) + (Lambda (Times + (ConfidenceOf (Variable "$X")) + (ConfidenceOf (Variable "$Y"))))) + (List + (Concept "A") + (Concept "B")))) + + +; The PedicateFormulaLink behaves just like any other algebraic +; expression with VariableNodes in it. When executed, it might +; reduce a bit, but that is all. +(cog-execute! + (PredicateFormula + (Plus (Number 41) + (Minus + (Number 1) + (Times + (StrengthOf (Variable "$VA")) + (StrengthOf (Variable "$VB"))))) + (Times + (ConfidenceOf (Variable "$VA")) + (ConfidenceOf (Variable "$VB"))))) + +; Beta-reducation works as normal. The below will create an +; EvaluationLink with ConceptNode A and B in it, and will set the +; truth value according to the formula. +(define the-put-result + (cog-execute! + (PutLink + (VariableList (Variable "$VA") (Variable "$VB")) + (Evaluation + ; Compute TV = (1-sA*sB, cA*cB) + (PredicateFormula + (Minus + (Number 1) + (Times + (StrengthOf (Variable "$VA")) + (StrengthOf (Variable "$VB")))) + (Times + (ConfidenceOf (Variable "$VA")) + (ConfidenceOf (Variable "$VB")))) + (List + (Variable "$VA") (Variable "$VB"))) + (Set (List (Concept "A") (Concept "B")))))) + +; The scheme variable `the-put-result` contains a SetLink with the +; result in it. Lets unwrap it, so that `evelnk` is just the +; EvaluationLink. And tehn we play a little trick. +(define evelnk (cog-outgoing-atom the-put-result 0)) + +; Change the truth value on the two concept nodes ... +(Concept "A" (stv 0.3 0.5)) +(Concept "B" (stv 0.4 0.5)) + +; Re-evaluate the EvaluationLink. Note the TV has been updated! +(cog-evaluate! evelnk) + +; Do it again, for good luck! +(Concept "A" (stv 0.1 0.99)) +(Concept "B" (stv 0.1 0.99)) + +; Re-evaluate the EvaluationLink. The TV is again recomputed! +(cog-evaluate! evelnk) + +; One can also use DefinedPredicates, to give the formula a name. +(DefineLink + (DefinedPredicate "has a reddish color") + (PredicateFormula + (Minus + (Number 1) + (Times + (StrengthOf (Variable "$X")) + (StrengthOf (Variable "$Y")))) + (Times + (ConfidenceOf (Variable "$X")) + (ConfidenceOf (Variable "$Y"))))) + +(Concept "A" (stv 0.9 0.98)) +(Concept "B" (stv 0.9 0.98)) + +; The will cause the formula to evaluate. +(cog-evaluate! + (Evaluation + (DefinedPredicate "has a reddish color") + (List + (Concept "A") + (Concept "B")))) diff --git a/opencog/atoms/atom_types/atom_types.script b/opencog/atoms/atom_types/atom_types.script index f4c8b1d9f6..0b84158a0e 100644 --- a/opencog/atoms/atom_types/atom_types.script +++ b/opencog/atoms/atom_types/atom_types.script @@ -571,9 +571,20 @@ FUNCTION_LINK <- FREE_LINK EXECUTION_OUTPUT_LINK <- FUNCTION_LINK // Return the indicated value on the indicated atom. -// XXX It doesn't *always* return numbers ... +// XXX It's marked as "NUMERIC_OUTPUT, but doesn't *always* return +// numbers ... Probably should have a FLOAT_VALUE_OF_LINK to fix this. +// Ignore this issue for now ... VALUE_OF_LINK <- FUNCTION_LINK,NUMERIC_OUTPUT_LINK TRUTH_VALUE_OF_LINK <- VALUE_OF_LINK +STRENGTH_OF_LINK <- VALUE_OF_LINK +CONFIDENCE_OF_LINK <- VALUE_OF_LINK + +// The opposite of the above: given something that evaluates to a +// FloatValue, return a TruthValue. Kind-of-like +// GROUNDED_PREDICATE_NODE, but holding the forumla in the atomspace. +// This is not really needed, but will make the transition process +// smoother. +PREDICATE_FORMULA_LINK <- EVALUATABLE_LINK // Basic arithmetic operators FOLD_LINK <- FUNCTION_LINK diff --git a/opencog/atoms/base/ClassServer.h b/opencog/atoms/base/ClassServer.h index cad99be1e4..6362a68b87 100644 --- a/opencog/atoms/base/ClassServer.h +++ b/opencog/atoms/base/ClassServer.h @@ -102,6 +102,9 @@ class ClassServer ClassServer& classserver(); +#define TOKENPASTE(x, y) x ## y +#define TOKENPASTE2(x, y) TOKENPASTE(x, y) + #define DEFINE_LINK_FACTORY(CNAME,CTYPE) \ \ Handle CNAME::factory(const Handle& base) \ @@ -123,7 +126,8 @@ Handle CNAME::factory(const Handle& base) \ } \ \ /* This runs when the shared lib is loaded. */ \ -static __attribute__ ((constructor)) void init(void) \ +static __attribute__ ((constructor)) void \ + TOKENPASTE2(init, __COUNTER__)(void) \ { \ classserver().addFactory(CTYPE, &CNAME::factory); \ } @@ -138,7 +142,8 @@ Handle CNAME::factory(const Handle& base) \ } \ \ /* This runs when the shared lib is loaded. */ \ -static __attribute__ ((constructor)) void init(void) \ +static __attribute__ ((constructor)) void \ + TOKENPASTE2(init, __COUNTER__)(void) \ { \ classserver().addFactory(CTYPE, &CNAME::factory); \ } diff --git a/opencog/atoms/core/PrenexLink.cc b/opencog/atoms/core/PrenexLink.cc index a52060605b..276484e12b 100644 --- a/opencog/atoms/core/PrenexLink.cc +++ b/opencog/atoms/core/PrenexLink.cc @@ -89,7 +89,9 @@ Handle PrenexLink::reassemble(Type prenex, // prenexed. Check for PutLink to avoid infinite recursion. if (PUT_LINK != prenex and not final_varlist.empty() and nameserver().isA(prenex, PRENEX_LINK)) + { return Handle(createLink(prenex, vdecl, newbod)); + } // Otherwise, we are done with the beta-reduction. return newbod; diff --git a/opencog/atoms/core/TruthValueOfLink.cc b/opencog/atoms/core/TruthValueOfLink.cc index a847d4715d..a39b2ef37d 100644 --- a/opencog/atoms/core/TruthValueOfLink.cc +++ b/opencog/atoms/core/TruthValueOfLink.cc @@ -61,6 +61,102 @@ ValuePtr TruthValueOfLink::execute() const return ValueCast(_outgoing[0]->getTruthValue()); } +// ============================================================= + +StrengthOfLink::StrengthOfLink(const HandleSeq& oset, Type t) + : ValueOfLink(oset, t) +{ + if (not nameserver().isA(t, STRENGTH_OF_LINK)) + { + const std::string& tname = nameserver().getTypeName(t); + throw InvalidParamException(TRACE_INFO, + "Expecting an StrengthOfLink, got %s", tname.c_str()); + } +} + +StrengthOfLink::StrengthOfLink(const Link &l) + : ValueOfLink(l) +{ + // Type must be as expected + Type tscope = l.get_type(); + if (not nameserver().isA(tscope, STRENGTH_OF_LINK)) + { + const std::string& tname = nameserver().getTypeName(tscope); + throw InvalidParamException(TRACE_INFO, + "Expecting an StrengthOfLink, got %s", tname.c_str()); + } +} + +// --------------------------------------------------------------- + +/// When executed, this will return the Strengths of all of the +/// atoms in the outgoing set. +ValuePtr StrengthOfLink::execute() const +{ + std::vector strengths; + + for (const Handle& h : _outgoing) + { + // Cannot take the strength of an ungrounded variable. + Type t = h->get_type(); + if (VARIABLE_NODE == t or GLOB_NODE == t) + return get_handle(); + + strengths.push_back(h->getTruthValue()->get_mean()); + } + + return createFloatValue(strengths); +} + +// ============================================================= + +ConfidenceOfLink::ConfidenceOfLink(const HandleSeq& oset, Type t) + : ValueOfLink(oset, t) +{ + if (not nameserver().isA(t, CONFIDENCE_OF_LINK)) + { + const std::string& tname = nameserver().getTypeName(t); + throw InvalidParamException(TRACE_INFO, + "Expecting an ConfidenceOfLink, got %s", tname.c_str()); + } +} + +ConfidenceOfLink::ConfidenceOfLink(const Link &l) + : ValueOfLink(l) +{ + // Type must be as expected + Type tscope = l.get_type(); + if (not nameserver().isA(tscope, CONFIDENCE_OF_LINK)) + { + const std::string& tname = nameserver().getTypeName(tscope); + throw InvalidParamException(TRACE_INFO, + "Expecting an ConfidenceOfLink, got %s", tname.c_str()); + } +} + +// --------------------------------------------------------------- + +/// When executed, this will return the Confidences of all of the +/// atoms in the outgoing set. +ValuePtr ConfidenceOfLink::execute() const +{ + std::vector confids; + + for (const Handle& h : _outgoing) + { + // Cannot take the confidence of an ungrounded variable. + Type t = h->get_type(); + if (VARIABLE_NODE == t or GLOB_NODE == t) + return get_handle(); + + confids.push_back(h->getTruthValue()->get_confidence()); + } + + return createFloatValue(confids); +} + DEFINE_LINK_FACTORY(TruthValueOfLink, TRUTH_VALUE_OF_LINK) +DEFINE_LINK_FACTORY(StrengthOfLink, STRENGTH_OF_LINK) +DEFINE_LINK_FACTORY(ConfidenceOfLink, CONFIDENCE_OF_LINK) /* ===================== END OF FILE ===================== */ diff --git a/opencog/atoms/core/TruthValueOfLink.h b/opencog/atoms/core/TruthValueOfLink.h index 1bb9d8ac88..77627ea2c8 100644 --- a/opencog/atoms/core/TruthValueOfLink.h +++ b/opencog/atoms/core/TruthValueOfLink.h @@ -39,7 +39,7 @@ class TruthValueOfLink : public ValueOfLink TruthValueOfLink(const HandleSeq&, Type=TRUTH_VALUE_OF_LINK); TruthValueOfLink(const Link &l); - // Return a pointer to the atom being specified. + // Return a pointer to the extracted value. virtual ValuePtr execute() const; static Handle factory(const Handle&); @@ -53,6 +53,56 @@ static inline TruthValueOfLinkPtr TruthValueOfLinkCast(AtomPtr a) #define createTruthValueOfLink std::make_shared +// ==================================================================== + +/// The StrengthOfLink returns the strength of a truth value on the +/// indicated atom. (Strength is the first of the sequence of floats). +/// +class StrengthOfLink : public ValueOfLink +{ +public: + StrengthOfLink(const HandleSeq&, Type=STRENGTH_OF_LINK); + StrengthOfLink(const Link &l); + + // Return a pointer to the extracted value. + virtual ValuePtr execute() const; + + static Handle factory(const Handle&); +}; + +typedef std::shared_ptr StrengthOfLinkPtr; +static inline StrengthOfLinkPtr StrengthOfLinkCast(const Handle& h) + { return std::dynamic_pointer_cast(h); } +static inline StrengthOfLinkPtr StrengthOfLinkCast(AtomPtr a) + { return std::dynamic_pointer_cast(a); } + +#define createStrengthOfLink std::make_shared + +// ==================================================================== + +/// The ConfidenceOfLink returns the strength of a truth value on the +/// indicated atom. (Confidence is the first of the sequence of floats). +/// +class ConfidenceOfLink : public ValueOfLink +{ +public: + ConfidenceOfLink(const HandleSeq&, Type=CONFIDENCE_OF_LINK); + ConfidenceOfLink(const Link &l); + + // Return a pointer to the extracted value. + virtual ValuePtr execute() const; + + static Handle factory(const Handle&); +}; + +typedef std::shared_ptr ConfidenceOfLinkPtr; +static inline ConfidenceOfLinkPtr ConfidenceOfLinkCast(const Handle& h) + { return std::dynamic_pointer_cast(h); } +static inline ConfidenceOfLinkPtr ConfidenceOfLinkCast(AtomPtr a) + { return std::dynamic_pointer_cast(a); } + +#define createConfidenceOfLink std::make_shared + /** @}*/ } diff --git a/opencog/atoms/execution/EvaluationLink.cc b/opencog/atoms/execution/EvaluationLink.cc index 722032a497..e29a9dcaf2 100644 --- a/opencog/atoms/execution/EvaluationLink.cc +++ b/opencog/atoms/execution/EvaluationLink.cc @@ -98,8 +98,35 @@ EvaluationLink::EvaluationLink(const Link& l) "Expecting an EvaluationLink"); } -// Pattern matching hack. The pattern matcher returns sets of atoms; -// if that set contains a single number, then unwrap it. +/// We get exceptions in two differet ways: (a) due to user error, +/// in which case we need to report the error to the user, and +/// (b) occasionally expected errors, which might occur during normal +/// processing, and should be ignored. The "normal" errors should not +/// be reported to the user; nor should they be printed to the log-file. +/// Using a try-catch block is enough to prevent them from being passed +/// to the user; but it is not enough to prevent them from printing. +/// Thus, we use a bool flag to not print. (It would be nice if C++ +/// offered a way to automate this in the catch-block, so that the +/// pesky "silent" flag was not needed.) +/// +/// DefaultPatternMatchCB.cc and also Instantiator.cc both catch +/// the NotEvaluatableException thrown here. Basically, these +/// know that they might be sending non-evaluatable atoms here, and +/// don't want to garbage up the log files with bogus errors. +/// +void throwSyntaxException(bool silent, const char* message...) +{ + if (silent) + throw NotEvaluatableException(); + va_list args; + va_start(args, message); + throw SyntaxException(TRACE_INFO, message, args); + va_end(args); +} + +/// Pattern matching hack. The pattern matcher returns sets of atoms; +/// if that set contains a single number, then unwrap it. +/// See issue #1502 which proposes to eliminate this SetLink hack. static NumberNodePtr unwrap_set(Handle h) { if (SET_LINK == h->get_type()) @@ -125,6 +152,8 @@ static NumberNodePtr unwrap_set(Handle h) return na; } +/// Extract a single floating-point double out of a value expected to +/// contain a number. static double get_numeric_value(const ValuePtr& pap) { Type t = pap->get_type(); @@ -147,12 +176,12 @@ static double get_numeric_value(const ValuePtr& pap) pap->to_string().c_str()); } -// Perform a GreaterThan check +/// Perform a GreaterThan check static TruthValuePtr greater(AtomSpace* as, const Handle& h) { const HandleSeq& oset = h->getOutgoingSet(); if (2 != oset.size()) - throw RuntimeException(TRACE_INFO, + throw SyntaxException(TRACE_INFO, "GreaterThankLink expects two arguments"); Instantiator inst(as); @@ -173,7 +202,7 @@ static TruthValuePtr identical(const Handle& h) { const HandleSeq& oset = h->getOutgoingSet(); if (2 != oset.size()) - throw RuntimeException(TRACE_INFO, + throw SyntaxException(TRACE_INFO, "IdenticalLink expects two arguments"); if (oset[0] == oset[1]) @@ -187,7 +216,7 @@ static TruthValuePtr equal(AtomSpace* as, const Handle& h, bool silent) { const HandleSeq& oset = h->getOutgoingSet(); if (2 != oset.size()) - throw RuntimeException(TRACE_INFO, + throw SyntaxException(TRACE_INFO, "EqualLink expects two arguments"); Instantiator inst(as); @@ -200,6 +229,65 @@ static TruthValuePtr equal(AtomSpace* as, const Handle& h, bool silent) return TruthValue::FALSE_TV(); } +static HandleSeq get_seq(const Handle& cargs) +{ + if (LIST_LINK == cargs->get_type()) return cargs->getOutgoingSet(); + return HandleSeq(1, cargs); +} + +/// Evalaute a formula defined by a PREDICATE_FORMULA_LINK +static TruthValuePtr eval_formula(const Handle& predform, + const Handle& cargs) +{ + // Collect up two floating point values. + std::vector nums; + for (const Handle& h: predform->getOutgoingSet()) + { + // An ordinary number. + if (NUMBER_NODE == h->get_type()) + { + nums.push_back(NumberNodeCast(h)->get_value()); + continue; + } + + // In case the user wanted to wrap everything in a + // LambdaLink. I don't understand why this is needed, + // but it seems to make some people feel better, so + // we support it. + Handle flh(h); + if (LAMBDA_LINK == h->get_type()) + { + // Set flh and fall through, where it is executed. + flh = LambdaLinkCast(h)->beta_reduce(get_seq(cargs)); + } + + // At this point, we expect a FunctionLink of some kind. + if (not nameserver().isA(flh->get_type(), FUNCTION_LINK)) + throw SyntaxException(TRACE_INFO, "Expecting a FunctionLink"); + + // If the FunctionLink has free variables in it, + // reduce them with the provided arguments. + FunctionLinkPtr flp(FunctionLinkCast(flh)); + const FreeVariables& fvars = flp->get_vars(); + if (not fvars.empty()) + { + flh = fvars.substitute_nocheck(flh, get_seq(cargs)); + flp = FunctionLinkCast(flh); + } + + // Expecting a FunctionLink without variables. + ValuePtr v(flp->execute()); + FloatValuePtr fv(FloatValueCast(v)); + nums.push_back(fv->value()[0]); + } + + // XXX FIXME; if we are given more than two floats, then + // perhaps we should create some other kind of TruthValue? + // Maybe a distributional one ?? Or a CountTV ?? + return createSimpleTruthValue(nums); +} + + static bool is_evaluatable_sat(const Handle& satl) { if (1 != satl->get_arity()) @@ -300,7 +388,9 @@ TruthValuePtr EvaluationLink::do_eval_scratch(AtomSpace* as, Instantiator inst(scratch); Handle args(HandleCast(inst.execute(sna.at(1), silent))); - return do_evaluate(scratch, sna.at(0), args, silent); + TruthValuePtr tvp(do_evaluate(scratch, sna.at(0), args, silent)); + evelnk->setTruthValue(tvp); + return tvp; } else if (IDENTICAL_LINK == t) { @@ -505,6 +595,27 @@ TruthValuePtr EvaluationLink::do_eval_scratch(AtomSpace* as, { return evelnk->getTruthValue(); } + else if (PREDICATE_FORMULA_LINK == t) + { + // A shortened, argument-free version of eval_formula() + std::vector nums; + for (const Handle& h: evelnk->getOutgoingSet()) + { + if (NUMBER_NODE == h->get_type()) + { + nums.push_back(NumberNodeCast(h)->get_value()); + continue; + } + + if (not nameserver().isA(h->get_type(), FUNCTION_LINK)) + throw SyntaxException(TRACE_INFO, "Expecting a FunctionLink"); + + ValuePtr v(FunctionLinkCast(h)->execute()); + FloatValuePtr fv(FloatValueCast(v)); + nums.push_back(fv->value()[0]); + } + return createSimpleTruthValue(nums); + } else if (TRUTH_VALUE_OF_LINK == t) { // If the truth value of the link is being requested, @@ -530,22 +641,11 @@ TruthValuePtr EvaluationLink::do_eval_scratch(AtomSpace* as, return TruthValueCast(pap); } - // We get exceptions here in two differet ways: (a) due to user - // error, in which case we need to print an error, and (b) intentionally, - // e.g. when Instantiator calls us, knowing it will get an error, - // in which case, printing the exception message is a waste of CPU - // time... - // - // DefaultPatternMatchCB.cc and also Instantiator wants to catch - // the NotEvaluatableException thrown here. Basically, these - // know that they might be sending non-evaluatable atoms here, and - // don't want to garbage up the log files with bogus errors. - if (silent) - throw NotEvaluatableException(); - - throw SyntaxException(TRACE_INFO, + throwSyntaxException(silent, "Either incorrect or not implemented yet. Cannot evaluate %s", evelnk->to_string().c_str()); + + return TruthValuePtr(); // not reached } TruthValuePtr EvaluationLink::do_evaluate(AtomSpace* as, @@ -568,7 +668,7 @@ TruthValuePtr EvaluationLink::do_evaluate(AtomSpace* as, { if (2 != sna.size()) { - throw RuntimeException(TRACE_INFO, + throw SyntaxException(TRACE_INFO, "Incorrect arity for an EvaluationLink!"); } return do_evaluate(as, sna[0], sna[1], silent); @@ -603,22 +703,31 @@ TruthValuePtr EvaluationLink::do_evaluate(AtomSpace* as, dtype = defn->get_type(); } + if (PREDICATE_FORMULA_LINK == dtype) + { + return eval_formula(defn, cargs); + } + // If its not a LambdaLink, then I don't know what to do... if (LAMBDA_LINK != dtype) - throw RuntimeException(TRACE_INFO, + throw SyntaxException(TRACE_INFO, "Expecting definition to be a LambdaLink, got %s", defn->to_string().c_str()); // Treat it as if it were a PutLink -- perform the // beta-reduction, and evaluate the result. LambdaLinkPtr lam(LambdaLinkCast(defn)); - Type atype = cargs->get_type(); - Handle reduct = lam->beta_reduce(atype == LIST_LINK ? - cargs->getOutgoingSet() - : HandleSeq(1, cargs)); + Handle reduct = lam->beta_reduce(get_seq(cargs)); return do_evaluate(as, reduct, silent); } + // Like a GPN, but the entire function is declared in the + // AtomSpace. + if (PREDICATE_FORMULA_LINK == pntype) + { + return eval_formula(pn, cargs); + } + if (GROUNDED_PREDICATE_NODE != pntype) { // Throw a silent exception; this is called in some try..catch blocks. @@ -672,7 +781,8 @@ TruthValuePtr EvaluationLink::do_evaluate(AtomSpace* as, return applier->apply_tv(schema.substr(pos), args); #else throw RuntimeException(TRACE_INFO, - "Cannot evaluate scheme GroundedPredicateNode!"); + "This binary does not have scheme support in it; " + "Cannot evaluate scheme GroundedPredicateNode!"); #endif /* HAVE_GUILE */ } @@ -688,7 +798,8 @@ TruthValuePtr EvaluationLink::do_evaluate(AtomSpace* as, return applier.apply_tv(as, schema.substr(pos), args); #else throw RuntimeException(TRACE_INFO, - "Cannot evaluate python GroundedPredicateNode!"); + "This binary does not have python support in it; " + "Cannot evaluate python GroundedPredicateNode!"); #endif /* HAVE_CYTHON */ } @@ -710,18 +821,13 @@ TruthValuePtr EvaluationLink::do_evaluate(AtomSpace* as, result = *res; free(res); } - if (nullptr == result) { - // If silent is true, return a simpler and non-logged - // exception, which may, in some contexts, be considerably - // faster than the one below. - if (silent) - throw NotEvaluatableException(); - - throw RuntimeException(TRACE_INFO, - "Invalid return value from predicate %s\nArgs: %s", - pn->to_string().c_str(), - cargs->to_string().c_str()); - } + + if (nullptr == result) + throwSyntaxException(silent, + "Invalid return value from predicate %s\nArgs: %s", + pn->to_string().c_str(), + cargs->to_string().c_str()); + return result; } diff --git a/opencog/atoms/execution/Instantiator.cc b/opencog/atoms/execution/Instantiator.cc index 6844571849..42b68ae88b 100644 --- a/opencog/atoms/execution/Instantiator.cc +++ b/opencog/atoms/execution/Instantiator.cc @@ -271,7 +271,9 @@ Handle Instantiator::walk_tree(const Handle& expr, bool silent) else { try { - EvaluationLink::do_evaluate(_as, plo, true); + TruthValuePtr tvp = + EvaluationLink::do_evaluate(_as, plo, true); + plo->setTruthValue(tvp); } catch (const NotEvaluatableException& ex) {} unwrap.push_back(plo); @@ -493,6 +495,14 @@ Handle Instantiator::walk_tree(const Handle& expr, bool silent) return beta_reduce(expr, *_vmap); } + // Do not reduce PredicateFormulaLink. That is because it contains + // formulas that we will need to re-evaluate in the future, so we + // must not clobber them. + if (PREDICATE_FORMULA_LINK == t) + { + return expr; + } + // If an atom is wrapped by the DontExecLink, then unwrap it, // beta-reduce it, but don't execute it. Consume the DontExecLink. // Actually, don't consume it. See discussion at issue #1303. diff --git a/tests/atoms/CMakeLists.txt b/tests/atoms/CMakeLists.txt index adfd4836c8..4fbbec9e5e 100644 --- a/tests/atoms/CMakeLists.txt +++ b/tests/atoms/CMakeLists.txt @@ -1,6 +1,9 @@ ADD_SUBDIRECTORY (truthvalue) + IF(HAVE_GUILE) + ADD_CXXTEST(HashUTest) + TARGET_LINK_LIBRARIES(HashUTest smob atomspace) ADD_CXXTEST(FreeLinkUTest) TARGET_LINK_LIBRARIES(FreeLinkUTest smob atomspace) ADD_CXXTEST(MapLinkUTest) @@ -19,8 +22,8 @@ IF(HAVE_GUILE) TARGET_LINK_LIBRARIES(PutLinkUTest execution smob atomspace) ADD_CXXTEST(QuotationUTest) TARGET_LINK_LIBRARIES(QuotationUTest execution smob atomspace) - ADD_CXXTEST(HashUTest) - TARGET_LINK_LIBRARIES(HashUTest smob atomspace) + ADD_CXXTEST(FormulaUTest) + TARGET_LINK_LIBRARIES(FormulaUTest execution smob atomspace) ENDIF(HAVE_GUILE) ADD_CXXTEST(AlphaConvertUTest) diff --git a/tests/atoms/FormulaUTest.cxxtest b/tests/atoms/FormulaUTest.cxxtest new file mode 100644 index 0000000000..911e01f6ba --- /dev/null +++ b/tests/atoms/FormulaUTest.cxxtest @@ -0,0 +1,293 @@ +/* + * tests/atoms/FormulaUTest.cxxtest + * + * Copyright (C) 2019 Linas Vepstas + * 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 +#include +#include +#include + +#include + +using namespace opencog; + +class FormulaUTest : public CxxTest::TestSuite +{ +private: + AtomSpace _as; + SchemeEval _eval; + +public: + FormulaUTest() : _eval(&_as) + { + logger().set_timestamp_flag(false); + logger().set_print_to_stdout_flag(true); + + _eval.eval("(add-to-load-path \"" PROJECT_SOURCE_DIR "\")"); + _eval.eval("(load-from-path \"tests/atoms/formulas.scm\")"); + } + + void setUp() {} + + void tearDown() {} + + void test_strength_of(); + void test_formula(); + void test_evaluation(); + void test_evalform(); + void test_putlink(); + void test_define(); +}; + +#define MAXERR 1.0e-12 + +void FormulaUTest::test_strength_of() +{ + logger().info("BEGIN TEST: %s", __FUNCTION__); + + _eval.eval("(Concept \"A\" (stv 0.8 1.0))"); + _eval.eval("(Concept \"B\" (stv 0.6 0.9))"); + + ValuePtr sof = _eval.eval_v("(cog-execute! (StrengthOf (Concept \"A\")))"); + printf("Get strenght_of=%s\n", sof->to_string().c_str()); + TS_ASSERT_EQUALS(sof->get_type(), FLOAT_VALUE); + FloatValuePtr fvp = FloatValueCast(sof); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.8), MAXERR); + + ValuePtr cof = _eval.eval_v("(cog-execute! (ConfidenceOf (Concept \"B\")))"); + printf("Get conf_of=%s\n", cof->to_string().c_str()); + TS_ASSERT_EQUALS(cof->get_type(), FLOAT_VALUE); + fvp = FloatValueCast(cof); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.9), MAXERR); + + ValuePtr pof = _eval.eval_v("(cog-execute! prod)"); + printf("Get product=%s\n", pof->to_string().c_str()); + TS_ASSERT_EQUALS(pof->get_type(), FLOAT_VALUE); + fvp = FloatValueCast(pof); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.48), MAXERR); + + logger().info("END TEST: %s", __FUNCTION__); +} + +void FormulaUTest::test_formula() +{ + logger().info("BEGIN TEST: %s", __FUNCTION__); + + TruthValuePtr tvp = _eval.eval_tv("(cog-evaluate! stv-const)"); + printf("Get stv-const=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + FloatValuePtr fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.7), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.314), MAXERR); + + tvp = _eval.eval_tv("(cog-evaluate! formula-stv)"); + printf("Get formula-stv=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.52), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9), MAXERR); + + logger().info("END TEST: %s", __FUNCTION__); +} + +void FormulaUTest::test_evaluation() +{ + logger().info("BEGIN TEST: %s", __FUNCTION__); + + TruthValuePtr tvp = _eval.eval_tv("(cog-tv my-ev-link)"); + printf("Get before-ev-stv=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + FloatValuePtr fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 1.0), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.0), MAXERR); + + tvp = _eval.eval_tv("(cog-evaluate! my-ev-link)"); + printf("Get eval-tv=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.75), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.628), MAXERR); + + tvp = _eval.eval_tv("(cog-tv my-ev-link)"); + printf("Get after-eval-stv=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.75), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.628), MAXERR); + + logger().info("END TEST: %s", __FUNCTION__); +} + +void FormulaUTest::test_evalform() +{ + logger().info("BEGIN TEST: %s", __FUNCTION__); + + TruthValuePtr tvp = _eval.eval_tv("(cog-tv eval-formula)"); + printf("Get before-ev-form=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + FloatValuePtr fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 1.0), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.0), MAXERR); + + tvp = _eval.eval_tv("(cog-evaluate! eval-formula)"); + printf("Get eval-formula=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.52), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9), MAXERR); + + tvp = _eval.eval_tv("(cog-tv eval-formula)"); + printf("Get after-eval-form=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.52), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9), MAXERR); + + // ------------------ + tvp = _eval.eval_tv("(cog-tv eval-lambda)"); + printf("Get before-ev-lambda=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 1.0), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.0), MAXERR); + + tvp = _eval.eval_tv("(cog-evaluate! eval-lambda)"); + printf("Get eval-lambda=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.52), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9), MAXERR); + + tvp = _eval.eval_tv("(cog-tv eval-lambda)"); + printf("Get after-eval-lambda=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.52), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9), MAXERR); + + logger().info("END TEST: %s", __FUNCTION__); +} + +void FormulaUTest::test_putlink() +{ + logger().info("BEGIN TEST: %s", __FUNCTION__); + + Handle hset = _eval.eval_h("(cog-execute! put-link)"); + Handle hevo = hset->getOutgoingAtom(0); + + TruthValuePtr tvp = hevo->getTruthValue(); + printf("Putter result=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + FloatValuePtr fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.52), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9), MAXERR); + + _eval.eval("(define putex (cog-execute! put-link))"); + _eval.eval("(define pevu (cog-outgoing-atom putex 0))"); + + // -------------- + _eval.eval("(Concept \"A\" (stv 0.3 0.5))"); + _eval.eval("(Concept \"B\" (stv 0.4 0.5))"); + + tvp = _eval.eval_tv("(cog-evaluate! pevu)"); + printf("New pevu=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.88), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.25), MAXERR); + + tvp = _eval.eval_tv("(cog-tv pevu)"); + printf("Pevu-tv=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.88), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.25), MAXERR); + + // -------------- + _eval.eval("(Concept \"A\" (stv 0.1 0.99))"); + _eval.eval("(Concept \"B\" (stv 0.1 0.99))"); + + tvp = _eval.eval_tv("(cog-evaluate! pevu)"); + printf("New pevu=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.99), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9801), MAXERR); + + tvp = _eval.eval_tv("(cog-tv pevu)"); + printf("Pevu-tv=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.99), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9801), MAXERR); + + logger().info("END TEST: %s", __FUNCTION__); +} + +void FormulaUTest::test_define() +{ + logger().info("BEGIN TEST: %s", __FUNCTION__); + + TruthValuePtr tvp = _eval.eval_tv("(cog-tv red-form)"); + printf("Get before red-form=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + FloatValuePtr fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 1.0), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.0), MAXERR); + + // -------------- + _eval.eval("(Concept \"A\" (stv 0.3 0.5))"); + _eval.eval("(Concept \"B\" (stv 0.4 0.5))"); + + tvp = _eval.eval_tv("(cog-evaluate! red-form)"); + printf("New red-form=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.88), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.25), MAXERR); + + tvp = _eval.eval_tv("(cog-tv red-form)"); + printf("red-form-tv=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.88), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.25), MAXERR); + + // -------------- + _eval.eval("(Concept \"A\" (stv 0.2 0.98))"); + _eval.eval("(Concept \"B\" (stv 0.2 0.98))"); + + tvp = _eval.eval_tv("(cog-evaluate! red-form)"); + printf("New red-form=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.96), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9604), MAXERR); + + tvp = _eval.eval_tv("(cog-tv red-form)"); + printf("red-form-tv=%s\n", tvp->to_string().c_str()); + TS_ASSERT_EQUALS(tvp->get_type(), SIMPLE_TRUTH_VALUE); + fvp = FloatValueCast(ValueCast(tvp)); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[0] - 0.96), MAXERR); + TS_ASSERT_LESS_THAN(fabs(fvp->value()[1] - 0.9604), MAXERR); + + logger().info("END TEST: %s", __FUNCTION__); +} diff --git a/tests/atoms/formulas.scm b/tests/atoms/formulas.scm new file mode 100644 index 0000000000..17a3767b42 --- /dev/null +++ b/tests/atoms/formulas.scm @@ -0,0 +1,119 @@ +; +; formulas.scm -- Declaring formulas that compute truth values. +; +; This is a modified copy of an example program. It helps verify that +; the example actually works. +; +(use-modules (opencog) (opencog exec)) + +(define atom-a (Concept "A" (stv 0.8 1.0))) +(define atom-b (Concept "B" (stv 0.6 0.9))) + +; Multiply the strength of the TV's of two atoms. +(define prod + (Times (StrengthOf (Concept "A")) (StrengthOf (Concept "B")))) + +(define stv-const (PredicateFormula (Number 0.7) (Number 0.314))) + +(define formula-stv + (PredicateFormula + (Minus + (Number 1) + (Times (StrengthOf (Concept "A")) (StrengthOf (Concept "B")))) + (Times (ConfidenceOf (Concept "A")) (ConfidenceOf (Concept "B"))))) + +; The below computes a truth value, and attaches it to the +; EvaluationLink. +(define my-ev-link + (Evaluation + (PredicateFormula (Number 0.75) (Number 0.628)) + (List + (Concept "A") + (Concept "B")))) + +; Formula with variables +(define eval-formula + (Evaluation + ; Compute TV = (1-sA*sB, cA*cB) + (PredicateFormula + (Minus + (Number 1) + (Times + (StrengthOf (Variable "$X")) + (StrengthOf (Variable "$Y")))) + (Times + (ConfidenceOf (Variable "$X")) + (ConfidenceOf (Variable "$Y")))) + (List + (Concept "A") + (Concept "B")))) + +; Optionally, you can wrap formulas with LambdaLinks. This doesn't +; really change anything; formulas work fine without LambdaLinks. +(define eval-lambda + (Evaluation + ; Compute TV = (1-sA*sB, cA*cB) + (PredicateFormula + (Lambda + ; Lambda without a decl, intentionally so. + ; (NopeVariableList (Variable "$X") (Variable "$Y")) + (Minus + (Number 1) + (Times + (StrengthOf (Variable "$X")) + (StrengthOf (Variable "$Y"))))) + (Lambda + (VariableList (Variable "$X") (Variable "$Y")) + (Times + (ConfidenceOf (Variable "$X")) + (ConfidenceOf (Variable "$Y"))))) + (List + (Concept "A") + (Concept "B")))) + + +; Beta-reducation works as normal. The below will create an +; EvaluationLink with ConceptNode A and B in it, and will set the +; truth value according to the formula. +(define put-link + (PutLink + (VariableList (Variable "$VA") (Variable "$VB")) + (Evaluation + ; Compute TV = (1-sA*sB, cA*cB) + (PredicateFormula + (Minus + (Number 1) + (Times + (StrengthOf (Variable "$VA")) + (StrengthOf (Variable "$VB")))) + (Times + (ConfidenceOf (Variable "$VA")) + (ConfidenceOf (Variable "$VB")))) + (List + (Variable "$VA") (Variable "$VB"))) + (Set (List (Concept "A") (Concept "B"))))) + + +; One can also use DefinedPredicates, to give the formula a name. +(DefineLink + (DefinedPredicate "has a reddish color") + (PredicateFormula + (Minus + (Number 1) + (Times + (StrengthOf (Variable "$X")) + (StrengthOf (Variable "$Y")))) + (Times + (ConfidenceOf (Variable "$X")) + (ConfidenceOf (Variable "$Y"))))) + +(Concept "A" (stv 0.9 0.98)) +(Concept "B" (stv 0.9 0.98)) + +; The will cause the formula to evaluate. +(define red-form + (Evaluation + (DefinedPredicate "has a reddish color") + (List + (Concept "A") + (Concept "B"))))