Named expressions, conditions and macros#
An expressions: entry names a quantity once, for the math to read or for a
solve to report. A conditions: entry names a condition once, for every
where:, when: and holds: that reads it. A macros: entry is a template that takes arguments,
which the loader substitutes into an expression 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
An entry is a bare string, or a mapping with a description: and a dims:.
The dims: are the dimensions over which the quantity is indexed. When
dims: is left out, the expression itself decides them. When dims: is
declared, it names the dimensions over which the expression is indexed. The loader refuses an expression that is
indexed over a dimension not given in dims:, and a dims: that gives a
dimension over which the expression is not indexed. An expression that reads
the entry repeats its value along each of its own dimensions that the entry is
not indexed over.
expressions:
headroom:
dims: [generator, snapshot]
expression: p_max - p
description: the capacity a generator leaves unused in a snapshot
adds_to: adds this expression as a term to the sum
it names. The sum is a given expression
of the same file, and merge
adds the entry to it by its own name.
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#
Write a quantity with several regimes as cases:, and write the rule that
reads it 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, sorted by name, and
otherwise: as the last:
A named expression carries exactly one of expression: and cases:.
| Key | |
|---|---|
dims |
required with cases:. The dimensions over which every case is indexed |
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.
to_specrefuses the file when twowhen:masks can be true at once:
Named expression 'previous_status': casesalways_onandboundaryboth claim the value where committable is false, the position of snapshot is 0. Narrow one of the twowhen:strings by the negation of the other, or drop the wider one and letotherwise:cover that region.
The cases carry no order, so to_spec sorts them by name.
-
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. For example,
position(snapshot) == 0andposition(snapshot) == -1pick the same row on a dimension with one member, so count from one end only. -
Where
cases:holds two or more cases, awhen:may not compare expressions, such asc > 2 * k, because the loader cannot prove such a case apart from the others before the data arrives. Precompute the test as a boolean parameter. Acases:with one case may compare expressions, because itsotherwise:claims only what the case leaves. -
Each
when:and each value is indexed only over dimensions given indims:. A value indexed over fewer of them is repeated along the others, the same as a parameter with fewer dimensions. Each dimension indims:is one over which a value or awhen:is indexed.
A claimed coordinate can still have no value: the otherwise: above has none at
the first snapshot, where a case claims every unit. To give such a coordinate a
value, widen a when, give the shift an edge=, or set missing: neutral on
the masked variable.
cases: is not accepted inside a macros: template.
Reported expressions#
A named expression is either in the math or reported, and 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, while 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.
A reported entry has no degree limit, so it may divide by a variable, raise one to a power, and multiply two sums. It may not hold a comparison. 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 the dims of c. Only a reported entry may call it.
to_spec refuses it in a constraint, the objective, a piecewise: link, or an
entry that one of those inlines:
Constraint 'd': a dual exists only after a solve, so it cannot stand here. Keep the entry
that reads 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. The side you write a
term 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.
conditions#
A conditions: entry names a condition once,
and a where:, a when: or a holds: reads it by that name. Under a
where: the condition masks the entry. Under a holds: it is only checked
against the data:
dimensions:
period: { dtype: int, ordered: true }
generator: { dtype: str }
parameters:
build_year: { dims: [generator] }
lifetime: { dims: [generator] }
period_year: { dims: [period] }
p_nom: { dims: [generator] }
conditions:
stands:
condition: build_year <= period_year AND period_year < build_year + lifetime
description: the generator stands in this period
variables:
p:
dims: [period, generator]
where: stands
bounds: { lower: 0, upper: p_nom }
constraints:
ramp_up:
dims: [period, generator]
where: stands AND shift(stands, along=period, offset=1)
expression: p - shift(p, along=period, offset=1) <= 0.5 * p_nom
The typeset document prints the symbol of the condition where a file reads it, upright as data is. The condition prints once, under Conditions, with ⟺ where a named expression prints =:
| Field | ||
|---|---|---|
condition |
required. The condition, in the condition grammar | |
description |
free text | default null |
An entry with no description: may be the bare condition.
- The bare name stands for its condition in a
where:, awhen:and aholds:, underNOT, and insidecount,shiftandat. It selects the same rows as the condition written out. - A named condition declares no
dims:. It is indexed over the dimensions its condition reads, sorted by name, and the symbol prints with those indices. A use over more dimensions repeats the condition along the others, andshiftreads it back only along a dimension over which the condition is indexed. - A named condition may read another named condition, and a named expression that reads only data. The loader refuses a cycle, and the message names its chain:
- A named condition is never a number, so it is refused in arithmetic and in a comparison:
Named expression 'e': 'stands' is a named condition, which is true or false where it is read, and not a number. Write it bare — stands, or NOT stands — rather than comparing it or computing with it.
- A condition that folds to
TrueorFalseis refused, because it names every row or none. - The
where:of a variable may not ask, through a named condition, whether that variable exists. The loader refuses this the same as the bare name of the variable.
Another file reads a named condition under
given: conditions.
macros#
A macro declares its arguments and its template:
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.
- The formal arguments are the names under
argsandkwargs. Inside a template, a formal hides a name the spec declares, but a formal may not collide with a declared dimension. - A call passes
argsby position andkwargsby name. Soto_speckeeps the order ofargsand sortskwargsby name. - The loader checks the number of arguments at each call site. It reports a cycle of macros with the chain of names that forms it.
to_specchecks every template against every rule a call site is held to, whether or not anything calls it. 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.
Write a composition of the built-in operators as a macro. The limits list what the language will not express.