ADR-0016: One reading of an aggregation's loops, shared by emitter, views and grounder¶
- Status: accepted
- Date: 2026-10-04
Context¶
A canonical Term with operator="sum" stores two things about its binder:
operator_over, the families it ranges over, and per slot of the referenced
template a Binding whose expr names the dummy feeding that slot. The
binder's own dummy names are not stored. Every consumer therefore has to pair
the summed families with the dummies the bindings use, and three consumers did
that pairing three different ways:
| consumer | reading | defect |
|---|---|---|
solve/grounder.py |
loop over the dummies the bindings use that the row leaves free; operator_over never read |
\sum_{i \in I, r \in R} dur_r grounded to \sum_r dur_r, \sum_{i \in I} 5 to 5 (issue #60) |
views/ground.py |
loop over operator_over, scope keyed by family, bindings resolved by dummy |
no summed term ever produced an edge; bindings collected into a dict keyed by family, so y_{i,j} over I x I reached y[j,j] |
codec/latex.py emitter |
each family takes the first binding of that family | \sum_{t \in T} x_{t-1} printed as the unparseable \sum_{t-1 \in T}; \sum_{j \in I} y_{i,j} under \forall i printed as \sum_{i \in I} y_{i,j}; a summed constant printed without its binder |
None of these was caught: the codec's fixed-point tests ran over models whose
sums use every summed family with a fresh dummy, the ground-view tests counted
nodes and never edges, and no solved model had a summed family its referent
does not use. The defects surfaced while canonicalising the Gurobi
railway-dispatching MILP for the Paper-1 anatomy figure, whose objective
subtracts \sum_{i \in I, r \in R} dur_r.
The paper states that the ground view "is used for rendering at small sizes and, exported to PyG/DGL, as input to downstream graph learning", and that the grounder makes a mined model executable; both claims rest on these readings.
Decision¶
- One module,
lp2graph.core.scope, owns the reading. A family ofoperator_overtakes the first free dummy (one the row's quantifiers do not bind) whose slot family it is; a family no dummy matches is a loop the referent does not vary with, which repeats the summand once per element (\sum_{i \in I} c = |I| c).aggregation_loopsis the evaluation reading (grounder, ground view);binder_loopsis the emitter's, which reproduces the binder as written. - Free dummies no summed family takes are aggregated, as the grounder
always did: that is how
absterms (which carry nooperator_over) and models written beforeoperator_overwas populated are read (|t_i|under a freeiis\sum_i |t_i|). The ground view now readsabsthe same way. - A binder whose family disagrees with the referent's declared shape is
refused for evaluation (
AggregationError, surfaced by the grounder asUnsupportedModel): in\sum_{k \in K} x_kwithxdeclared overI, which elements ofIthe elements ofKdenote is not in the model. The grounder used to loopkoverI, silently. The emitter still prints the binder as written, so the text round-trips. - Bindings resolve by slot position in the ground view, as in the
grounder. A recurring summand (a counting loop, a cyclic wrap onto the same
instance) is one edge carrying
multiplicity. - The emitter names a counting loop with a fresh dummy (the family's
lower-case name, suffixed
puntil it is unused in the row), so it never re-binds a quantifier's dummy.
Issue #61 is decided alongside because it is the same kind of omission (a
model fact the graph did not carry): a quantifier's where-predicate is a
uses_parameter edge of role where in the schema and hybrid views.
Consequences¶
Measured over the 10 catalog fixtures and the 25 canonical models of the lab corpus (18 promotions, 7 repository conversions) before and after:
- Solved values change for every model with a summed family its referent does not use, and for summed constants: they are now the values the text states. In the lab corpus that is one model (the railway-dispatching anatomy example); its argmin is unchanged because the affected term is constant.
- Five lab models sum over a subset family (
\sum_{r \in \mathcal{R_k}} x_rwithxdeclared overR). The emitter used to print them with a dummy the summand does not use (\sum_{r_k \in \mathcal{R_k}} x_r, the model round trip was intact, the text was not); they now print as written. The grounder now refuses them (decision 3) where it used to sum over all ofR. Grounding them needs the subset as data (a membership parameter and awhere-predicate), which is a modelling step, not a reading. - The 10 catalog fixtures emit byte-identical LaTeX.
- Ground views gain the edges of every aggregated term. Any ground graph exported before this change was missing them.
- Schema graphs gain one edge per
where-predicate, so isomorphism and WL results change for models that select rows by attribute (one in the lab corpus). Every other schema graph is byte-identical. - Hybrid graphs change for the five models with a same-family pair variable
(
I.1/I.2offset keys); every other hybrid graph is byte-identical. - Tests:
tests/test_aggregation_scope.py(each defect, confirmed to fail on the previous code).