Reported expressions#
A named expression is one of two things, and the file never says which — the objective and the constraints do:
dimensions:
generator: { dtype: str }
snapshot: { dtype: int }
parameters:
marginal_cost: { dims: [generator] }
variables:
p: { foreach: [snapshot, generator] }
expressions:
system_cost: sum(sum(p * marginal_cost, over=generator), over=snapshot)
delivered: sum(sum(p, over=generator), over=snapshot)
lcoe: system_cost / delivered
objective: { sense: minimize, expression: system_cost }
system_cost is in the math: the objective inlines it, so its body stands
inside the program a solver sees, held to the
degree-2 ceiling
where it is read. delivered and lcoe are reported: nothing in the
objective or a constraint names them, so no solver sees them — each is a
quantity read back after a solve. lcoe could be nothing else, since a
variable divisor is a shape the math refuses; delivered could have stood in
the math and simply does not. Nothing about the YAML marks either. Which is
which is decided at load, no data, by expanding the objective and every
constraint and noting each entry they inline.
What lifts and why#
An entry the math reads is substituted before anything consumes the model, so it stays inside the ceiling the math holds to. A reported entry is read by nothing in the model: it is arithmetic over numbers a solve has already produced, and the restrictions the math carries because a sink must build it all lift for a body nothing ingests:
- No degree cap.
system_cost / deliveredabove divides one variable quantity by another.p * p * pis sayable. A quotient, a cube, a ratio of two sums — each is a number once the solve is done. - The divisor may carry variables, and so may an exponent.
/and**drop the variable-free operand they require in the math, because there is no degree left for a variable operand to change. Where such a divisor solves to zero the quotient is absent there, the null a masked row leaves (absence). - A divisor, a base or an exponent may be a sum. The math refuses
x / (a + b)and(1 + rate) ** periodeven with no variable in sight, because a quotient compiles to one reciprocal factor and neither operator distributes over+. A reported body compiles to nothing, so the precompute an entry in the math needs —(1 + rate) ** periodbound as a parameter — is no longer needed. - A factor may be a sum of terms with no ceiling on the other. The one-sum-factor rule is about how many rows a product builds; a reported body builds none.
Without this lift, LCOE — cost over delivered energy — is unsayable, because its divisor is a variable.
Comparisons stay out, exactly as for an entry in the math: an
expressions: body is arithmetic, and >= belongs to a constraint.
Which is which#
An entry is in the math when the objective or a constraint inlines it.
Expansion substitutes every reference before anything reads an expression, so
the question is answered on the expanded tree, and an entry reached through
another entry or through a macro call counts the same as one named in place. A
bound and a where name no entry; a piecewise: link may, and reaches the
math through the constraints its expansion emits. A cased entry is in the math
on the same terms as a plain one.
Reported is about what the math reads, not about the body's shape or when its
value exists. delivered above is affine and reported, because nothing reads
it. A quantity such as (1 + rate) ** period, with no variable in it, needs no
solve at all, and is reported all the same. The one cost of deciding by use
rather than by shape is a typo: an entry meant for a constraint and never
named there loads as a reported quantity instead of failing.
A consumer reads the answer off the program —
Program.named_expressions[name].in_math — and
typeset prints every entry that is not under its own name in
a "Reported quantities" section, its body expanded to the leaves — lcoe
prints as the quotient of the two sums it names, since an entry the math reads
has no symbol of its own. The two cannot disagree: one function decides both.
The math reads at its own ceiling#
An entry's declaration is not degree-checked at all — there is nothing to
check it against until something reads it. Degree is a rule about the
position doing the reading, so it fires, unconditionally, on the expanded tree
of every constraint, the objective and each piecewise link — the same
rules and the same messages that would refuse a variable divisor or a degree-3
product written out by hand. A constraint that references lcoe inlines its
body and hits the divisor rule at that position:
Constraint 'cap': the divisor contains variables, which is not affine. Divide
by a parameter, or precompute the reciprocal as one.
The message names the constraint and the operation the inlined body performs,
not the entry lcoe the author wrote — expansion has already substituted it
away by the time the ceiling is checked. Move the quantity a constraint needs
into an entry whose shape the math can read; a reported one is for reading back
after the solve, never for feeding one.
Reading a constraint's dual#
dual(c) is the one builtin only a reported entry may call: it reads the
row dual of constraint c — the shadow price a solve puts on it — over
c's own foreach frame. c names a constraint, and only a constraint: it
resolves against constraints alone, never the
flat namespace, so a variable or parameter sharing the name is not what dual
reads.
A dual exists only after a solve, so it may stand only in an entry the math never reads. Written anywhere the solver ingests — a constraint, the objective, a piecewise link, or an entry one of those inlines — it is a load error naming the rewrite:
Constraint 'd': a dual exists only after a solve; the math cannot read one —
keep the entry that carries it out of constraints, the objective, bounds and where.
The check runs on the expanded tree, so a macro or an inlined named
expression cannot smuggle a dual into the math.
Where a constraint's where: deletes a row, that row has no dual, so dual(c)
is absent there too — the null reading a lookup gets
(absence).
A solve does not always return one. A model with integer or binary
variables, a quadratic constraint, or a set reformulated into binaries may come
back with no dual for a row that carries one in a pure linear model — and
solvers legitimately differ on which. The language refuses none of these at
load: capability is not the ceiling
(ceiling), where a set
reformulated into binaries "returns no duals where the native form does". So
dual(c) where a solve reports none is a documented absence a consumer
names, the same null — not a value the language promises is there.
The sign is fixed by the constraint as written and the declared sense.
dual(c) is the rate the optimal objective improves as c is relaxed in the
direction its sense points, under the model's own minimize or maximize.
The orientation the file wrote — which side is lhs, which is rhs, which way
the sense faces — is kept verbatim, so the sign is a function of two facts the
file states. A solver that normalises signs its own way is reconciling its
representation, not the language's; two consumers reading the same model still
agree on the sign.
What a consumer does with it#
A reported entry is observable, like one in the math: after a solve, a
consumer reads its value back over its own dims, which fall out of its body
exactly as any entry's do — no foreach, no where. The difference is what
the math reads: an entry in the math is a form a sink ingests, inlined
wherever the objective or a constraint names it; a reported entry is read by
nothing in the model. Where a masked row leaves a solved quantity absent, the
reported value is absent there too — the null reading a lookup gets
(absence).
Nothing in this repository evaluates a reported body; computing the number is a consumer's business (what counts as language). The language's job is to say, once and unambiguously, what the number is, and which entries the math reads.