Skip to content

Piecewise curves and SOS#

piecewise: states a curve through breakpoints. sos: states a family of variables of which only one, or only two neighbours, may be non-zero. Both are formulations: each states plain variables and constraints, and spec.expand() writes them out.

piecewise#

A piecewise block ties two or more expressions to one piecewise-linear curve. The curve is given as breakpoints: the corner values each expression takes together.

piecewise:
  chp:
    over: bp # breakpoint dimension
    links:
      - [power, power_bp] # [expression, values-parameter]
      - [fuel, fuel_bp]
      - [heat, heat_bp]
    method: adjacency # how the weights are restricted — below
    activity: null # optional: a binary variable that the weights sum to

  # a two-link block may bound one side instead of pinning it
  fuel_cap:
    over: bp
    links:
      - [power, power_bp]
      - [fuel, fuel_bp, "<="]
Part of a link
expression Any affine expression. The simplest is a bare variable name
values A parameter that carries the over dimension, plus any dimensions the link expressions carry. A dimension the links do not carry is refused
sign <= or >=. At most one per block, and only in a block with exactly two links. It bounds the link instead of pinning it
Key
over required. The breakpoint dimension
links required. Two or more links
method adjacency, sos2, convex or lp: how the weights are restricted (below) default adjacency
activity a binary variable that gates the curve (below) default null
points how far each curve runs, where the curves are not all the same length (below) default null

A block states one weight per breakpoint in [0, 1], a row making the weights sum to 1, and a row per link tying its expression to the weighted breakpoints. The breakpoint order is the declared order of over. What a block assumes of its numbers is on what a curve assumes.

activity#

activity: names a binary variable, and the weights then sum to that variable instead of to 1. So 0 pins the curve off.

The gate is a declaration:

variables:
  running:
    dims: [snapshot, generator]
    domain: binary
    where: committable # only some units have a commitment decision

Where the gate does not exist, the curve is ungated. To have no curve there instead, put absence: zero on the gate.

points#

A values parameter short of a row does not build a shorter curve: the missing row reads as a breakpoint at the origin. A curve with fewer breakpoints than the dimension holds says so with points:. Name one of the block's own values parameters, and the curve is as long as that parameter has rows:

piecewise:
  cost_curve:
    over: bp
    points: bp_x # this curve runs as far as its own breakpoints do
    links:
      - [p, bp_x]
      - [op_cost, bp_y]

A row missing from bp_y is still refused. Where the length is its own data, name a boolean parameter instead. The marked breakpoints are one consecutive run, anywhere on the axis.

method#

method says how the weights are restricted once they exist.

method What it adds
adjacency (default) an sos: block over the weights, written out as binaries the curve, built
sos2 an sos: block over the weights, left as a set the curve, stated for a solver that branches on the set itself
convex nothing the hull, which is a pure linear program
lp no weights at all: one row per segment line, plus two rows holding the domain the curve as its own lines

convex takes exactly two links and no activity:. The shape it needs is an assumption.

lp states the curve as its segment lines. It needs exactly two links, one of them bounded with <= or >=, and no activity::

piecewise:
  cost_curve:
    over: bp
    method: lp
    links:
      - [p, bp_x]
      - [op_cost, bp_y, ">="] # cost bounded below by the curve

Where the number of links is data, write the formulation out (a curve by hand).

sos#

An sos block declares a special-ordered set: one dimension of one variable, and how many members of that family may be non-zero at once.

sos:
  pick_one_size:
    variable: build # the variable the set is over
    along: size # the dimension it runs along — one set per coordinate of the rest
    type: 1 # 1: at most one non-zero; 2: at most two, and consecutive

A set is over one variable, and a variable holds one set. A second block naming the same variable is a load error.

A member the variable's where masks out is not in the set. The order is the declared order of the along dimension.

What a set is written out as#

spec.expand('sos') states the set as binaries: one per member for type: 1, one per segment for type: 2. A member the binaries do not admit is held at zero, from above and from below. For a set s over variable x along d, writing admitted for (s_seg) at type: 1 and (s_seg + shift(s_seg, along=d, offset=1, edge=0)) at type: 2:

Emitted
s_seg a binary over x's own dims, masked as x is
s_pick: sum(s_seg, over=d) <= 1 at most one is picked
s_nonzero (type: 1), s_adjacency (type: 2) x <= upper * admitted
the same name plus _below x >= lower * admitted, where lower is not 0

upper and lower are the member's own bounds:, a number or a parameter; a binary member's are 0 and 1. A model is refused at load where a member has no bounds.lower, or no bounds.upper and no domain: binary. A name the expansion writes that the file already declares is refused at load too.

Writing a formulation out#

Writing a formulation out replaces the block with the variables and constraints it states. Spec.expand() is the call, and see what a curve or a set expands to shows a model before and after.

  • Every name written out starts with the name of the block. The weights of the curve curve are curve_lam.
  • No formulation emits a parameter. The same data binds a model and its expansion.
  • The assumptions a method: implies become assumptions: entries with the same names.