Skip to content

Manually constructing a network

Nils Niemann edited this page Aug 12, 2020 · 4 revisions

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.

On network construction

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()
{
// ...

step 1

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;
// ...

step 2

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);
// ...

step 3

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);
// ...

step 4

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);
// ...

step 5

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);
// ...

step 6

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.

step 7

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:

img

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.

After retractiong t2

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

Clone this wiki locally