Skip to content

Absence and where#

A where: does not set a variable to zero. It leaves the variable unbuilt at the masked coordinates: no column, and no value. Every rule on this page follows from that.

dimensions:
  g: { dtype: str }
parameters:
  capacity: { dims: [g] }
variables:
  dispatch:
    dims: [g]
    where: "capacity > 0"

With capacity = {wind: 10, gas: 5, old: 0}, the model has dispatch[wind] and dispatch[gas]. There is no dispatch[old].

The grammar says what a where: may contain. This page says what the mask means for the rows that are built.

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, along=d, offset=n) without edge= the vacated edge coordinate (shift)
a label a relation does not map that label's group membership (relations)

Nothing else creates absence. A missing parameter row is not absence, and what it is instead is the parameter's coverage to say. Under total, the default, a coordinate the dims reach with no row is an error when data binds, naming the coordinate: a row lost in preparation, and not a mask. Under masked the sparse table is a compressed dense table, and the missing row reads as the value that contributes nothing: 0 as a coefficient, and false in a where.

Where no such value exists, the bind is refused rather than guessed. There are four such positions: a divisor, a bounds: entry, the whole constant side of a comparison, and a piecewise: breakpoint.

How absence travels#

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

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

each has no row at old, so there is no x[old] >= 1. total sums the summand wherever the summand exists, so x[old] goes away with y[old]. split sums each operand over its own domain, so x[old] counts. Rewriting one into the other reads the absent y[old] as a zero, and they are different questions.

Beside a parameter, the rule reads the other way:

constraints:
  cap:
    dims: [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 reads as 0, and the row stands as x <= 0. To drop the row there instead, write where: rel_max on the constraint.

Every operator falls on one side of the line, and one question decides which: 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=relation) every member of the group is one summand fewer; the row stands
sum_back(x, along=d, window=w) the positions the window covers is one summand fewer; the row stands
shift(x, along=d, offset=n) one position, n back is the output, so it spreads
at(x, by=relation) one position, through the map is the output, so it spreads

The three summing operators put several slots into one, so a missing slot gives a shorter sum and the row survives. A window that reaches past the start of its axis is short for the same reason. The other two map one slot to one slot, so absence passes straight through, and the vacated edge of a bare shift takes its row with it.

What a missing coordinate means#

By default a masked coordinate has no value. A store that is not there has no state of charge, so a row that needs that state is not asserted.

Some quantities are zero outside their mask. A reservoir with no inflow spills nothing, and a model like that wants its row. The variable says which reading applies:

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

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

absence: zero needs a where:. It is the only fill a variable takes, and it changes nothing inside a summing operator, because a summing operator never spread absence.

Rows with no variable terms#

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

The engine that builds the model is the one that knows which rows it did not build, so it is the engine that reports them: rows lost to a mask, to a deleted variable, and to this rule. The first row of a storage balance is always among them, and that is the start of the recurrence rather than a bug.

Reported values#

A reported expression is arithmetic over solved numbers, so it inherits their absence by the same rule as above. Through pointwise arithmetic, a null spreads: cost / delivered has no value wherever either operand is masked. Out of a summing operator, it does not: sum(dispatch, over=g) is one summand shorter where a dispatch[g] is masked, and stands as long as one slot does.

A quotient whose divisor solved to zero is absent in the same way. The language has one "no value", and an undefined quotient joins it rather than raising a separate not-a-number.

dual(c) follows the same rule from the constraint side. A row that c's where: leaves unbuilt has no shadow price, so dual(c) has no value there.

Asking for the opposite 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: capacity on the constraint
a vacated shift position to contribute shift(x, along=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 the bound, because inf is a value, or mask the variable. These are different models, so the language infers neither