Tiramemsu

Reasoning with N3 · Chapter 3 of 10

Chapter 3

Your memory as N3

A reasoning run loads part of a view into eyeron and reads the conclusions back. This chapter shows what a rule sees: values, statements and their layers, the virtual tm: predicates, and how a run decides what to load.

Values become N3 terms

Values map to N3 the way bundle export renders them as RDF, and back again:

In memoryIn N3
An IRIthe IRI itself, e.g. v:alice
A literalthe literal: "Alice", 34, 0.9, "2020-01-01T00:00:00Z"^^xsd:dateTime
An anonymous node N<urn:tiramemsu:node:N>
A blank node N<urn:tiramemsu:bnode:N>
A statement with eid N<urn:tiramemsu:stmt:N>

The mapping is exact in both directions. A conclusion about <urn:tiramemsu:node:7> is about node 7, and a premise <urn:tiramemsu:stmt:1> is statement e1.

A statement is a triple and a name

Take a small memory. Statement e1 says Alice works at Acme, valid from 2020 to 2023. Statement e2 is a layer on e1: its confidence is 0.9.

A rule sees e1 as an ordinary triple, so { ?p v:worksAt ?c } matches it. It sees e2 as a triple whose subject is the IRI of e1:

@prefix v: <urn:tiramemsu:v:>.

v:alice v:worksAt v:acme.
<urn:tiramemsu:stmt:1> v:confidence 0.9.

That is enough for rules that only follow facts. A rule that reads a layer needs one more thing: from the name <urn:tiramemsu:stmt:1> it must get to what e1 says. That is the job of the virtual predicates.

The virtual predicates

For each loaded statement e, tiramemsu can offer three triples that are not stored anywhere:

what a rule sees for e1

@prefix v: <urn:tiramemsu:v:>.
@prefix tm: <urn:tiramemsu:tm:>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.

<urn:tiramemsu:stmt:1> tm:content { v:alice v:worksAt v:acme };
    tm:validFrom "2020-01-01T00:00:00Z"^^xsd:dateTime;
    tm:validTo "2023-01-01T00:00:00Z"^^xsd:dateTime.

The names match the SPARQL virtual predicates tm:validFrom and tm:validTo, so a rule and a query read validity the same way. With them, a rule can combine a layer with the fact it is about:

trusted.n3

@prefix v: <urn:tiramemsu:v:>.
@prefix tm: <urn:tiramemsu:tm:>.
@prefix math: <http://www.w3.org/2000/10/swap/math#>.

{ ?e v:confidence ?c.
  ?c math:greaterThan 0.8.
  ?e tm:content { ?s v:worksAt ?o }
} => { ?s v:trustedWorksAt ?o }.

The conclusion (v:alice v:trustedWorksAt v:acme) has two premises, [e2, e1]: the layer it matched directly, and the statement whose content it matched. Both are statement ids. If either is retracted, a stored conclusion goes with it (chapter 7).

Three rules govern the virtual triples:

Why IRIs and not quoted triples

N3 could name a statement by its content alone, as the formula { v:alice v:worksAt v:acme }. Tiramemsu does not, for two reasons. Two episodes of the same fact, Alice at Acme in 2020–22 and again from 2024, have the same content but are different statements with different valid times and layers. And a proof must point at the exact statement it used, so the conclusion can fall with it. Identity is the eid, so <urn:tiramemsu:stmt:N> it is.

Scopes: what a run may see

A run never loads the whole store by accident. It reads one scope of its view:

ScopeLoads
ReasonScope::All (default)every statement visible in the view
ReasonScope::Graphs(graphs)the members of the given named graphs
ReasonScope::Seeds { nodes, hops }statements whose subject or object lies within hops of the seed nodes
use tiramemsu::{ReasonOptions, ReasonScope, Rules};

// Reason only over what one session wrote.
let session = ReasonOptions {
    scope: ReasonScope::Graphs(vec![v("session-42")]),
    ..ReasonOptions::default()
};
let r = db.now().reason(Rules::Stored(v("mortality")), &session)?;

// Reason only around Alice, two hops out.
let around_alice = ReasonOptions {
    scope: ReasonScope::Seeds { nodes: vec![v("alice")], hops: 2 },
    ..ReasonOptions::default()
};

From the command line, --graph picks graphs and may be repeated:

tm reason memory.db --rule-set urn:tiramemsu:v:mortality --graph urn:tiramemsu:v:session-42

If (v:a rdf:type v:Human) is in graph v:g1 and (v:b rdf:type v:Human) is not, a run scoped to v:g1 concludes only that v:a is mortal.

Predicate pruning

Inside its scope, a run loads only statements whose predicate appears in a predicate position of some rule body. Forward rules, backward rules and formulas nested in bodies all count. The mortality rule mentions only rdf:type, so a memory with 100 v:name statements and 3 rdf:type v:Human statements loads 3.

A variable in a predicate position turns pruning off for the whole run, because the rule could match any predicate:

@prefix v: <urn:tiramemsu:v:>.

# Loads every statement of the scope: ?p could be anything.
{ ?s ?p ?o. ?o a v:Secret } => { ?s v:touches v:secret }.

Every result reports loaded, the number of statements loaded, and pruned, whether pruning applied. Check them when a run is slower than expected.

Name your predicates. A variable predicate inside a nested formula counts too. ?e tm:content { ?s ?p ?o } turns pruning off. ?e tm:content { ?s v:worksAt ?o } keeps it, and loads only v:worksAt statements and the layers your rule names.

Reading the result

reason returns the conclusions that are not already among the loaded statements, and facts about the run:

let r = db.now().reason(Rules::Stored(v("trusted")), &ReasonOptions::default())?;

println!("tx {}  now {}  loaded {}  pruned {}  segments {}",
         r.t, r.now, r.loaded, r.pruned, r.segments);
for c in &r.conclusions {
    // c.valid is the interval in which the conclusion holds (chapter 5)
    // c.proof.premises lists Premise::Statement(eid) or Premise::Conclusion(index)
    println!("{} {} {}  {:?}  rule {}  from {:?}",
             c.s, c.p, c.o, c.valid, c.proof.rule, c.proof.premises);
}
if let Some(x) = &r.contradiction { println!("fuse fired: rule {}", x.rule); }

A premise is either a loaded statement (by eid) or another conclusion of the same run (by index), so a chain of rules is a tree you can walk. The two fields t and now say which view and which instant the run used. They matter in the next chapter.