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.

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], but no dispatch[old].

The condition grammar says what a where: may hold.

What creates absence#

Construct What is absent
where: on a variable the variable, at the masked coordinates
where: on a parameter the parameter, at the masked coordinates (a parameter's mask)
where: on a constraint the row
shift(x, along=d, offset=n) without edge= the vacated edge coordinate (shift)
a key combination a missing: absent relation has no row for that combination's place in the relation (relations)
a missing row of a missing: absent parameter the parameter, at that coordinate

Nothing else creates absence. Inside its mask, a missing row is the only gap a parameter can have, because a null or NaN value is refused when the data is attached. The parameter's missing: key says what a missing row is:

missing: A missing row
refused, by default is refused when the data is attached
absent is absence, by the rules on this page
a value reads as that value, wherever the model reads the parameter

Under refused, the table has a row at every combination of the labels of its dimensions, so no position ever reads a missing row. A value is the same number at every position, so the file says what a missing row is: missing: 0 for a cost that is zero, missing: 1 for an efficiency that is one, and missing: false for a flag that is off. The table below says what a missing row of an absent parameter, or of a parameter with a value, does at each position:

Position absent a value
a coefficient, or a constant on either side of a comparison removes the row reads the value
a factor of a summand in a sum removes the summand, not the row reads the value
a divisor removes the row reads the value. 0 is refused at load, unless a guard leaves the row out
a bounds: entry leaves that side of the variable open reads the value
the amount of an offset= or a window= makes the operator absent there reads the value
a comparison in a where is false compares the value
a bare bool name in a where is false reads the value
a bare number or label in a where is false is false

A value of 0 as an amount reads shift(x, along=t, offset=0) as x itself, and sums nothing under sum_back(x, along=t, window=0). A curve needs a row at each breakpoint its points: admits, and missing breakpoints says what a missing row of a table that a curve reads means. For a bound only where the data has one, mask the variable, or declare the parameter missing: absent or missing: .inf.

A guarded divisor#

A divisor that a missing row of a missing: 0 parameter can make 0 is refused at load, unless a guard leaves every missing row out where the quotient is read. A guard is a condition that is false at a missing row, such as c > 0, c != 0 or the bare name c. It guards the quotient when it is one of the conditions joined by AND in one of these places:

  • the where: of the constraint,
  • the when: of the case that holds the quotient,
  • the where: of a variable that multiplies the quotient and is missing: absent, because the product is absent where that variable is absent. A variable with a missing: number reads that number outside its mask, so its mask guards nothing.
parameters:
  c: { dims: [g], missing: 0 }
  d: { dims: [g] }
constraints:
  per_unit:
    dims: [g]
    where: "c > 0"
    expression: y <= d / c

A guard holds only at the coordinate where it is read. A sum, a window, a shift or an at reads other coordinates, so a guard outside one of them does not guard a divisor inside it. A named expression is read over its own dimensions, so a guard for its divisor goes in its own cases:.

Zero divisors in the data#

An engine must refuse a divisor that is 0 in a row it builds, or in a summand it adds, when the data is attached. The refusal names the divisor. The check at load reads only the shape of a divisor, so these files load. The engine refuses each one where the data makes its divisor 0:

  • x / (1 - c) with c: { missing: 1 }, at a missing row of c;
  • x / ((c + 1) - 1) with c: { missing: 0 }, at a missing row of c;
  • a case that holds 1 - 1, where its when: holds;
  • x / c, at a present row of c that is 0.

A row or a summand that is absent divides nothing. So the engine does not refuse a divisor where a where:, a case or an absent variable leaves it out. A reported expression is not refused: its quotient is absent where its divisor is 0.

How absence travels#

An absent term in arithmetic removes the whole row. An absent term inside a summing operator is only one summand fewer, so the row stays.

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

total sums the summand wherever the summand exists, but split sums each operand over its own domain, so the two are different constraints.

A parameter with a missing row works the other way:

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

Where the variable y is masked, the row is gone. Where a missing: 0 parameter rel_max has no row, it reads as 0, and the row stands as x <= 0. To drop the row there instead, declare rel_max missing: absent, or write where: rel_max on the constraint.

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, over=d, by=relation[c]) 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[c]) one position, through the map is the output, so it spreads

A table with gaps on purpose#

A table that has rows only where the model needs them is declared missing: absent. An assumption then says where the table must have a row, and the tool that attaches the data checks it:

dimensions:
  g: { dtype: str }
parameters:
  capacity: { dims: [g] }
  p_max: { dims: [g], missing: absent }
variables:
  dispatch:
    dims: [g]
    where: "capacity > 0"
    bounds: { lower: 0, upper: p_max }
assumptions:
  p_max_where_dispatch_exists:
    holds: "p_max"
    where: "capacity > 0"

With capacity = {wind: 10, gas: 5, old: 0}, the model has no dispatch[old], so a p_max table with rows for wind and gas only is accepted. A table with no row for gas fails the assumption, because capacity > 0 holds at gas. Without the assumption, a missing p_max[gas] leaves the upper bound of dispatch[gas] open.

What a missing coordinate means#

By default a masked coordinate has no value, and a row that needs it is not built. Some quantities have a known number outside their mask, such as zero, and the missing: key of the variable names it:

variables:
  spill:
    dims: [storage]
    where: has_inflow
    missing: 0 # outside the mask spill is 0 and the row stands
  soc:
    dims: [storage]
    where: has_store # the default, missing: absent — 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.

A variable takes missing: absent or a finite number, because a variable has no data to refuse. A number needs a where:. It must lie in the domain: of the variable, so a binary variable takes 0 or 1, and to_spec refuses the file otherwise. The bounds: hold only inside the mask, so the number may lie outside them: a unit with lower: 1 where it is built can have missing: 0 where it is not. Inside a summing operator, missing: 0 changes nothing, and another number adds itself once for each coordinate that the mask leaves out. The typeset legend prints the number where the mask leaves it out beside the variable.

Rows with no variable terms#

A missing row that reads as 0 can leave a row with nothing to decide, such as 0 == load at a bus with no generator, and such a row is not built. to_spec refuses an expression that names no variable in the file.

Reported values#

A reported expression inherits the absence of the solved numbers it reads, by the rules above. A quotient is absent where its divisor is absent, and where its divisor is exactly zero, whether a solve or the data gave that zero. A solved value is read as the engine reads it back, so an engine may read a value within its solver's tolerance of zero as zero. A missing row of an absent divisor parameter makes the quotient absent, and to_spec refuses a divisor that a missing: 0 parameter can make zero, unless a guard leaves the missing row out. A deleted row has no dual.