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 constraint or the objective may use a named expression, and a solve may report its value:
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
It is a bare string, or a mapping with a description: and a dims:. The
dims: are the frame, the dimensions the quantity is read over. Left out,
the body decides the frame. Declared, the body may carry no dimension the
frame does not name, which the loader checks, and it may carry fewer: the
quantity is then constant along the rest, and a constraint over the whole frame
reads it at every coordinate.
expressions:
cap:
dims: [snapshot, generator]
expression: p_max
description: the nominal capacity, the same in every snapshot
An entry written empty: true over a dims: is an empty sum: a
quantity this file declares and other files add terms to, through
merge. It has no
expression:, cases: or otherwise:, and dims: is required. empty
defaults to false, and a spec writes it only where it is true. An entry
with a dims: and no body, and no empty: true, is refused. Alone, the file
reads it as a column over the frame, the way it reads a
given expression, and its definition
prints as injection = ⋯: the name is declared here, and the body is what
the other files add. A term a file adds
is the other half.
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:, or
is an empty sum, empty: true.
| Key | |
|---|---|
dims |
required with cases:. 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.
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. -
A
when:may not compare expressions, such asc > 2 * k, even in a block with one case. Precompute the test as a boolean parameter. -
Each
when:and each value sits inside the frame. A narrower case broadcasts as a parameter with fewer dimensions does.
A claimed coordinate can still have no value: the otherwise: above has none
at the first snapshot, where a case claims every unit. 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. delivered and lcoe are reported.
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.
No degree limit applies to a reported entry: it may divide by a variable, raise one to a power, and multiply two sums. A comparison stays out. A constraint that names such an entry is refused 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.
dual(c) is the rate at which the optimal objective rises as the right side
of c rises. Read lhs <= rhs as lhs <= rhs + d: the dual is the rate in d
at d = 0. The rule is the same for <=, >= and ==, and under minimize
and maximize, so an equality has a dual with a sign too. Which side a term is
written on decides the sign: p <= cap and -p >= -cap state one row, and their
duals are opposite.
Under minimize, a binding row |
Its dual |
|---|---|
p <= cap |
at most 0 |
p >= load |
at least 0 |
sum(p, over=g) == load |
the price of one more unit of load |
A row that c's where: deletes has no dual.
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 the names the spec declares. 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.
- A formal may stand in a list, as in
sum(x, over=[d, snapshot]). There the call binds it to a name, or to a list of names that is spliced in.
A composition of the built-in operators belongs here. What the language will not express is in the limits.