A qualitative constraint network over Allen relations, with the path-consistency propagation of Allen (1983) §3.
Where every interval is dated, Tempo.relation/2 answers directly.
This module is for the case where they are not: you know how some
periods relate to each other, not when any of them happened, and want
to know what follows. Each pair of periods carries a set of the
relations still possible; asserting a constraint narrows a set, and
propagation narrows every other set that the assertion implies.
Not Tempo.Network
Tempo.Network solves the Simple Temporal Problem — metric
constraints (at_least: 20, unit: :year) over dated periods, by
shortest paths. It is complete and polynomial, and its relation
vocabulary is convex: it cannot represent "before or after".
This module is the qualitative counterpart. It has no dates and no
metric constraints, and it can represent disjunction — which is
exactly why it is incomplete (below). Use Tempo.Network when you have
bounds and want dates; use this when you have relations and want
relations.
Sound but incomplete
propagate/1 computes path consistency, not satisfiability.
Every relation it removes is genuinely impossible. Pruning is sound, so a surviving set is an over-approximation you can trust as an upper bound on what might hold.
A network that propagates cleanly may still be unsatisfiable. Deciding satisfiability for the full algebra is NP-complete (Vilain & Kautz 1986; Nebel & Bürckert 1995), and path consistency is the standard polynomial approximation.
So {:ok, network} means "no contradiction was found", not "a
consistent assignment exists". {:error, {:inconsistent, pair}} is
definitive in the other direction: an empty set is a real contradiction.
Example
iex> alias Tempo.Interval.RelationNetwork, as: Net
iex> net =
...> Net.new([:fire, :rebuild, :occupation])
...> |> Net.constrain(:fire, [:precedes], :rebuild)
...> |> Net.constrain(:rebuild, [:during], :occupation)
iex> {:ok, net} = Net.propagate(net)
iex> Net.between(net, :fire, :occupation)
[:precedes, :meets, :overlaps, :starts, :during]The fire precedes the rebuild, and the rebuild happened during the occupation — so the fire is somewhere before the occupation ended, but which of those five relations holds is not determined by what was asserted.
Summary
Types
A set of Allen's relations, in Allen's order: what may hold between two intervals.
Functions
The relations still possible from a to b.
Whether propagation finds no contradiction.
Narrow what is possible from a to b by relations.
A network over labels, with nothing asserted between any pair.
Propagate every constraint to a fixpoint.
Types
Functions
The relations still possible from a to b.
Arguments
networkis at/0.aandbare labels in the network.
Returns
The relation set, in Allen's canonical order.
[:equals]whenaandbare the same label.{:error, {:unknown_label, label}}when a label is not in the network.
Examples
iex> net = Tempo.Interval.RelationNetwork.new([:a, :b])
iex> net = Tempo.Interval.RelationNetwork.constrain(net, :a, [:meets], :b)
iex> Tempo.Interval.RelationNetwork.between(net, :a, :b)
[:meets]
iex> net = Tempo.Interval.RelationNetwork.new([:a, :b])
iex> Tempo.Interval.RelationNetwork.between(net, :a, :zzz)
{:error, {:unknown_label, :zzz}}
Whether propagation finds no contradiction.
Arguments
networkis at/0.
Returns
truewhenpropagate/1succeeds,falsewhen it reports an inconsistency. Atrueresult is not proof of satisfiability — see the module docs.
Examples
iex> alias Tempo.Interval.RelationNetwork, as: Net
iex> Net.new([:a, :b])
...> |> Net.constrain(:a, [:precedes], :b)
...> |> Net.consistent?()
true
iex> alias Tempo.Interval.RelationNetwork, as: Net
iex> Net.new([:a, :b])
...> |> Net.constrain(:a, [:precedes], :b)
...> |> Net.constrain(:a, [:preceded_by], :b)
...> |> Net.consistent?()
false
@spec constrain(t(), label(), relations(), label()) :: t() | {:error, {:unknown_label, label()} | {:invalid_relation, term()}}
Narrow what is possible from a to b by relations.
Constraints accumulate: asserting twice narrows twice, and asserting
something already known changes nothing. The inverse is recorded on the
reverse pair automatically, so constrain(net, a, [:precedes], b) also
establishes that b is preceded by a.
This records the assertion only. Call propagate/1 to draw out what it
implies for other pairs.
Arguments
networkis at/0.aandbare labels in the network.relationsis the set of relations to narrow by.
Returns
The updated
t/0.{:error, {:unknown_label, label}}when a label is not in the network.{:error, {:invalid_relation, term}}whenrelationsholds something that is not one of the thirteen.
Examples
iex> alias Tempo.Interval.RelationNetwork, as: Net
iex> net =
...> Net.new([:a, :b])
...> |> Net.constrain(:a, [:precedes, :meets], :b)
iex> Net.between(net, :b, :a)
[:met_by, :preceded_by]Asserting a second constraint narrows rather than replaces:
iex> alias Tempo.Interval.RelationNetwork, as: Net
iex> net =
...> Net.new([:a, :b])
...> |> Net.constrain(:a, [:precedes, :meets], :b)
...> |> Net.constrain(:a, [:meets, :overlaps], :b)
iex> Net.between(net, :a, :b)
[:meets]
A network over labels, with nothing asserted between any pair.
Every pair starts as the full set of thirteen relations — the state of knowing nothing — and every period equals itself.
Arguments
labelsis a list of period identifiers. Any term may be a label.
Returns
- A
t/0.
Examples
iex> net = Tempo.Interval.RelationNetwork.new([:a, :b])
iex> Tempo.Interval.RelationNetwork.between(net, :a, :b)
...> |> length()
13
iex> net = Tempo.Interval.RelationNetwork.new([:a, :b])
iex> Tempo.Interval.RelationNetwork.between(net, :a, :a)
[:equals]
Propagate every constraint to a fixpoint.
Allen's §3 path consistency: for every triple, the relations possible
from i to k are narrowed by what a step through j allows —
R(i,k) ← R(i,k) ∩ (R(i,j) ∘ R(j,k)) — repeated until nothing changes.
Read the module docs before trusting the result: pruning is sound, but a clean pass is not proof of consistency.
Arguments
networkis at/0.
Returns
{:ok, network}with every set narrowed as far as path consistency reaches. No contradiction was found.{:error, {:inconsistent, {a, b}}}when some pair narrowed to the empty set. That is definitive: the constraints cannot all hold.
Examples
iex> alias Tempo.Interval.RelationNetwork, as: Net
iex> net =
...> Net.new([:a, :b, :c])
...> |> Net.constrain(:a, [:precedes], :b)
...> |> Net.constrain(:b, [:precedes], :c)
iex> {:ok, net} = Net.propagate(net)
iex> Net.between(net, :a, :c)
[:precedes]A cycle that cannot be satisfied is reported:
iex> alias Tempo.Interval.RelationNetwork, as: Net
iex> net =
...> Net.new([:a, :b, :c])
...> |> Net.constrain(:a, [:precedes], :b)
...> |> Net.constrain(:b, [:precedes], :c)
...> |> Net.constrain(:c, [:precedes], :a)
iex> {:error, {:inconsistent, _pair}} = Net.propagate(net)