Repository navigation
Linear_Programs
A scenario linear program (LP) minimises a linear cost subject to linear constraints that depend on an uncertain parameter
This page covers the LP formulation, its inputs in the web interface, in the program JSON file and in Python, and a worked example. For a first walk-through of the interface itself, see the Web Interface example; for Python usage, see the Python example.
The most general LP is the relaxed–regularized LP:
with
| Symbol | Meaning |
|---|---|
| decision vector | |
| cost vector | |
|
|
scenario constraints: each scenario |
|
|
hard constraints, which do not depend on |
| slack of scenario |
|
| penalty on the slacks (relaxation) | |
|
|
weight, reference point and norm of the regularization term |
The hard constraints are always enforced. They are never part of the support list, so they do not count towards the complexity
Each variant is a special case of the formulation above. The hard constraints
| Variant |
option value |
Objective | Scenario constraints |
|---|---|---|---|
| Robust | robust |
||
| Regularized | regularization |
||
| Relaxed | relaxation |
|
|
| Relaxed–regularized | regularization-relaxation |
|
-
Robust:
$\tau = 0$ ,$\rho = 0$ and$\zeta_i = 0$ for all$i$ . Every scenario constraint must hold. -
Relaxed:
$\tau = 0$ . Scenario constraints may be violated, and each violation is penalised by$\rho \zeta_i$ . -
Regularized:
$\rho = 0$ and$\zeta_i = 0$ . The solution is pulled towards$\bar{x}$ .
How the variant is selected:
-
Web interface: the Select Option dropdown sets the variant, and it shows only the parameters that variant uses (
$\tau$ ,$\bar{x}$ and$p$ for regularization;$\rho$ for relaxation). Changing the option clears$\tau$ ,$\rho$ ,$p$ and$\bar{x}$ . The selected variant also decides which parameters the server uses: for Robust and Regularization$\rho$ is ignored, and for Robust and Relaxation$\tau$ is ignored, even if a hidden field still holds a value. Ignored fields are neither applied nor swept. -
Relaxed options with
$\rho = 0$ : the slacks are kept even when$\rho = 0$ . The scenario constraints can then be violated at no cost, and an LP is usually unbounded. The error column then showsLP did not solve to optimality. Status: unbounded, .... Use$\rho > 0$ . -
Python:
solve_lphas nooptionargument. Slack variables are added wheninclude_slack=True. With the defaultinclude_slack=None, they are added wheneverrho != 0. The regularization term is added only whentau > 0, so withtau = 0the program is a pure LP.
You can set the norm p, or with the Python argument norm_type. If you leave it empty, inf or fro; a value below 1 is rejected with p (norm order) must be at least 1.
The term is the vector inf), and fro is the same as norm_type=1, 2, 3, np.inf, 'fro' and so on.
The norm decides which solvers can be used. With inf the term is linear, so the program stays an LP. With fro) it adds a second-order cone, and other orders add power cones; these need a conic solver such as MOSEK, CLARABEL or SCS. Without regularization (
| Symbol | Shape | Web interface | Program JSON key |
solve_lp argument |
|---|---|---|---|---|
| integer |
dimension field d | n_x |
inferred from A_d
|
|
| integer |
dimension field 𝔪 | rows_A |
inferred from A_d
|
|
| integer |
dimension field 𝔫 | rows_G |
inferred from G
|
|
| Edit c | c |
c: array (d, 1) or (d,)
|
||
delta[k]
|
Edit A(δ) (symbolic mode only) | A_d |
A_d: callable, delta -> (m, d) array |
|
delta[k]
|
Edit b(δ) (symbolic mode only) | b_d |
b_d: callable, delta -> (m, 1) or (m,) array |
|
|
|
Edit G (enabled when 𝔫 > 0) | G |
G: array (n, d), or np.array([])
|
|
|
|
Edit h (enabled when 𝔫 > 0) | h |
h: array (n, 1) or (n,), or np.array([])
|
|
| scenarios | see input modes | Scenarios upload box | separate file |
deltas: 2-D array (N, q)
|
| Symbol | Web interface | Program JSON key | Python | Default |
|---|---|---|---|---|
| τ ≥ 0 (comma-separated list allowed) | tau |
tau (float) |
0 | |
|
Edit x̄ ( |
x_ref |
x_ref: scalar (applied to every entry) or d entries |
0 | |
| p | p |
norm_type |
2 | |
| ρ ≥ 0 (comma-separated list allowed) | rho |
rho (float) |
0 | |
| Confidence ... where β | confidence |
beta argument of quantify_risk(k, N, beta)
|
none; required, with 1e-06
|
|
| solver | Solver dropdown | not stored |
solver, e.g. "CLARABEL"
|
CVXPY's choice (None) |
-
deltasshould be 2-D, with shape(N, q), even when$q = 1$ . For example, usenp.loadtxt(path, delimiter=",", ndmin=2). (A 1-D array also works with the string parser, becausedelta[0]of a scalar scenario is the scalar itself, but your own callables then receive scalars.) An emptydeltasraisesValueError. -
b_d(delta)andhare reshaped to columns, so(m, 1)and(m,)give the same constraints. -
Hard constraints are used only when both
Gandhare non-empty. -
x_refmust contain either 1 ordentries; any other size raises aValueError. -
solve_lp(deltas, A_d, b_d, G, h, c, tau=0.0, x_ref=np.array([0.0]), rho=0.0, norm_type=2, solver=None, include_slack=None)returns(x, zeta, cost, N, complexity, constraints, degeneracy):-
xhas shape(d, 1). -
zetahas shape(N,)and is all zeros without slacks. -
costis the optimal value of the full objective, including the$\tau$ and$\rho$ terms. The web interface reports only$c^\top x^\ast$ . -
constraintsholds one CVXPY constraint per scenario.
Pass
complexityandNtoquantify_riskto get the risk bounds. Implementation details are on the LP.py and Risk.py pages. -
In symbolic mode, each entry of delta[0], delta[1], …, delta[q-1] of a scenario. Indexing starts at 0: delta[0] is the first column of the scenario file. Expressions are evaluated by a restricted parser, which allows:
- numbers,
+ - * / // % **, unary minus and parentheses;^also means power, as in SymPy, with the precedence of**; -
delta[k]; - the functions
sin,cos,tan,asin,acos,atan,atan2,sinh,cosh,tanh,exp,log,ln,log10,log2,sqrt,abs,Abs,floor,ceil,ceiling,min,Min,maxandMax, and the constantspi,eandE, e.g.sin(delta[1]),exp(-delta[0]),pi * delta[2]ordelta[0]^2; - functions and constants of Python's
mathmodule with themath.prefix, e.g.math.sin(delta[1]).
Anything else is rejected; for example sec(delta[1]) gives the error disallowed name: sec. The matrices delta; in a program file they may hold numbers or constant expressions such as "1/2".
In the program JSON, entries may be plain numbers or quoted strings: [[0], [1]] and [["0"], ["1"]] are equivalent. The benchmark files quote every entry. Expressions must be quoted ("delta[0]", "-delta[0]*delta[2]").
Use the Symbolic / Numeric toggle in the web interface, or the JSON key mode, to pick how scenario data are supplied.
-
Symbolic (default).
$A(\delta)$ and$b(\delta)$ are entered as expressions. The scenario file has one row per scenario and$q$ columns,delta[0], ..., delta[q-1], so it is an$N \times q$ table. -
Numeric. The Edit A(δ) and Edit b(δ) buttons are hidden. Instead, row
$i$ of the scenario file holds the already evaluated$A_i = A(\delta_i)$ and$b_i = b(\delta_i)$ : first$A_i$ flattened row by row ($A_i[0,0], A_i[0,1], \dots, A_i[0,d-1], A_i[1,0], \dots$ ), then the$\mathfrak{m}$ entries of$b_i$ . Each row therefore has$\mathfrak{m} d + \mathfrak{m}$ numbers. The server checks this and otherwise reportsEach scenario row must have ... elements. Because there is no$A(\delta)$ to infer them from, the dimensions$d$ and$\mathfrak{m}$ must be set, through the dimension fields or the JSON keysn_x/candrows_A.
Both modes solve the same program. The example below gives identical results in either mode. Scenario files are uploaded as CSV without a header row, or in another format accepted by the upload box (see the Web Interface example).
The 0.02, 0.1, 1;
By the union bound, all of these bounds hold together with confidence at least
A program file holds everything except the scenarios and the solver. Load it with Upload Program, next to the LP/QP/SDP tabs: either choose the file or paste its content, then press Load. After a solve, the Download Program button saves the current problem in the same format.
| Key | Required | Content |
|---|---|---|
type |
yes | "LP" |
mode |
no |
"symbolic" (default) or "numeric"
|
option |
no |
"robust", "regularization", "relaxation" or "regularization-relaxation". If absent, it is inferred from the first value of rho and tau: both > 0 gives regularization-relaxation, only rho > 0 gives relaxation, only tau > 0 gives regularization, otherwise robust. |
c |
yes |
|
A_d, b_d
|
symbolic mode |
|
G, h
|
no (both or neither) |
|
n_x, rows_A, rows_G
|
rows_A in numeric mode |
integers c, A_d or G are present, the dimensions are taken from their shapes. |
rho, tau
|
no | strings, e.g. "0" or "0.1, 1, 10"
|
confidence |
no, but β must be set before solving | β, e.g. "1e-06", strictly between 0 and 1 |
p |
no | a number "1", "2", "3"), "inf" or "fro"
|
x_ref |
no |
[["0"], ["0"]]
|
Optional fields that a problem does not use can be omitted or left empty ([], "" or null), e.g. "G": []. For the robust option, leave tau and rho at "0" or omit them.
Benchmark: benchmarks/LP_half_width_2d.
The goal is the smallest interval
so that
data/program_symbolic.json (whitespace reformatted):
{
"type": "LP",
"A_d": [["-1", "-1"], ["1", "-1"]],
"b_d": [["delta[0]"], ["-delta[0]"]],
"c": [["0"], ["1"]],
"rho": "0",
"tau": "0",
"confidence": "1e-06",
"mode": "symbolic",
"n_x": 2,
"rows_A": 2,
"rows_G": 0
}The scenario file data/scenarios.csv has 100 rows with one number each (delta[0]).
- Click Upload Program, choose
data/program_symbolic.jsonand press Load. The LP tab opens with option Robust,$d = 2$ , 𝔪 = 2 and 𝔫 = 0. - Under Scenarios, upload
data/scenarios.csv. - Choose a solver (CLARABEL was used for the numbers below) and press Solve.
The results table shows:
- optimal cost 0.4983;
- optimal x = (0.5006, 0.4983);
- complexity 2;
- risk bounds [0.0000, 0.2085];
- confidence 0.999999;
- no degeneracy;
- total constraints 100 (the 100 scenario constraints; there are no hard constraints).
Run the snippet from the root of the Scen-Opt repository, so that src and benchmarks/ can be found. It reads the same JSON file and converts it with the parser that the web interface uses.
import json
import numpy as np
from src.LP import solve_lp
from src.Risk import quantify_risk
from src.parsing import generate_matrix_function, generate_matrix
data = "benchmarks/LP_half_width_2d/data/"
prog = json.load(open(data + "program_symbolic.json"))
# Turn the JSON entries into Python objects, exactly as the web interface does
A_d = generate_matrix_function(json.dumps(prog["A_d"])) # callable: delta -> (2, 2) array
b_d = generate_matrix_function(json.dumps(prog["b_d"])) # callable: delta -> (2, 1) array
c = generate_matrix(json.dumps(prog["c"])) # (2, 1) array
G, h = np.array([]), np.array([]) # no hard constraints
deltas = np.loadtxt(data + "scenarios.csv", delimiter=",", ndmin=2) # shape (100, 1)
# Robust LP: tau = 0 and rho = 0 (the defaults)
x, zeta, cost, N, k, constraints, degenerate = solve_lp(
deltas, A_d, b_d, G, h, c, solver="CLARABEL")
eps_L, eps_U = quantify_risk(k, N, beta=1e-6)
print("x* = [center, half-width]:", x.ravel())
print("cost:", cost)
print("N =", N, "| k =", k, "| degenerate:", degenerate)
print("risk bounds:", eps_L, eps_U)Output (CVXPY 1.7.2, CLARABEL):
x* = [center, half-width]: [0.5006347 0.49830997]
cost: 0.498309968245085
N = 100 | k = 2 | degenerate: False
risk bounds: 0.0 0.20852406037505716
The JSON is not required: you can replace the A_d, b_d and c lines with hand-written Python objects and get the same result:
A_d = lambda delta: np.array([[-1.0, -1.0], [1.0, -1.0]])
b_d = lambda delta: np.array([[delta[0]], [-delta[0]]]) # (2, 1)
c = np.array([[0.0], [1.0]])-
Solution: the interval is
$[0.0023,\ 0.9989]$ . -
Complexity: the support list consists of the smallest sample (0.0023) and the largest (0.9989), so
$k = 2$ . -
Risk: with confidence at least
$1 - 10^{-6}$ , the probability that a new point drawn from the same distribution falls outside the interval is at most$\overline{\varepsilon} = 0.2085$ . The lower bound is$\underline{\varepsilon} = 0$ . -
MOSEK: the stored MOSEK result in
results/metrics.jsonagrees.
For numeric mode, use data/program_numeric.json, which has no A_d/b_d and declares "rows_A": 2, together with data/scenarios_numeric.csv. Each row of that file is
-1, -1, 1, -1, 0.013984..., -0.013984...
and the result is the same as in symbolic mode.
Select option Relaxation and enter ρ = 0.02, 0.1, 1. The tool then solves three relaxed LPs (web interface, CLARABEL, β = 10⁻⁶):
| half-width |
violated points | |||
|---|---|---|---|---|
| 0.02 | 0.2736 | 50 | 50 | [0.2367, 0.7638] |
| 0.1 | 0.4644 | 10 | 10 | [0, 0.3432] |
| 1 | 0.4983 | 0 | 2 | [0, 0.2085] |
A small
Each benchmark's README describes the problem, its matrices and the expected results.
-
LP_growth_bound_12d: data-driven growth bound for a finite abstraction of 3-D vehicle dynamics, from 3127 sampled transitions. Robust LP with$d = 12$ ; degeneracy is detected. -
LP_inventory_11d: daily ordering of 5 perishable products under uncertain yield, demand and packing, with budget and warehouse limits ($d = 11$ ,$N = 500$ ). The slacks are part of$x$ and their penalty is built into$c$ , so the program files use Robust with$\rho = 0$ . -
LP_portfolio_cvar_13d: CVaR (95%) portfolio over 8 ETFs, using historical returns from Yahoo Finance ($d = 13$ ,$N = 1255$ ). As in the inventory benchmark, the penalty$\rho = 0.015936$ is built into$c$ and the program files use$\rho = 0$ . -
LP_power_dispatch_120d: 24-hour dispatch of 3 thermal generators with uncertain wind and solar output ($d = 120$ , 330 hard constraints).
Each of these benchmarks ships data/program_symbolic.json with data/scenarios.csv, and data/program_numeric.json with data/scenarios_numeric.csv.
- Quadratic Programs and Semidefinite Programs
- Scenario Theory: complexity, risk, degeneracy
- LP.py, Risk.py, Miscellaneous.py: implementation details