Skip to content

See what a piecewise curve or an SOS expands to#

This guide shows how to write a piecewise curve (a piecewise: entry) or a special ordered set (an SOS) out as plain entries. You can then hand the expanded file to software that builds the model (an engine) but cannot read an sos: entry.

You need mathspec installed and the specification of an optimisation model (a spec) that loads.

Write a piecewise curve out#

Save the spec in the "Before" tab below as before.yaml, and load it. Its curve has method: sos2:

import mathspec as ms

spec = ms.to_spec('before.yaml')

Write out only the piecewise curve with spec.expand('piecewise'), which keeps every sos: entry. Then write out both with spec.expand():

dimensions:
  bp: { dtype: int, ordered: true }

parameters:
  x_bp: { dims: [bp] }
  y_bp: { dims: [bp] }

variables:
  x: { dims: [], bounds: { lower: 0 } }
  y: { dims: [], bounds: { lower: 0 } }

piecewise:
  curve:
    along: bp
    dims: []
    method: sos2
    links:
      x: [x, x_bp]
      y: [y, y_bp]

Variable domains

x

\[ x \ge 0 \]

y

\[ y \ge 0 \]

Subject to

curve

\[ \left( x,\ y \right) \in \mathrm{pwl}_{b \in \mathcal{B}}(\mathrm{x}^{\mathrm{bp}}_{b},\ \mathrm{y}^{\mathrm{bp}}_{b}) \]

Assumptions

curve_complete

\[ \mathrm{x}^{\mathrm{bp}}_{b} \text{ is defined} \wedge \mathrm{y}^{\mathrm{bp}}_{b} \text{ is defined} \qquad \forall\, b \in \mathcal{B} \]
dimensions:
  bp: { dtype: int, ordered: true }

parameters:
  x_bp: { dims: [bp] }
  y_bp: { dims: [bp] }

variables:
  x: { dims: [], bounds: { lower: 0 } }
  y: { dims: [], bounds: { lower: 0 } }
  curve_lam:
    dims: [bp]
    bounds: { lower: 0, upper: 1 }
    description: convex-combination weight on a breakpoint

constraints:
  curve_convexity:
    dims: []
    expression: sum(curve_lam, over=bp) == 1
  curve_x:
    dims: []
    expression: (x) == sum(curve_lam * x_bp, over=bp)
  curve_y:
    dims: []
    expression: (y) == sum(curve_lam * y_bp, over=bp)

sos:
  curve:
    variable: curve_lam
    along: bp
    type: 2

assumptions:
  curve_complete:
    holds: x_bp AND y_bp
    description: >-
      piecewise 'curve': every breakpoint the curve runs through needs a row in
      'x_bp', 'y_bp'. A missing row does not shorten the curve. Attach the rows,
      or declare a where: that sets how far the curve runs.

Variable domains

curve_lam

\[ 0 \le \mathit{curve\_lam}_{b} \le 1 \qquad \forall\, b \in \mathcal{B} \]

x

\[ x \ge 0 \]

y

\[ y \ge 0 \]

Subject to

curve_convexity

\[ \sum_{b \in \mathcal{B}} \mathit{curve\_lam}_{b} = 1 \]

curve_x

\[ x = \sum_{b \in \mathcal{B}} \mathit{curve\_lam}_{b} \cdot \mathrm{x}^{\mathrm{bp}}_{b} \]

curve_y

\[ y = \sum_{b \in \mathcal{B}} \mathit{curve\_lam}_{b} \cdot \mathrm{y}^{\mathrm{bp}}_{b} \]

curve

\[ \left( \mathit{curve\_lam}_{b} \right)_{b \in \mathcal{B}} \in \mathrm{SOS}2 \]

Assumptions

curve_complete

\[ \mathrm{x}^{\mathrm{bp}}_{b} \text{ is defined} \wedge \mathrm{y}^{\mathrm{bp}}_{b} \text{ is defined} \qquad \forall\, b \in \mathcal{B} \]
dimensions:
  bp: { dtype: int, ordered: true }

parameters:
  x_bp: { dims: [bp] }
  y_bp: { dims: [bp] }

variables:
  x: { dims: [], bounds: { lower: 0 } }
  y: { dims: [], bounds: { lower: 0 } }
  curve_lam:
    dims: [bp]
    bounds: { lower: 0, upper: 1 }
    description: convex-combination weight on a breakpoint
  curve_seg:
    dims: [bp]
    domain: binary
    description: a binary per segment, 1 where the two members it spans may be nonzero

constraints:
  curve_convexity:
    dims: []
    expression: sum(curve_lam, over=bp) == 1
  curve_x:
    dims: []
    expression: (x) == sum(curve_lam * x_bp, over=bp)
  curve_y:
    dims: []
    expression: (y) == sum(curve_lam * y_bp, over=bp)
  curve_pick:
    dims: []
    expression: sum(curve_seg, over=bp) <= 1
  curve_adjacency:
    dims: [bp]
    expression: curve_lam <= (curve_seg + shift(curve_seg, along=bp, offset=1, edge=0))

assumptions:
  curve_complete:
    holds: x_bp AND y_bp
    description: >-
      piecewise 'curve': every breakpoint the curve runs through needs a row in
      'x_bp', 'y_bp'. A missing row does not shorten the curve. Attach the rows,
      or declare a where: that sets how far the curve runs.

Variable domains

curve_lam

\[ 0 \le \mathit{curve\_lam}_{b} \le 1 \qquad \forall\, b \in \mathcal{B} \]

curve_seg

\[ \mathit{curve\_seg}_{b} \in \{0, 1\} \qquad \forall\, b \in \mathcal{B} \]

x

\[ x \ge 0 \]

y

\[ y \ge 0 \]

Subject to

curve_adjacency

\[ \mathit{curve\_lam}_{b} \le \mathit{curve\_seg}_{b} + \mathit{curve\_seg}_{b \boxminus_{0} 1} \qquad \forall\, b \in \mathcal{B} \]

curve_convexity

\[ \sum_{b \in \mathcal{B}} \mathit{curve\_lam}_{b} = 1 \]

curve_pick

\[ \sum_{b \in \mathcal{B}} \mathit{curve\_seg}_{b} \le 1 \]

curve_x

\[ x = \sum_{b \in \mathcal{B}} \mathit{curve\_lam}_{b} \cdot \mathrm{x}^{\mathrm{bp}}_{b} \]

curve_y

\[ y = \sum_{b \in \mathcal{B}} \mathit{curve\_lam}_{b} \cdot \mathrm{y}^{\mathrm{bp}}_{b} \]

Assumptions

curve_complete

\[ \mathrm{x}^{\mathrm{bp}}_{b} \text{ is defined} \wedge \mathrm{y}^{\mathrm{bp}}_{b} \text{ is defined} \qquad \forall\, b \in \mathcal{B} \]

expand('piecewise') writes the curve out as the weights curve_lam, the row that makes them sum to one and one row per entry of links:. The call keeps the SOS that the curve states as an sos: entry, and expand() writes that SOS out too. curve_complete, a statement about the data (an assumption), stays in the spec after either call writes the curve out.

Keep this Python session open, because later steps use spec and ms.

Print before.yaml with each entry that expands (formulation) written out as plain entries:

python -m mathspec markdown before.yaml --expand
print(ms.to_markdown(spec.expand()))

The command and the Python call print the symbol tables of the spec, then the math. The "Math" tab under expand() in the section above contains the same math without the symbol tables.

Write an SOS out#

Save the spec in the "Before" tab below as sos.yaml, and write it out with ms.to_spec('sos.yaml').expand(). In this spec, the sos: entry pick requires at most one member of p to be non-zero:

dimensions:
  g: { dtype: str }

variables:
  p:
    dims: [g]
    bounds: { lower: 0, upper: 10 }

sos:
  pick:
    variable: p
    along: g
    type: 1

Variable domains

p

\[ 0 \le p_{g} \le 10 \qquad \forall\, g \in \mathcal{G} \]

Subject to

pick

\[ \left( p_{g} \right)_{g \in \mathcal{G}} \in \mathrm{SOS}1 \]
dimensions:
  g: { dtype: str }

variables:
  p:
    dims: [g]
    bounds: { lower: 0, upper: 10 }
  pick_seg:
    dims: [g]
    domain: binary
    description: a binary per member, 1 where that member may be nonzero

constraints:
  pick_pick:
    dims: []
    expression: sum(pick_seg, over=g) <= 1
  pick_nonzero:
    dims: [g]
    expression: p <= 10.0 * (pick_seg)

Variable domains

p

\[ 0 \le p_{g} \le 10 \qquad \forall\, g \in \mathcal{G} \]

pick_seg

\[ \mathit{pick\_seg}_{g} \in \{0, 1\} \qquad \forall\, g \in \mathcal{G} \]

Subject to

pick_nonzero

\[ p_{g} \le 10 \cdot \mathit{pick\_seg}_{g} \qquad \forall\, g \in \mathcal{G} \]

pick_pick

\[ \sum_{g \in \mathcal{G}} \mathit{pick\_seg}_{g} \le 1 \]

expand() replaces pick with the binary variable pick_seg and two constraints.

Check the expanded spec#

Load the expanded spec again, to check that mathspec accepts it as a spec of its own:

ms.to_spec(spec.expand().to_yaml())

The call returns a Spec without an error. Save the YAML text that to_yaml() returns as a file, and hand the file to an engine that reads only plain entries.

See also