Rich model representations#

A discopt Model renders itself as a typeset optimization problem in standard form [Williams, 2013] — minimize f(x) / subject to g(x) ≤ 0 / variable domains. In Jupyter the model displays automatically; you can also export the LaTeX or HTML for papers and docs.

Let us build a small mixed-integer nonlinear program and just look at it.

import discopt.modeling as dm

m = dm.Model("blend")
x = m.continuous("x", lb=0, ub=10)
y = m.binary("y")
m.minimize((x - 3) ** 2 + 2 * y)
m.subject_to(x + 5 * y >= 4)
m.subject_to(dm.exp(x) <= 100)

m  # renders as a typeset problem (HTML/LaTeX via MathJax)
Model blend (2 variables, 2 constraints)
$$ \begin{aligned} & \text{minimize} \quad && (x - 3)^{2} + 2 y \\ & \text{subject to} \quad && x + 5 y \ge 4 \\ & \quad && \exp(x) \le 100 \\ & \text{with} \quad && 0 \le x \le 10 \\ & \quad && y \in \{0, 1\} \end{aligned} $$

The constraints are shown in their natural form (x + 5y ≥ 4), even though they are stored internally in normalised g(x) ≤ 0 form. To get the markup directly — e.g. to paste into a paper — use to_latex():

print(m.to_latex())
\begin{aligned}
& \text{minimize} \quad && (x - 3)^{2} + 2 y \\
& \text{subject to} \quad && x + 5 y \ge 4 \\
&  \quad && \exp(x) \le 100 \\
& \text{with} \quad && 0 \le x \le 10 \\
&  \quad && y \in \{0, 1\}
\end{aligned}

Nonlinear expressions#

The renderer handles powers (superscripts), division (fractions), and the elementary functions (exp, log, sqrt, the trig family, …).

n = dm.Model("nlp")
a = n.continuous("a", lb=1, ub=5)
b = n.continuous("b", lb=1, ub=5)
n.maximize(dm.exp(a) / (b + 1))
n.subject_to(dm.sqrt(a) + b**2 <= 10)
n
Model nlp (2 variables, 1 constraint)
$$ \begin{aligned} & \text{maximize} \quad && \frac{\exp(a)}{b + 1} \\ & \text{subject to} \quad && \sqrt{a} + b^{2} \le 10 \\ & \text{with} \quad && 1 \le a \le 5 \\ & \quad && 1 \le b \le 5 \end{aligned} $$

Publication conventions#

to_latex() is meant to be pasted into a paper, so it follows the typographic conventions of mathematical writing rather than echoing the expression graph [International Organization for Standardization, 2019]:

  • products are implicit with the coefficient first and signs folded (x - 2y, not x + -2 \cdot y);

  • functions use their standard names (\arcsin, \log_{2}, \operatorname{erf}, \max\{\cdot\}, \lVert x \rVert_{2}), and composite atoms their textbook form (log1p(x) is \(\ln(1 + x)\), xlogx(x) is \(x \ln(x)\));

  • identifiers are typeset as symbols: alpha is \(\alpha\), T_max is \(T_{\mathrm{max}}\), multi-letter names are italic words;

  • an indexed sum whose terms share one structure is recovered as \(\sum_{i=lo}^{hi}\), and a whole-array sum as \(\sum_{i}\). Recovery is exact: every term must match the template at its index, otherwise the terms are written out;

  • vector domains read \(x \in [0, 10]^{6}\), and constraints keep the variables on the left.

Numbers are printed exactly by default (the shortest decimal that round-trips to the stored value), because rounding a coefficient changes the constraint being shown. Pass precision= to round for readability, and standalone=True for a complete document that compiles with pdflatex.

import numpy as np

p = dm.Model("plant")
x = p.continuous("x", shape=(6,), lb=0, ub=10)
on = p.binary("on", shape=(6,))
c = p.parameter("c", value=np.arange(1.0, 7.0))
alpha = p.continuous("alpha", lb=0.1, ub=2)
T = p.continuous("T_max", lb=300, ub=600)
p.minimize(dm.sum(c * x) + 25 * dm.sum(on) - alpha * dm.log(T))
p.subject_to(dm.sum(x[k] ** 2 for k in range(6)) <= 50)
p.subject_to(dm.sum(x[k + 1] - x[k] for k in range(5)) >= -4)
p.subject_to(x[0] <= 10 * on[0])
p.subject_to((4 / 9) * T - dm.sin(x[3]) ** 2 / (1 + x[4]) <= 1)
p.subject_to(dm.maximum(x[0], x[1], x[2]) - dm.log1p(x[5]) <= 8)
p
Model plant (4 variables, 5 constraints)
$$ \begin{aligned} & \text{minimize} \quad && \sum_{i} c_{i} x_{i} + 25 \sum_{i} \mathit{on}_{i} - \alpha \ln(T_{\mathrm{max}}) \\ & \text{subject to} \quad && \sum_{i=0}^{5} x_{i}^{2} \le 50 \\ & \quad && \sum_{i=1}^{5} (x_{i} - x_{i-1}) \ge -4 \\ & \quad && x_{0} \le 10\,\mathit{on}_{0} \\ & \quad && 0.4444444444444444 T_{\mathrm{max}} - \frac{\sin^{2}(x_{3})}{1 + x_{4}} \le 1 \\ & \quad && \max\{x_{0}, x_{1}, x_{2}\} - \ln(1 + x_{5}) \le 8 \\ & \text{with} \quad && x \in [0, 10]^{6} \\ & \quad && \mathit{on} \in \{0, 1\}^{6} \\ & \quad && 0.1 \le \alpha \le 2 \\ & \quad && 300 \le T_{\mathrm{max}} \le 600 \end{aligned} $$
print(p.to_latex(precision=4))
\begin{aligned}
& \text{minimize} \quad && \sum_{i} c_{i} x_{i} + 25 \sum_{i} \mathit{on}_{i} - \alpha \ln(T_{\mathrm{max}}) \\
& \text{subject to} \quad && \sum_{i=0}^{5} x_{i}^{2} \le 50 \\
&  \quad && \sum_{i=1}^{5} (x_{i} - x_{i-1}) \ge -4 \\
&  \quad && x_{0} \le 10\,\mathit{on}_{0} \\
&  \quad && 0.4444 T_{\mathrm{max}} - \frac{\sin^{2}(x_{3})}{1 + x_{4}} \le 1 \\
&  \quad && \max\{x_{0}, x_{1}, x_{2}\} - \ln(1 + x_{5}) \le 8 \\
& \text{with} \quad && x \in [0, 10]^{6} \\
&  \quad && \mathit{on} \in \{0, 1\}^{6} \\
&  \quad && 0.1 \le \alpha \le 2 \\
&  \quad && 300 \le T_{\mathrm{max}} \le 600
\end{aligned}

Large models are summarised#

Auto-display truncates large models so a notebook is never flooded: the first few constraints are shown, then a ⋮ row with the totals, and variables are summarised by type. Pass max_rows=None to to_latex() / to_html() for the complete form.

big = dm.Model("assignment")
z = [big.binary(f"z{i}") for i in range(40)]
big.minimize(sum((i + 1) * z[i] for i in range(40)))
for i in range(40):
    big.subject_to(z[i] + z[(i + 1) % 40] <= 1)
big  # truncated summary
Model assignment (40 variables, 40 constraints)
$$ \begin{aligned} & \text{minimize} \quad && 0 + z_{0} + 2 z_{1} + 3 z_{2} + 4 z_{3} + 5 z_{4} + 6 z_{5} + 7 z_{6} + 8 z_{7} + 9 z_{8} + 10 z_{9} + 11 z_{10} + 12 z_{11} + 13 z_{12} + 14 z_{13} + 15 z_{14} + 16 z_{15} + 17 z_{16} + 18 z_{17} + 19 z_{18} + 20 z_{19} + 21 z_{20} + 22 z_{21} + 23 z_{22} + 24 z_{23} + 25 z_{24} + 26 z_{25} + 27 z_{26} + 28 z_{27} + 29 z_{28} + 30 z_{29} + 31 z_{30} + 32 z_{31} + 33 z_{32} + 34 z_{33} + 35 z_{34} + 36 z_{35} + 37 z_{36} + 38 z_{37} + 39 z_{38} + 40 z_{39} \\ & \text{subject to} \quad && z_{0} + z_{1} \le 1 \\ & \quad && z_{1} + z_{2} \le 1 \\ & \quad && z_{2} + z_{3} \le 1 \\ & \quad && z_{3} + z_{4} \le 1 \\ & \quad && z_{4} + z_{5} \le 1 \\ & \quad && z_{5} + z_{6} \le 1 \\ & \quad && z_{6} + z_{7} \le 1 \\ & \quad && z_{7} + z_{8} \le 1 \\ & \quad && z_{8} + z_{9} \le 1 \\ & \quad && z_{9} + z_{10} \le 1 \\ & \quad && z_{10} + z_{11} \le 1 \\ & \quad && z_{11} + z_{12} \le 1 \\ & \quad && z_{12} + z_{13} \le 1 \\ & \quad && z_{13} + z_{14} \le 1 \\ & \quad && z_{14} + z_{15} \le 1 \\ & \quad && z_{15} + z_{16} \le 1 \\ & \quad && z_{16} + z_{17} \le 1 \\ & \quad && z_{17} + z_{18} \le 1 \\ & \quad && z_{18} + z_{19} \le 1 \\ & \quad && z_{19} + z_{20} \le 1 \\ & \quad && z_{20} + z_{21} \le 1 \\ & \quad && z_{21} + z_{22} \le 1 \\ & \quad && z_{22} + z_{23} \le 1 \\ & \quad && z_{23} + z_{24} \le 1 \\ & \quad && \vdots \quad (\text{40 constraints}) \\ & \text{with} \quad && 40\;\text{binary} \text{ variables} \end{aligned} $$

Exporting#

to_latex() returns a LaTeX aligned block; to_html() returns a self-contained HTML fragment (the LaTeX typeset via MathJax) with a header naming the model. Both take max_rows to control truncation and precision to round printed decimals; to_latex(standalone=True) wraps the block in a minimal amsmath document. The plain-text repr / Model.summary() remain available for terminals and logging.

print(m.to_html()[:300], "...")
<div class="discopt-model"><div style="font-weight:600">Model <code>blend</code> <span style="color:#888;font-weight:400">(2 variables, 2 constraints)</span></div>$$
\begin{aligned}
& \text{minimize} \quad && (x - 3)^{2} + 2 y \\
& \text{subject to} \quad && x + 5 y \ge 4 \\
&  \quad && \exp(x) \le  ...