quicopt.stochastic¶
stochastic ¶
quicopt.stochastic — optimization under uncertainty, written in Pyomo.
Part of a model's data is often unknown when the decision has to be made: demand, prices, yields, arrival times. Declare that data as random variables carrying distributions, and the model is solved over a sample of scenarios drawn from them.
Two rules describe the whole surface:
- A Pyomo variable given a distribution by
set_distributionis a random variable, not a decision variable. Every use of the variable references the same sample. - An expression containing a random variable is itself random, and cannot serve as an
objective or a constraint until an aggregator reduces it over the scenarios:
expectationfor the mean,cvarfor the tail,probfor a chance constraint.
set_scenarios sets how many scenarios are drawn
and from which seed. Both belong to the model, so repeated solves see the same sample.
Example
Order x units at 3 apiece against demand you will only learn later, pay
10 per unit of mismatch, and meet demand in at least 90% of scenarios:
import pyomo.environ as pyo
from quicopt import Client
from quicopt.stochastic import (Normal, set_distribution, set_scenarios,
expectation, prob, maximum)
m = pyo.ConcreteModel()
m.x = pyo.Var(bounds=(0, 200)) # decide now
m.demand = pyo.Var() # learn later
set_distribution(m, m.demand, Normal(100.0, 15.0))
set_scenarios(m, 512, seed=42)
m.cost = pyo.Objective(
expr=3 * m.x + 10 * expectation(maximum(m.demand - m.x, 0)),
sense=pyo.minimize)
m.service = pyo.Constraint(expr=prob(m.demand - m.x <= 0) >= 0.9)
result = Client().solve(m)
print(result.solution) # {'x1': 107.9…}
Every mention of m.demand refers to the same sample. Two independent random
variables are two Pyomo variables under two names.
maximum and minimum build the kinked expressions that price recourse:
maximum(demand - stock, 0) is the shortfall, and zero when there is none.
Models written this way are solved by the service. Attaching a local solver to one fails — an expectation has no value until the scenarios are drawn.
Empirical
dataclass
¶
A random variable given as a fixed scenario column of data.
Exactly Program.scenarios values, one per scenario. Several empirical
columns are read at the same scenario index, so columns drawn jointly stay
correlated — which is how a joint distribution is expressed.
Distribution ¶
Any distribution the service supports, named by head.
Use :class:Normal where it fits; use this for a distribution that has no
named class here yet, as in Distribution("uniform", 0.0, 1.0).
A parameter may be a number or a deterministic Pyomo expression. An expression gives an endogenous distribution — one whose parameters depend on the decision, such as a demand whose mean rises with the price you set.
Draw from the distribution head with the given parameters, each a
number or a deterministic Pyomo expression.
Source code in src/quicopt/stochastic.py
Normal ¶
Bases: Distribution
The normal distribution with mean mu and standard deviation sigma.
Declare a draw from N(mu, sigma) — the mean and the standard
deviation, each a number or a deterministic Pyomo expression.
Source code in src/quicopt/stochastic.py
set_distribution ¶
Turn the Pyomo variable v into a random variable drawn from dist.
v is no longer a decision variable: the solver is handed its value rather
than choosing it, and every use of v means the same sample within a scenario.
dist is a :class:Distribution such as Normal(100.0, 15.0), or an
:class:~quicopt.ir.Empirical column holding one observed value per scenario.
name defaults to the variable's own name and is what identifies the random
variable, so two independent ones need two names. Returns v.
Give v no bounds and no domain. Its distribution already says what values
it takes, and a variable carrying both is rejected on import.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Source code in src/quicopt/stochastic.py
set_scenarios ¶
Draw n scenarios, optionally from a given seed.
More scenarios estimate the true problem more closely and cost more to solve. Both settings belong to the model, not to the solve, so the same model always faces the same sample and two solves of it are comparable. Left unset, a model is solved over one scenario, where every random variable takes a single value.
n and seed are both at least 1. Leaving seed out keeps the current
one.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/quicopt/stochastic.py
expectation ¶
The expected value of x over the scenarios.
Minimizing an expectation optimizes the average case and says nothing about
the bad ones; use :func:cvar when the bad ones are what matter.
x is any expression containing a random variable. The result is
deterministic, and can be used anywhere a number can.
Source code in src/quicopt/stochastic.py
cvar ¶
The conditional value at risk of x at level alpha.
The mean of x over its worst 1 − alpha fraction of scenarios — at
alpha = 0.95, the average of the worst 5%. Minimizing it optimizes the
tail instead of the average, and is the usual way to ask for a solution that
holds up in bad scenarios rather than merely on average.
x is any expression containing a random variable. alpha is the tail
level, a plain number strictly between 0 and 1; it cannot depend on a decision.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Source code in src/quicopt/stochastic.py
prob ¶
The probability that relation holds — the fraction of scenarios it does.
This is what a chance constraint is built from::
m.service = pyo.Constraint(expr=prob(m.demand - m.x <= 0) >= 0.9)
which reads as demand is met in at least 90% of scenarios. Note that the
line holds two comparisons: the one inside prob is the event being
measured, the outer one is the service level demanded of it.
relation is a comparison, a <= b or a >= b, with at least one side
containing a random variable. The result is a probability between 0 and 1.
Raises:
| Type | Description |
|---|---|
TypeError
|
If the argument is not a comparison. An equality is refused too: for a continuous quantity its probability is zero. |
ValueError
|
If the comparison is strict. Use |
Source code in src/quicopt/stochastic.py
maximum ¶
The largest of two or more expressions.
This is how recourse is priced: maximum(demand - stock, 0) is the
shortfall, and zero whenever stock covers demand. Python's built-in max
cannot do this — it would have to compare the arguments, and their values are
not known yet.
Raises:
| Type | Description |
|---|---|
ValueError
|
If given fewer than two arguments. |
Source code in src/quicopt/stochastic.py
minimum ¶
The smallest of two or more expressions.
The counterpart of :func:maximum — minimum(demand, stock) is what you
actually sell.
Raises:
| Type | Description |
|---|---|
ValueError
|
If given fewer than two arguments. |