You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The typed expression AST (expression_parser + where_parser dataclass nodes,
together with the schema) is not just a parser internal — it is the narrow
waist of the whole system. Today it has one producer (YAML) and two consumers
(the eager builder, the relational lowering/IR). Both backends are already
just consumers of a shared, solver-agnostic algebraic model.
Recognising this reframes the project: it is not "YAML → linopy", it is a
portable model AST with a YAML front-end, N execution backends, and M
renderers / analyzers / transformers. That is a stronger and more durable
architecture, and it changes how we should scope several roadmap items
(LaTeX export, the eager-vs-relational router, piecewise/SOS support).
This issue records the finding and the design rules that follow from it.
Where it came from
Two prior findings led here:
linopy 0.9 has moved past the flat A-matrix. It now ships add_piecewise_formulation, add_sos_constraints, add_indicator_constraints, semi_continuous, and a reformulation layer
(reformulate_sos_constraints / apply_sos_reformulation / dualize).
These are higher-level modeling constructs; the relational backend, which
bypasses linopy.Model and streams (col, row, coeff) to the solver,
inherits none of them, while the eager builder (a pure consumer of linopy)
inherits them for free. The two backends are diverging in capability.
LaTeX export forced the layering question. Symbolic LaTeX (\sum_{g} p_g c_g) must be rendered from the AST, never the IR — because the IR has
already thrown away exactly what LaTeX needs (sum(over=…) → GROUP BY, where → row absence, names → integer labels; this is the readable-vs-
functional round-trip already noted in SPEC Q8). The moment you say "LaTeX
is a consumer of the shared AST", a whole family of consumers falls out.
The consumer taxonomy
Consumers cluster into four kinds, and the layer rule is uniform:
Consumer
Kind
Layer
Needs data?
Symbolic LaTeX / MathML / Markdown-math
renderer
AST
no
Natural-language "explain"
renderer
AST
no
Graphviz (var↔constraint / param→var DAG)
renderer
AST
no
AMPL / GAMS / Pyomo / JuMP emitter
renderer
AST
no
Shape / dimension inference
analyzer
AST
no
Unit / dimensional-consistency checker
analyzer
AST + units meta
no
Size / sparsity estimator (nnz, nrows)
analyzer
AST
no
Model diff (structural, for extend())
analyzer
AST
no
Reformulation (piecewise/SOS → aux vars)
transformer
AST→AST
no
Symbolic presolve / constant-folding
transformer
AST→AST
no
LSP / language server (hover, go-to-def)
tooling
AST
no
Instantiated LaTeX (concrete rows)
sink
IR
yes
LP / MPS
sink
IR
yes
Layer rule: renderers and analyzers branch at the AST and never touch the
IR; transformers run before lowering (AST→AST); only data-bearing,
fully-expanded outputs are IR sinks. Same reason each time — the IR has already
discarded names, quantifiers, and where-structure.
Load-bearing consumers (not just nice-to-have)
Size / sparsity estimator. This is the cost model the automatic
eager-vs-relational router needs. Reads schema dim-sizes + where-strings,
estimates nrows/ncols/nnz without building anything. The router is not
optional glue — it is a consumer the architecture requires, and it lives
purely at the AST.
AST-level reformulation. The clean fix for the piecewise/SOS divergence
in finding (1): if a piecewise:/sos: block reformulates at the AST into
aux-variable declarations + linking constraints, both backends inherit it
automatically — no reimplementing big-M in SQL, no eager/relational
capability gap. Highest-leverage item here.
Shape / dimension inference. Both backends already do broadcast reasoning
implicitly. Factoring it out as one shared analyzer removes duplication and
simultaneously powers the size estimator, the unit checker, the LSP, and
better load-time errors.
The recurring boundary: custom Python helpers
Every consumer hits the same ragged edge. The closed built-in language
(sum, group_sum, roll, arithmetic, comparisons) is universally
consumable — renderable, analyzable, reformulable. A registered custom Python
helper is opaque to every consumer, so each must reject-or-placeholder it.
Design pressure follows: every built-in added to the closed vocabulary lights
up all M consumers for free; every custom-helper escape hatch stays dark. This
is a concrete argument to grow the built-in vocabulary rather than lean on
custom helpers.
Second producer
The waist has a second potential producer, not just consumers. SPEC Q8 /
issue #3 (intercept add_variables/add_constraints to synthesise schema) is
exactly "a Python DSL that feeds the same AST". Under this framing it stops
being a curiosity and becomes "add a second front-end" — symmetric with adding
a renderer.
Proposed actions
Add an "AST as narrow waist" section to SPEC (producer / consumer /
transformer taxonomy; the AST→IR loses-readability boundary; the
closed-language vs custom-helper rule).
Extract shape/dimension inference as a shared AST analyzer (unblocks
several consumers below).
Build the size/sparsity estimator and wire it into automatic
eager-vs-relational backend selection.
Formalise AST-level reformulation as the mechanism for
piecewise/SOS/indicator, so both backends stay in sync (ties to the
linopy-0.9 finding).
Then: symbolic LaTeX and AMPL/GAMS emitters as thin renderers on
the same rails.
Related
SPEC §12 (relational backend), SPEC §10 (piecewise/SOS "out of scope in v1"),
SPEC Q8 (readable-vs-functional round-trip).
Summary
The typed expression AST (
expression_parser+where_parserdataclass nodes,together with the schema) is not just a parser internal — it is the narrow
waist of the whole system. Today it has one producer (YAML) and two consumers
(the eager
builder, the relationallowering/IR). Both backends are alreadyjust consumers of a shared, solver-agnostic algebraic model.
Recognising this reframes the project: it is not "YAML → linopy", it is a
portable model AST with a YAML front-end, N execution backends, and M
renderers / analyzers / transformers. That is a stronger and more durable
architecture, and it changes how we should scope several roadmap items
(LaTeX export, the eager-vs-relational router, piecewise/SOS support).
This issue records the finding and the design rules that follow from it.
Where it came from
Two prior findings led here:
linopy 0.9 has moved past the flat A-matrix. It now ships
add_piecewise_formulation,add_sos_constraints,add_indicator_constraints,semi_continuous, and a reformulation layer(
reformulate_sos_constraints/apply_sos_reformulation/dualize).These are higher-level modeling constructs; the relational backend, which
bypasses
linopy.Modeland streams(col, row, coeff)to the solver,inherits none of them, while the eager builder (a pure consumer of linopy)
inherits them for free. The two backends are diverging in capability.
LaTeX export forced the layering question. Symbolic LaTeX (
\sum_{g} p_g c_g) must be rendered from the AST, never the IR — because the IR hasalready thrown away exactly what LaTeX needs (
sum(over=…)→GROUP BY,where→ row absence, names → integer labels; this is the readable-vs-functional round-trip already noted in SPEC Q8). The moment you say "LaTeX
is a consumer of the shared AST", a whole family of consumers falls out.
The consumer taxonomy
Consumers cluster into four kinds, and the layer rule is uniform:
extend())Layer rule: renderers and analyzers branch at the AST and never touch the
IR; transformers run before lowering (AST→AST); only data-bearing,
fully-expanded outputs are IR sinks. Same reason each time — the IR has already
discarded names, quantifiers, and where-structure.
Load-bearing consumers (not just nice-to-have)
Size / sparsity estimator. This is the cost model the automatic
eager-vs-relational router needs. Reads schema dim-sizes + where-strings,
estimates nrows/ncols/nnz without building anything. The router is not
optional glue — it is a consumer the architecture requires, and it lives
purely at the AST.
AST-level reformulation. The clean fix for the piecewise/SOS divergence
in finding (1): if a
piecewise:/sos:block reformulates at the AST intoaux-variable declarations + linking constraints, both backends inherit it
automatically — no reimplementing big-M in SQL, no eager/relational
capability gap. Highest-leverage item here.
Shape / dimension inference. Both backends already do broadcast reasoning
implicitly. Factoring it out as one shared analyzer removes duplication and
simultaneously powers the size estimator, the unit checker, the LSP, and
better load-time errors.
The recurring boundary: custom Python helpers
Every consumer hits the same ragged edge. The closed built-in language
(
sum,group_sum,roll, arithmetic, comparisons) is universallyconsumable — renderable, analyzable, reformulable. A registered custom Python
helper is opaque to every consumer, so each must reject-or-placeholder it.
Design pressure follows: every built-in added to the closed vocabulary lights
up all M consumers for free; every custom-helper escape hatch stays dark. This
is a concrete argument to grow the built-in vocabulary rather than lean on
custom helpers.
Second producer
The waist has a second potential producer, not just consumers. SPEC Q8 /
issue #3 (intercept
add_variables/add_constraintsto synthesise schema) isexactly "a Python DSL that feeds the same AST". Under this framing it stops
being a curiosity and becomes "add a second front-end" — symmetric with adding
a renderer.
Proposed actions
transformer taxonomy; the AST→IR loses-readability boundary; the
closed-language vs custom-helper rule).
several consumers below).
eager-vs-relational backend selection.
piecewise/SOS/indicator, so both backends stay in sync (ties to the
linopy-0.9 finding).
the same rails.
Related
SPEC Q8 (readable-vs-functional round-trip).
.yamlrepresentation #3 (Roadmap: complete.yamlrepresentation) — the "second producer" angle.