-
Notifications
You must be signed in to change notification settings - Fork 4
Manually constructing a network
NOTE: This is probably not what you are looking for! Setting up rete networks manually is a tedious task. A more convenient way is to use the RuleParser as described in the reasoning chapter.
If you really want to construct the rete network yourself, or just want to see how it is done internally by the rule parser, please continue reading. This example will construct a network to partly implement the following rule:
[(?a rdfs:subClassOf ?b), (?b rdfs:subClassOf ?c) -> (?a rdfs:subClassOf ?c)]
Partly in the sense that it implements the conditions and schedules a production node for the effect of the rule whenever a match is found, but it does not actually execute the effect. To do so you would need to iterate the networks agenda, or just use the reasoner that it provided with this project in the first place.
After constructing the network it adds the facts:
(A rdfs:subClassOf B) , (B rdfs:subClassOf C) and
(C rdfs:subClassOf D), and exports the reasoners state as a dot-file.
Note: This example is also included in the projects example folder, in
examples/ManualSetup.cpp.
In the next sections we will create a network for the above purpose step by
step. The complete example can be found in the projects example folder, i.e.
examples/ManualSetup.cpp.
We start with the usual stuff: Including the neccessary header files, and a convenience function for exporting the networks state as a dot-file.
#include <iostream>
#include <fstream>
#include <memory>
#include "../rete-core/ReteCore.hpp"
#include "../rete-rdf/ReteRDF.hpp"
#include "../rete-reasoner/Reasoner.hpp"
#include "../rete-reasoner/AssertedEvidence.hpp"
using namespace rete;
void save(Network& net, const std::string& filename)
{
std::ofstream out(filename);
out << net.toDot();
out.close();
}
int main()
{
// ...At first you need an instance of rete::Network. It contains the root node of
the network beneath which you will add your own nodes, the agenda onto which
productions are scheduled when a match is found, and a list of productions
which simple serves to keep the network alive.
// ...
/* ------------------ */
/* ----- STEP 1 ----- */
/* ------------------ */
Network net;
// ...Then you start creating alpha nodes to select only the relevant WMEs for your
use case. Here we are only interested in triples with a rdfs:subClassOf
predicate, and thus need only one alpha node for it, and attach it to the root
node of the network using rete::SetParent(parent, child).
// ...
/* ------------------ */
/* ----- STEP 2 ----- */
/* ------------------ */
// predicate check
auto foo = std::make_shared<TripleAlpha>(Triple::PREDICATE, "rdfs:subClassOf");
SetParent(net.getRoot(), foo);
// ...Both of our conditions work on the same type of triples, so we need a join that is connected with both sides to the results of the previous alpha node. But joins can only be connected to beta memories on the left, and alpha memories on the right. Hence we now connect an alpha memory to the alpha node from step 2, and below that a beta memory, through an alpha-beta-adapter node.
// ...
/* ------------------ */
/* ----- STEP 3 ----- */
/* ------------------ */
auto foomem = std::make_shared<AlphaMemory>();
SetParent(foo, foomem);
// adapter for first (and only) join
auto adapter = std::make_shared<AlphaBetaAdapter>();
SetParents(nullptr, foo->getAlphaMemory(), adapter);
auto adapterMem = std::make_shared<BetaMemory>();
SetParent(adapter, adapterMem);
// ...Next we need the join node that combines our two conditions. Since the object
of one triple must equal the subject of the other (same value for ?b), we
need to tell the rete::GenericJoin about this constraint. This is a bit
complicated, as these checks can be performed on different datatypes (what the
variables refer to), and on values from different types of WME. To allow this
flexibility, we use accessor objects: rete::TripleAccessor is used to extract
a value from a triple WME, as defined in its constructor. The index of the
accessor defines how far back in a token it should look for the WME -- the
default value of -1 signals that the accessor should be used on a WME directly
(as in an alpha memory), while 0 is the latest addition to the token.
By adding the accessors to the join node with join->addCheck the join will
only pass through combinations where the values that the accessors point to are
equal.
The join node is connected to the alpha and beta memory from step 3 by using
rete::SetParents(leftParent, rightParent, child).
And of course, the join node will need a beta memory for its output, so we add
that, too.
// ...
/* ------------------ */
/* ----- STEP 4 ----- */
/* ------------------ */
// join where object of the most recent wme in the token matches the subject of the wme
TripleAccessor::Ptr acc0(new TripleAccessor(Triple::OBJECT));
acc0->index() = 0;
TripleAccessor::Ptr acc1(new TripleAccessor(Triple::SUBJECT));
auto join = std::make_shared<GenericJoin>();
join->addCheck(acc0, acc1);
SetParents(adapter->getBetaMemory(), foo->getAlphaMemory(), join);
auto joinmem = std::make_shared<BetaMemory>();
SetParent(join, joinmem);
// ...Next we want a production that creates new triples as stated in our rule, the
rete::InferTriple. It makes use of accessors for the same reason as the
generic join from step 4. Note that it is not a node -- we still need a
production node that holds the production and decides what to do with it on a
match. We just use the rete::AgendaNode which schedules productions to the
agenda of the network, but does not execute them itself.
// ...
/* ------------------ */
/* ----- STEP 5 ----- */
/* ------------------ */
std::unique_ptr<AccessorBase> accA(new TripleAccessor(Triple::SUBJECT));
accA->index() = 1;
std::unique_ptr<AccessorBase> accC(new TripleAccessor(Triple::OBJECT));
accC->index() = 0;
// the consequence: construct (C1.?a rdfs:subClassOf C2.?c)
auto accB = std::unique_ptr<AccessorBase>(
new ConstantAccessor<TriplePart>({"rdfs:subClassOf"}));
InferTriple::Ptr infer(new InferTriple(
std::move(accA),
std::move(accB),
std::move(accC)
));
// create an AgendaNode for the production
auto inferNode = std::make_shared<AgendaNode>(infer, net.getAgenda());
rete::SetParent(join->getBetaMemory(), inferNode);
// ...Lastly, we add the agenda node to the network using net.addProduction(inferNode. This does nothing more than adding the node to a vector of std::shared_ptr, and thus makes sure that the node and all its ancestors are kept alive together with the network.
This has been changed in favour to the RuleParser (more on that in later chapters). The rete::Network does no longer store productions, you will need to keep these nodes alive yourself. When construction networks from a textual representation of rules, the parser will return objects that contain information about the parsed rules and also keep the production nodes alive.
By activating the root node with the created WMEs and the
PropagationFlag::ASSERT, the data is processed through the network and the
found matches are handed over to the production nodes. Please note that the
InferTriple did not take effect, yet: It is put on the agenda, but not
actually executed. But of course you can iterate the agenda and process
the scheduled effects, or write your own production nodes that work without an
Agenda.
// ...
/* ------------------ */
/* ----- STEP 7 ----- */
/* ------------------ */
// put in some data
auto t1 = std::make_shared<Triple>("A", "rdfs:subClassOf", "B");
auto t2 = std::make_shared<Triple>("B", "rdfs:subClassOf", "C");
auto t3 = std::make_shared<Triple>("C", "rdfs:subClassOf", "D");
net.getRoot()->activate(t1, PropagationFlag::ASSERT);
net.getRoot()->activate(t2, PropagationFlag::ASSERT);
net.getRoot()->activate(t3, PropagationFlag::ASSERT);
save(net, "manual_setup_add.dot");
net.getRoot()->activate(t2, PropagationFlag::RETRACT);
save(net, "manual_setup_retract.dot");
return 0;
}This is what the internal state of network looks like after asserting the data:

When we retract t2 the matches are no longer valid -- they are removed from
from the beta memory and the production node is informed about the change.
The AgendaNode in this case removes the previously scheduled effect from the
agenda. If we had already executed it, it would schedule InferTriple tagged
with the PropagationFlag::RETRACT, so that we can react to the change.

- [Overview](rete/Rete algorithm in C++)
- Implementation notes
- [Usage / Examples](rete/Manually constructing a network)
- [Overview](reasoner/Rule based forward reasoner)
- [Examples](reasoner/How to use the reasoner)
- [How to extend it](reasoner/How to extend the reasoner)