Named expressions and macros#
An expressions: entry names a quantity once, for the math to read or for a
solve to report. A macros: entry is a template with arguments, substituted
before anything reads it.
expressions#
A named expression is a quantity the model names once. A constraint or the objective may use it, and the engine can report its value after a solve:
dimensions:
generator: { dtype: str }
parameters:
rate: { dims: [generator] }
variables:
p: { dims: [generator] }
expressions:
total_generation: sum(p, over=generator)
emissions:
expression: sum(p * rate, over=generator)
description: CO2 released, the quantity a cap would bound
Write it as a bare string, or as a mapping when it carries a description:. Its
dimensions follow from its body, so there is no dims:.
Where the objective or a constraint names it, the body is substituted there, and the degree limit applies where it is read. Where nothing in the math names it, the entry is reported.
cases#
A quantity with several regimes is written as cases, and the rule that reads it
is written once. The commitment state a unit carries into a snapshot is 1
for a unit that is never switched off, an initial condition at the first
snapshot, and the previous snapshot's status everywhere else:
expressions:
previous_status:
description: the commitment state a unit carries into a snapshot
dims: [snapshot, generator]
cases:
always_on:
when: "not committable"
expression: 1
boundary:
when: "committable and position(snapshot) == 0"
expression: status_initial
otherwise: shift(status, along=snapshot, offset=1)
constraints:
ramp_up:
dims: [snapshot, generator]
expression: >-
p - shift(p, along=snapshot, offset=1, edge=0)
<= ramp_limit * previous_status + start_up_limit * (1 - previous_status)
Each case prints as one row of the definition, and otherwise: as the last:
A named expression carries exactly one of expression: and cases:.
| Key | |
|---|---|
dims |
required with cases:, and refused without. The frame: the dimensions every case ranges over |
cases |
a map of named cases, each with a when: mask and an expression: |
otherwise |
required. The value at every coordinate the cases leave |
The rules that keep the cases apart#
- No two cases may claim one coordinate. If two
when:masks can hold at once, the file is refused at load:
Named expression 'previous_status': casesalways_onandboundaryboth claim the value where committable is false, the position of snapshot is 0. A coordinate two cases claim has two values, so it has none — narrow one of the twowhen:strings by the negation of the other, or drop the wider one and letotherwise:carry that region.
That is why boundary above says committable and. The cases carry no order.
-
A
when:must be a question the data answers.True,False, and a mask that folds to one of them, such ascommittable OR True, are refused. -
A pair the check cannot decide is refused.
position(snapshot) == 0againstposition(snapshot) == -1pick the same row on an axis with one member. Count from one end only. -
Each
when:and each value sits inside the frame. A narrower case broadcasts as a parameter with fewer dimensions does.
Claiming a coordinate is not the same as having a value there. The otherwise:
above carries no edge=, so its shift has no value at the first snapshot, and
previous_status is whole there only because a case claims every unit at that
snapshot. To close such a hole, widen a when, give the shift an edge=, or
set absence: zero on the masked variable.
cases: is not accepted inside a macros: template.
Reported expressions#
A named expression is either in the math or reported. The objective and the constraints decide which:
dimensions:
generator: { dtype: str }
snapshot: { dtype: int }
parameters:
marginal_cost: { dims: [generator] }
variables:
p: { dims: [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 uses it, so the solver sees its body.
delivered and lcoe are reported: nothing in the math uses them, so the
engine computes them from the solution after the solve.
An entry is in the math when the objective, a constraint or a piecewise:
link reaches it, directly or through another entry or a macro. A bound and a
where name no entry.
A reported body is built by no solver, so no degree limit applies to it: it may divide by a variable, raise one to a power, and multiply two sums. A comparison stays out. A constraint that later names such an entry reads its body, and is refused there under the constraint's own name.
Reading a constraint's dual#
dual(c) reads the row dual of the constraint c: the shadow price a solve
puts on that row, over c's own dims. Only a reported entry may call it. In a
constraint, the objective, a piecewise link, or an entry one of those inlines,
it is refused:
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.
c resolves against the constraints alone.
dual(c) is the rate at which the optimal objective improves as c is relaxed
in the direction its comparator points, under the model's own minimize or
maximize.
A row that c's where: deletes has no dual. Where the solver returns no dual,
as for a model with integer variables, the engine reports no value.
macros#
A macro is a template that takes arguments and is substituted into an expression before anything reads it:
macros:
weighted_sum:
args: [array, weights] # positional formals, default []
kwargs: [over] # keyword formals, default []
template: sum(array * weights, over=over)
- A template holds arithmetic, and no comparison.
- An argument may itself use macros and named expressions.
- Inside a template, the formal parameters shadow model names. A formal may not collide with a declared dimension.
- The number of arguments is checked at each call site. A cycle is reported with its reference chain.
- Every template is held at load to every rule a call site is, whether or not it is called. A formal is left for the call site to bind.
Anything composed out of the built-in operators belongs here. What the language cannot express is under what the language will not express.