Tiramemsu

Reasoning with N3 · Chapter 5 of 10

Chapter 5

Time, part 2: valid time

Valid time says when a fact held in the world. A rule must never combine two facts that were never true at the same time. This chapter shows how tiramemsu makes sure of that, and what it costs.

The trap

Memory holds two facts with valid times:

and the rule from chapter 1:

where-people-work.n3

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

{ ?p v:worksAt ?c. ?c v:locatedIn ?l } => { ?p v:worksIn ?l }.

A reasoner that ignores valid time joins the two triples and concludes that Alice works in Berlin. But Alice left Acme two years before Acme was in Berlin. At no instant were both facts true, so at no instant does the conclusion follow. Tiramemsu concludes nothing.

The error is easy to make and hard to notice. Agent memories are full of facts with lifetimes: jobs, addresses, owners, prices, team memberships. A join across lifetimes produces confident nonsense.

Pointwise semantics

The rule. A conclusion holds at instant t if and only if it follows from the loaded statements that are valid at t.

Everything else in this chapter follows from that sentence. Change the first fact to [2020, 2025) and the two facts overlap during 2024. The only conclusion is (v:alice v:worksIn v:berlin) with interval [2024, 2025), the intersection. No rule has to mention time for this to happen.

It also holds for rules that look for something missing, which is where intersecting intervals after the fact would go wrong. Bob is an employee for all time and has a manager from 2023:

unmanaged.n3

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

{ ?x a v:Employee. ?SCOPE log:notIncludes { ?x v:manager ?m } } => { ?x a v:Unmanaged }.

Pointwise, Bob is unmanaged at every instant before 2023 and at none after. The conclusion is (v:bob rdf:type v:Unmanaged) with interval (−∞, 2023). A reasoner run once over all facts would find the manager and conclude nothing at all.

The segment sweep

Instants are too many to try one by one. They do not need to be. Between two consecutive valid-time bounds, the set of valid statements does not change, so neither do the conclusions. A run therefore:

  1. takes its window, by default the whole timeline;
  2. cuts it at every v_from and v_to of the loaded statements that falls inside it;
  3. runs eyeron once per segment, over the statements valid throughout that segment;
  4. merges a conclusion across adjacent segments when its content and its support are the same in both.
The segment sweep Statement e1, Alice works at Acme, is valid from 2020 to 2025. Statement e2, Acme is located in Berlin, is valid from 2024 on. Their bounds cut the timeline at 2020, 2024 and 2025 into four segments: before 2020 nothing is valid; from 2020 to 2024 only e1; from 2024 to 2025 both; from 2025 on only e2. The rule fires only in the segment where both hold, so Alice works in Berlin is concluded for 2024 to 2025. e1 · alice worksAt acme e2 · acme locatedIn berlin segments alice worksIn berlin nothing e1 only: no match e1 e2 e2 only [2024, 2025) 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 valid time →
Every valid-time bound of a loaded statement cuts the timeline. The rule runs once per segment, over the statements valid throughout it. It fires only where e1 and e2 are both valid.

The result reports the number of segments as segments. Statements with no valid time are valid in every segment and add no cuts. A memory that never uses valid time is one segment, one run.

Episodes

Step 4 merges only when the support is identical. That keeps conclusions honest when the same thing follows for different reasons:

teams.n3

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

{ ?p v:team ?t. ?t v:locatedIn ?l } => { ?p v:worksIn ?l }.

Alice worked in Berlin from 2020 to 2024 without a break. But the first two years rest on team v:a and the last two on team v:b. Tiramemsu reports two conclusions, [2020, 2022) and [2022, 2024), each with its own premises. These are two episodes, the same way memory keeps two episodes of a fact. If the membership in team v:a is retracted, the first episode goes and the second stays.

Snapshots

Often you only care about one instant. A valid_at view is one snapshot: a single run over the statements valid at that instant, no sweep. Every conclusion gets the interval [t, t + 1 ms), because that is all the run can vouch for.

// ms(): your date-to-epoch-ms helper
let june = db.now().valid_at(ms("2024-06-01"));
let r = june.reason(Rules::Stored(v("where-people-work")), &ReasonOptions::default())?;
// (v:alice v:worksIn v:berlin), valid [2024-06-01, 2024-06-01 + 1 ms)
tm reason memory.db --rule-set urn:tiramemsu:v:where-people-work --valid-at 1717200000000   # 2024-06-01

Snapshots combine with transaction time. db.as_of(TimeRef::Tx(150)).valid_at(t) asks what followed, at instant t in the world, from what was known at transaction 150.

Validity as data

Sometimes a rule needs the interval itself, not just the guarantee. tm:validFrom and tm:validTo (chapter 3) give a statement’s bounds as xsd:dateTime. For e1 valid [2020-01-01, 2023-01-01), a rule binding ?e tm:validFrom ?a. ?e tm:validTo ?b sees "2020-01-01T00:00:00Z"^^xsd:dateTime and "2023-01-01T00:00:00Z"^^xsd:dateTime. Recipe 1 computes tenure this way.

During a sweep, each segment also offers its own bounds as tm:run tm:segmentFrom ?f and tm:run tm:segmentTo ?t (bounded ends only). Use them sparingly: a conclusion that mentions a segment bound differs from segment to segment, so it never merges.

These are data, not filters. A conclusion computed from tm:validFrom still holds only while its premises are valid. The pointwise rule always applies.

Windows

The window option, unbounded by default, limits the sweep to part of the timeline. Cuts outside it are ignored, and conclusions are clipped to it:

let in_2020s = ReasonOptions {
    window: Valid::between(ms("2020-01-01"), ms("2030-01-01")),
    ..ReasonOptions::default()
};
tm reason memory.db --rule-set urn:tiramemsu:v:where-people-work --valid-during 1577836800000..1893456000000

Cost and the segment limit

A sweep costs about one reasoner run per segment. Three things keep that small:

Opting out

time: TimeMode::Atemporal ignores valid time. Every loaded statement takes part in one run, and conclusions are reported unbounded. Use it for data whose valid times do not mean anything to the rule, or when you really mean “ever, at any time”. It must be asked for by name, because the default should never be the trap.

You wantUseRuns
What follows at one instanta valid_at(t) viewone
When each conclusion held, all timethe default, TimeMode::Pointwiseone per segment
When each conclusion held, in a periodthe default with a windowone per segment in the window
What follows if time is ignoredTimeMode::Atemporalone