Skip to content

See what a curve or a set expands to#

A piecewise: block and a sos: block each stand for plain variables and constraints. Read those rows to review a formulation, to teach one, or to hand the model to an engine that has no concept of a set.

The model below states one of each. A piecewise: block ties two variables to a curve through the breakpoints. A sos: block says that at most one member of a family is nonzero.

description: one plant whose fuel use follows a curve, and whose output picks one mode

dimensions:
  snapshot: { dtype: int }
  bp: { dtype: int }
  mode: { dtype: int }

parameters:
  demand: { dims: [snapshot] }
  bp_out: { dims: [bp] }
  bp_fuel: { dims: [bp] }

variables:
  output:
    dims: [snapshot]
    bounds: { lower: 0, upper: 100 }
  fuel:
    dims: [snapshot]
    bounds: { lower: 0 }
  level:
    dims: [snapshot, mode]
    bounds: { lower: 0, upper: 1 }

piecewise:
  fuel_curve:
    over: bp
    method: sos2
    links:
      - [output, bp_out]
      - [fuel, bp_fuel]

sos:
  mode_pick:
    variable: level
    over: mode
    type: 1

constraints:
  meet:
    dims: [snapshot]
    expression: output >= demand

objective:
  sense: minimize
  expression: sum(fuel, over=snapshot)

1. Write the formulation out#

expand() returns the same math with its formulations stated as rows. The command line prints the result instead of returning it.

from math_spec import to_spec

spec = to_spec('plant.yaml')
written_out = spec.expand()
python -m math_spec markdown plant.yaml --expand

2. Read the names it added#

The expansion declares what the two blocks stood for, and the blocks themselves are gone:

sorted(spec.variables)  # ['fuel', 'level', 'output']
sorted(written_out.variables)
# ['fuel', 'fuel_curve_lam', 'fuel_curve_seg', 'level', 'mode_pick_seg', 'output']

sorted(written_out.constraints)
# ['fuel_curve_adjacency', 'fuel_curve_convexity', 'fuel_curve_link0',
#  'fuel_curve_link1', 'fuel_curve_pick', 'meet', 'mode_pick_nonzero',
#  'mode_pick_pick']

written_out.piecewise  # {}
written_out.sos  # {}

Every emitted name starts with the block that emitted it, so fuel_curve_lam is the curve's weights and mode_pick_seg is the set's binaries. A file that already declares one of these names is refused, which keeps the two apart.

3. Read the rows as math#

Both readings print from the same file. The first states the construct, the second states the rows it stands for.

The curve is one line, and the set is a membership beside the variable it runs along:

\[ \left( \mathit{output}_{t},\ \mathit{fuel}_{t} \right) \in \mathrm{pwl}_{b \in \mathcal{B}}(\mathrm{bp\_out}_{b},\ \mathrm{bp\_fuel}_{b}) \qquad \forall\, t \in \mathcal{T} \]
\[ \left( \mathit{level}_{t,m} \right)_{m \in \mathcal{M}} \in \mathrm{SOS}1 \qquad \forall\, t \in \mathcal{T} \]

The curve becomes weights on the breakpoints, one link row per tied variable, and the binaries that keep the weights adjacent:

\[ \mathit{output}_{t} = \sum_{b \in \mathcal{B}} \mathit{fuel}^{\mathrm{curve,lam}}_{t,b} \cdot \mathrm{bp\_out}_{b} \qquad \forall\, t \in \mathcal{T} \]
\[ \mathit{fuel}^{\mathrm{curve,lam}}_{t,b} \le \mathit{fuel}^{\mathrm{curve,seg}}_{t,b} + \mathit{fuel}^{\mathrm{curve,seg}}_{t,b \boxminus_{0} 1} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \]

The set becomes one binary per member, a row that picks at most one, and a row that holds an unpicked member at zero:

\[ \sum_{m \in \mathcal{M}} \mathit{mode\_pick\_seg}_{t,m} \le 1 \qquad \forall\, t \in \mathcal{T} \]
\[ \mathit{level}_{t,m} \le \mathit{mode\_pick\_seg}_{t,m} \qquad \forall\, t \in \mathcal{T},\ m \in \mathcal{M} \]

Name the symbols as a paper would with a symbol table, which spells an emitted name as readily as a declared one. Print a model as math has the commands for a document that compiles.

4. Write out one kind at a time#

Pass a kind to keep the other construct. This is how to read a curve without the binaries underneath it:

spec.expand('piecewise')  # curves become weights; the sets stay
spec.expand('sos')  # sets become binaries; the curves stay

A method: sos2 curve states a set, so writing the curves out adds one:

sorted(spec.sos)  # ['mode_pick']
sorted(spec.expand('piecewise').sos)  # ['fuel_curve', 'mode_pick']

expand() with no argument writes the curves out first for that reason, and a set never states a curve.

Two things to know#

An expansion is a file like any other. A curve emits variables, constraints and assumptions over the parameters the file declared, and no parameter of its own, so to_yaml() writes every expansion and the same data binds it.

A method states what it assumes of the data. Every curve states that its breakpoints are there, because a missing parameter row reads as a zero rather than as a shorter curve. A method: lp or method: convex curve states more: it is exact only for breakpoints of the right shape. The expansion writes those conditions into assumptions: beside the rows. The method: sos2 curve above states nothing about the shape, because it takes a curve of any shape.

What expand() accepts, and what each method: emits, is under piecewise curves and SOS.