Tempo.Network.Solver (Tempo v0.20.0)

Copy Markdown View Source

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, with relation_certainty/4 for 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

certainly_contemporary?(network, p1, p2)

@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.

consistent?(network)

@spec consistent?(Tempo.Network.t()) :: boolean()

Is the network consistent — does it admit at least one valid assignment of dates?

Arguments

Returns

  • true when consistent, false when 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

contemporaneity(network, p1, p2)

@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

Returns

  • :certain when every valid chronology has the periods overlapping;

  • :possible when some but not all valid chronologies do;

  • :impossible when 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

possibly_contemporary?(network, p1, p2)

@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.

relation(network, p1, p2)

@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

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

relation_certainty(network, p1, p2, relation)

@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 for relation/3.

  • relation is an Allen relation atom (e.g. :during, :precedes).

Returns

  • :certain | :possible | :impossible, or {:error, reason} as relation/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

tighten(network)

@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

Returns

  • {:ok, network} with each period's bounds replaced by the computed tightest bounds (a bound that the constraints leave unbounded becomes nil); 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"

trace(network, boundary, options \\ [])

@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

Options

  • :bound is :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"