ADR-0011: The canonical Formulation is the interchange pivot¶
- Status: accepted
- Date: 2026-08-26
Context¶
lp2graph.interop (≈2650 LOC, the largest subpackage after mining) reads
and writes seven textual modelling formats — canonical JSON, canonical LaTeX,
CPLEX/Gurobi LP, MPS, GAMS, AMPL, JuMP — plus three live solver object APIs
(gurobipy, PuLP, Pyomo). The lp2graph convert subcommand exposes this as
"code ⇄ graph ⇄ code between modeling languages".
Ten formats that could each convert to the others directly would need up to
90 pairwise converters, every one of them a place for coefficient drift to
creep in. The package instead routes everything through the canonical
pydantic Formulation: each format has exactly one reader and one writer,
and conversion is always read → Formulation → write.
This is the decision the whole interop surface rests on, and it was
recorded only implicitly — in the shape of the code and a line of CLI help
text. It is also what makes ADR-0006 (determinism) hold across formats: the
pivot is frozen, schema-validated and insertion-order deterministic, so a
round trip cannot smuggle in ordering or precision differences.
Decision¶
No format converts directly to another. Every importer produces a validated
Formulation; every exporter consumes one.
- An importer is
text -> Formulationand raisesInteropErroron anything it cannot represent faithfully — it never returns a partial model. - An exporter is
(Formulation, Instance | None) -> text. - Formats that are scalar/flat (LP, MPS, and the live solver APIs) go through
the shared
GroundedModelintermediate ininterop/_grounded.py, which is still derived from the pivot rather than from another format. - Adding format F costs one reader and one writer (2 units of work), not one converter per existing format.
- The pivot is the schema-validated
Formulation— not a looser dict — so every conversion is validated at the waist.
Rationale¶
- Replaceability. A user can adopt lp2graph as a drop-in for a
format-specific tool and keep their existing pipeline: the round trip
through the pivot is lossless for anything the schema can express, and
loudly lossy (an
InteropError) for anything it cannot. - Linear extension cost. N+M adapters instead of N×M converters.
- One place to enforce determinism and validation (ADR-0006), rather than once per format pair.
- Fidelity is testable. Because every path crosses the pivot, round-trip equivalence can be asserted as canonical-normal-form equality (ADR-0010) rather than per-pair byte comparison.
Consequences¶
- A format's expressiveness is capped by the canonical schema. Features no format-pair pivot can carry (e.g. solver-specific hints) are dropped, and must surface as a reported failure rather than a silent omission.
- Two "flat" formats (LP → MPS) pay a template-level detour they do not strictly need. Accepted: the uniformity is worth more than the shortcut.
- Exporting a template-level formulation needs an
Instanceto ground it; the CLI therefore requires--instancefor those targets. - Each new format must be registered in
lp2graph.formats(ADR-0012), not in a private table belonging to the new reader.
Alternatives considered¶
- Direct pairwise converters: rejected — quadratic growth, and each pair becomes an independent opportunity for coefficient drift.
- A looser dict/AST as the pivot: rejected — it would move validation out of the waist and weaken the determinism guarantee that ADR-0006 makes global.