Skip to content

Absence and where#

A where: does not zero a variable out. It leaves the variable unbuilt at the masked coordinates — no column, no value — and every rule on this page follows from that one fact.

dimensions:
  g: { dtype: str }
parameters:
  p_max: { dims: [g] }
variables:
  p:
    foreach: [g]
    where: "p_max > 0"

With p_max = {wind: 10, gas: 5, old: 0} the model has p[wind] and p[gas]. There is no p[old]. What a where: may say is the grammar; this page is what it means.

What creates absence#

Construct What is absent
where: on a variable the variable, at the masked coordinates
where: on a constraint the row
shift(x, over=d, offset=n) without edge= the vacated edge coordinate (shift)
a label a lookup does not map that label's group membership (lookups)

Nothing else does. In particular a missing parameter row is not absence: a sparse table is a compressed dense one, and the missing row reads as the value that contributes nothing — 0 as a coefficient, false in a where. Where no such value exists the load is refused rather than guessed: a divisor, a bounds: entry, the whole constant side of a comparison, a piecewise: breakpoint.

How absence travels#

Through arithmetic it spreads and takes the row with it. Out of a summing operator it does not.

variables:
  x: { foreach: [g] }
  y: { foreach: [g], where: "p_max > 0" } # no y[old]
constraints:
  each:
    foreach: [g]
    expression: x + y >= 1 # rows at wind and gas; no row at old
  total:
    foreach: []
    expression: sum(x + y, over=g) >= 1 # x[wind] + y[wind] + x[gas] + y[gas] >= 1
  split:
    foreach: []
    expression: sum(x, over=g) + sum(y, over=g) >= 1 # x[old] is back in

each has no row at old — not x[old] >= 1. total sums the summand where the summand exists, so x[old] goes with y[old]. split sums each operand over its own domain, so x[old] counts. Different questions; rewriting one into the other reads the absent y[old] as a zero.

The same rule next to a parameter is the asymmetry that bites:

constraints:
  cap:
    foreach: [g]
    expression: x - rel_max * y <= 0

Where the variable y is masked the row is gone. Where the parameter rel_max has no row it is 0, and the row stands as x <= 0. To drop the row there instead, say so: where: rel_max on the constraint.

Every operator falls on one side of that line, and one question puts it there: does an output slot stand for several input slots, or for one?

Operator An output slot reads An absent input
sum(x, over=d) every position along d is one summand fewer; the row stands
sum(x, by=lookup) every member of the group is one summand fewer; the row stands
sum_back(x, over=d, within=w) the positions the window covers is one summand fewer; the row stands
shift(x, over=d, offset=n) one position, n back is the output, so it spreads
at(x, by=lookup) one position, through the map is the output, so it spreads

The three summing operators put several slots into one, so a missing slot is a shorter sum and the row survives — a window that reaches past the start of its axis is short for the same reason, not absent. The other two are one slot for one, so there is nothing to sum over and absence rides straight through, which is why a bare shift's vacated edge takes its row with it.

Reading a summing operator as though it spread absence is the same error as rewriting total into split above, one operator down.

What a missing coordinate means#

By default the masked coordinate has no value: a store that is not there has no state of charge, so a row needing it is not asserted. Some quantities are zero outside their mask — a reservoir with no inflow spills nothing — and that model wants its row. The variable says which:

variables:
  spill:
    foreach: [storage]
    where: has_inflow
    absence: zero # outside the mask spill is 0 and the row stands
  soc:
    foreach: [storage]
    where: has_store # the default, absence: undefined — no row
constraints:
  balance:
    foreach: [storage]
    expression: inflow - spill - soc == 0

At a storage with a store and no inflow, balance reads inflow - soc == 0. At one with inflow and no store, there is no row.

absence: zero needs a where:, is the only fill a variable takes, and changes nothing inside a summing operator, which never propagated absence in the first place.

A row with no variable terms is not built#

A missing parameter row can leave a row with nothing to decide — 0 == load at a bus no generator sits on. Such a row is not built, whatever left it that shape. An expression that names no variable in the file is different, and is refused at load where the message can quote the line.

Every row not built — by a mask, by a spread absence, by this rule — is reported by diagnostics().omissions as (constraint, rows_not_built). A recurrence's first row is in there and is the boundary, not a bug.

Reported values follow the rows that were built#

A reported expression is arithmetic over solved numbers, so it inherits their absence — by the same fork as above. Through pointwise arithmetic a null spreads and takes the coordinate with it: cost / delivered has no value wherever either operand is masked, the null reading a lookup gets rather than a zero. Out of a summing operator it does not: sum(p, over=g) is one summand shorter where a p[g] is masked, and stands so long as one slot does. A statistic is defined exactly where the rows it reduces over were built, and absent everywhere they were not.

A quotient whose divisor solved to zero is absent the same way. The row was built and the numbers are in hand, but the arithmetic has no value there, so the reported quantity reads that same null — the language has one "no value", and an undefined quotient joins it rather than raising a separate not-a-number.

A dual(c) follows the same rule from the constraint side. A constraint's where: leaves its row unbuilt at the masked coordinates, and an unbuilt row has no shadow price, so dual(c) has no value there — the same null, not a zero. The dual is defined at exactly the coordinates of c's frame where the row was built.

Asking for the other reading#

You want You write
the row kept, the masked variable read as zero absence: zero on the variable
the row dropped where a parameter has no data where: p on the constraint
a vacated shift position to contribute shift(x, over=d, offset=n, edge=0)
to test whether a variable exists here its bare name in a where
a bound only where the data has one supply it (inf is a value), or mask the variable — different models, so neither is inferred