Consistency checking and bound tightening for a chronological network, by solving its Simple Temporal Problem.
The network normalises (Tempo.Network.Normalize) to a directed
weighted graph — one node per boundary plus the origin z₀ — whose
all-pairs shortest paths are the minimal network: the tightest
bound on b₁ − b₂ is the shortest-path weight b₁ → b₂ (Dechter,
Meiri & Pearl 1991; the paper's Floyd 1962). The network is
consistent iff the graph has no negative cycle.
consistent?/1— does any valid assignment of dates exist?tighten/1— the narrowest start, end, and duration each period can take given every constraint together.contemporaneity/3— whether two periods can, must, or cannot overlap.relation/3— the tightest Allen relation(s) still possible between two periods, withrelation_certainty/4for a single named relation.
These all run in O(n³) on the boundary count (Floyd–Warshall), which is interactive for the hundreds of periods these chronologies contain.
Summary
Functions
Whether two periods are certainly contemporary — true only when
every valid chronology has them overlapping. See contemporaneity/3.
Is the network consistent — does it admit at least one valid assignment of dates?
Classify whether two periods overlap in time, given the whole network.
Whether two periods are possibly contemporary — true when at least
one valid chronology has them overlapping. See contemporaneity/3.
The Allen interval relation(s) still possible between two periods, given every constraint in the network.
Whether a specific Allen relation between two periods is :certain,
:possible, or :impossible under the network's constraints.
Tighten every period's start, end, and duration to the narrowest bounds the network implies.
Explain a tightened bound as a trace — the chain of constraints that forces it.
Functions
@spec certainly_contemporary?(Tempo.Network.t(), term(), term()) :: boolean()
Whether two periods are certainly contemporary — true only when
every valid chronology has them overlapping. See contemporaneity/3.
@spec consistent?(Tempo.Network.t()) :: boolean()
Is the network consistent — does it admit at least one valid assignment of dates?
Arguments
networkis aTempo.Network.t/0.
Returns
truewhen consistent,falsewhen the constraints contradict (the graph has a negative cycle).
Examples
iex> Tempo.Network.new()
...> |> Tempo.Network.add_period(:k, start: ~o"1200Y", end: ~o"1180Y")
...> |> Tempo.Network.Solver.consistent?()
false
@spec contemporaneity(Tempo.Network.t(), term(), term()) :: :certain | :possible | :impossible | {:error, :inconsistent}
Classify whether two periods overlap in time, given the whole network.
Returns :certain when the constraints force the periods to be
contemporary, :possible when they merely allow it, and :impossible
when they forbid it. The verdict is read from the minimal (tightened)
network, so it accounts for every constraint — not just the two
periods' own bounds — in the spirit of Geeraerts, Levy & Pluquet,
Models and Algorithms for Chronology (TIME 2017), Props. 7 and 10.
Two periods that merely touch (one ends exactly where the other
begins) count as contemporary, matching add_relation(:contemporary, …) — the endpoints are treated as closed here.
Arguments
networkis aTempo.Network.t/0.p1andp2are ids of periods present in the network.
Returns
:certainwhen every valid chronology has the periods overlapping;:possiblewhen some but not all valid chronologies do;:impossiblewhen none do; or{:error, :inconsistent}when the network has no valid chronology.
Examples
iex> network =
...> Tempo.Network.new()
...> |> Tempo.Network.add_period(:k1, start: {:not_before, 1200}, duration: {:at_most, 15})
...> |> Tempo.Network.add_period(:k2, end: {:not_after, 1300}, duration: {30, 100})
...> |> Tempo.Network.add_period(:s1, duration: {20, 100})
...> |> Tempo.Network.add_period(:s2, duration: {20, 100})
...> |> Tempo.Network.add_sequence([:k1, :k2])
...> |> Tempo.Network.add_sequence([:s1, :s2])
...> |> Tempo.Network.add_relation(:starts_during, :s1, :k1)
...> |> Tempo.Network.add_relation(:ends_during, :s2, :k2)
iex> Tempo.Network.Solver.contemporaneity(network, :k1, :s2)
:impossible
@spec possibly_contemporary?(Tempo.Network.t(), term(), term()) :: boolean()
Whether two periods are possibly contemporary — true when at least
one valid chronology has them overlapping. See contemporaneity/3.
@spec relation(Tempo.Network.t(), term(), term()) :: atom() | [atom()] | {:error, :inconsistent | :unknown_period}
The Allen interval relation(s) still possible between two periods, given every constraint in the network.
Generalises contemporaneity/3 from the single overlap question to the full
relational answer, in the same vocabulary Tempo.relation/2 uses for
grounded values. It reads off the minimal network — the same solved
shortest-path distances contemporaneity/3 and tighten/1 use — so it costs
no extra solve: a relation is possible iff adding its endpoint constraints to
the solved network stays consistent (no negative cycle). No qualitative
disjunction enters, so it remains polynomial.
Periods are treated as proper intervals (start strictly before end), matching
Tempo's no-degenerate-intervals ontology, and endpoints are compared
half-open — so "the end of A coincides with the start of B" is :meets, not
overlap. (That boundary case is still possibly_contemporary?/3, which asks
the looser "could they have coexisted" question.)
Arguments
networkis aTempo.Network.t/0.p1andp2are period identifiers added withTempo.Network.add_period/3.
Returns
A single Allen relation atom (e.g.
:during) when the constraints pin the relation to exactly one — it is entailed.A list of atoms (in Allen's canonical order) when several remain possible — the tightest qualitative statement the constraints support.
{:error, :inconsistent}when the network has no valid assignment, or{:error, :unknown_period}when an id is not in the network.
Examples
iex> Tempo.Network.new()
...> |> Tempo.Network.add_period(:a, start: ~o"1200Y", end: ~o"1250Y")
...> |> Tempo.Network.add_period(:b, start: ~o"1230Y", end: ~o"1280Y")
...> |> Tempo.Network.Solver.relation(:a, :b)
:overlaps
iex> Tempo.Network.new()
...> |> Tempo.Network.add_period(:a, duration: {:at_least, ~o"P1Y"})
...> |> Tempo.Network.add_period(:b, duration: {:at_least, ~o"P1Y"})
...> |> Tempo.Network.add_sequence([:a, :b])
...> |> Tempo.Network.Solver.relation(:a, :b)
:meets
@spec relation_certainty(Tempo.Network.t(), term(), term(), atom()) :: :certain | :possible | :impossible | {:error, :inconsistent | :unknown_period}
Whether a specific Allen relation between two periods is :certain,
:possible, or :impossible under the network's constraints.
The network counterpart of Tempo.relation_certainty/3 on grounded ±-margin
values — the same three-valued vocabulary, read from the solved network via
relation/3. A relation is :certain when it is the only one the
constraints allow, :possible when it is one of several, :impossible when
ruled out.
Arguments
network,p1,p2— as forrelation/3.relationis an Allen relation atom (e.g.:during,:precedes).
Returns
:certain | :possible | :impossible, or{:error, reason}asrelation/3.
Examples
iex> net =
...> Tempo.Network.new()
...> |> Tempo.Network.add_period(:a, start: ~o"1200Y", end: ~o"1250Y")
...> |> Tempo.Network.add_period(:b, start: ~o"1230Y", end: ~o"1280Y")
iex> Tempo.Network.Solver.relation_certainty(net, :a, :b, :overlaps)
:certain
iex> Tempo.Network.Solver.relation_certainty(net, :a, :b, :during)
:impossible
@spec tighten(Tempo.Network.t()) :: {:ok, Tempo.Network.t()} | {:error, :inconsistent}
Tighten every period's start, end, and duration to the narrowest bounds the network implies.
Arguments
networkis aTempo.Network.t/0.
Returns
{:ok, network}with each period's bounds replaced by the computed tightest bounds (a bound that the constraints leave unbounded becomesnil); or{:error, :inconsistent}when no valid assignment exists.
Examples
iex> {:ok, tightened} =
...> Tempo.Network.new()
...> |> Tempo.Network.add_period(:k1, start: ~o"1200Y", duration: {:at_least, ~o"P20Y"})
...> |> Tempo.Network.add_period(:k2, duration: {:at_least, ~o"P35Y"})
...> |> Tempo.Network.add_sequence([:k1, :k2])
...> |> Tempo.Network.Solver.tighten()
iex> tightened.periods[:k2].earliest_end
~o"1255Y"
@spec trace(Tempo.Network.t(), {:start | :end, term()}, keyword()) :: {:ok, map()} | {:error, :unbounded | :inconsistent}
Explain a tightened bound as a trace — the chain of constraints that forces it.
Reconstructs the shortest path in the constraint graph that produces
the :earliest or :latest value of a boundary, mirroring the
paper's Fig. 6c. Each step names the constraint responsible and the
bound derived so far, and :prose renders the whole chain as a
sentence.
Arguments
networkis aTempo.Network.t/0.boundaryis{:start, period_id}or{:end, period_id}.
Options
:boundis:earliest(the default) or:latest.
Returns
{:ok, %{value: t:Tempo.t/0, steps: list, prose: String.t()}};{:error, :unbounded}when the constraints leave the bound open; or{:error, :inconsistent}when the network has no valid assignment.
Examples
iex> {:ok, trace} =
...> Tempo.Network.new()
...> |> Tempo.Network.add_period(:k1, start: {:not_before, ~o"1200Y"}, duration: {:at_least, ~o"P20Y"})
...> |> Tempo.Network.add_period(:k2, duration: {:at_least, ~o"P35Y"})
...> |> Tempo.Network.add_sequence([:k1, :k2])
...> |> Tempo.Network.Solver.trace({:end, :k2})
iex> trace.value
~o"1255Y"