Repository navigation
Release 4.1.0 (20.10.2026) — two-stage & recourse stochastic investment
Release 4.1.0 (20.10.2026) — two-stage & recourse stochastic investment, realized-dispatch (UC→dispatch) solves, rolling unit-commitment continuity; construction lead time; indirect-unit capacity, per-unit min-load & reserve fixes (schema v76)
Schema change (adds solve.stochastic_invest_method,
solve.shared_invest_periods, solve.shared_operation_periods,
solve.dispatch_realization_method,
reserve__upDown__group.reserve_duration, and per-entity
construction_lead_time / construction_lead_time_method on
unit/connection/node; migrated automatically). The stochastic
shared-decision window parameters use positive names:
shared_invest_periods / shared_operation_periods name the periods
whose decisions are shared (identical) across stochastic branches.
-
Mixed period/time Map shapes within one parameter. A parameter can
now be authored with different Map shapes for different entities (for
example aflowGroup.min_instant_flowthat is aMap(period)on one
group and aMap(time)on another). Each row keeps its own index
semantics regardless of the order of rows in the database, instead of
being silently misplaced or dropped. As a consequence of classifying
every row, a Map shape that a parameter does not accept — for example a
time Map (or any other non-accepted shape) on a period-only parameter —
now raises an error that names the offending entity, instead of
being silently ignored. Databases that relied on such input being
dropped will stop with a clear message identifying the entity and
parameter to fix. -
Construction lead time. New per-entity parameters
construction_lead_time([years]) andconstruction_lead_time_method
(immediate/closest_seam/previous_seam/next_seam) model
physical build time onunit,connectionandnodeinvestments.
Capacity ordered in a period becomes available only after the lead
time, snapped to a period boundary — the investment decision and its
overnight annuity stay in the order period, while balances, flows and
reserves see the plant (and its full technical lifetime) only from its
commissioning year. It applies to deterministic and stochastic models
alike, and composes with the invest non-anticipativity window to stage
long- versus short-lead technologies across a stochastic reveal
automatically (a long-lead asset serving a post-reveal period must be
ordered in a pre-reveal, tied period). Left at the default 0 (or
immediate) every existing result is byte-identical. -
Standard two-stage stochastic investment (the main mode). The new
solve.shared_invest_periodsparameter names the periods over
which investment is a single shared here-and-now decision:v_investand
v_divestare tied across the stochastic branches over those periods, while
later periods invest per branch (recourse). Compose it with
stochastic_invest_method = recourseandshared_operation_periods = []
to get the classic capacity-expansion structure — shared first-period
investment, per-scenario recourse investment thereafter, operations free from
the first timestep. Both invest and divest are tied so the net first-stage
capacity is genuinely shared. Left unset (or empty) it adds no tie and every
existing result is byte-identical; on a non-recoursesolve it is a harmless
no-op. -
Per-scenario stochastic investment (opt-in
solve.stochastic_invest_method = recourse): each stochastic branch can now
make its own investment decisions at and after the branching period, with
probability-weighted investment costs and per-scenario investment limits — a
wait-and-see analysis giving per-scenario optimal plans and their expected
cost. Committed results report the realized scenario; non-realized branch
investments are available under the horizon-output debug flag
(model.output_horizon). Default behaviour (none) is unchanged, and the
option is rejected in combination with Benders decomposition. -
Mid-horizon reveal / hedged two-stage stochastics. A stochastic branch
may now start at any period boundary, not only the solve's first step, so the
pre-reveal periods stay a single shared here-and-now investment (a genuine
first stage) while the branches fan out into per-scenario recourse from the
reveal onward. Underrecoursethis makes the objective a real two-stage
hedge — strictly between the wait-and-see bound and the deterministic
mean-value plan (positive EVPI/VSS). Storage state is carried across the
reveal for stochastic-group storage nodes. Mid-horizon reveal is a general
stochastics capability; the previous multi-period restriction is lifted. A
malformed reveal (analysis time off a period boundary, or a branching period
without exactly one realized branch) is now rejected with a clear error. -
Separate realized-dispatch (unit-commitment → dispatch) solve
(solve.dispatch_realization_method). A stochastic solve can now hand its
committed decisions to a separate deterministic dispatch that re-solves the
realized operating conditions with the plan's unit commitment, investments
and storage held fixed — the classic commit-under-uncertainty then
dispatch-against-what-happened structure. Setdispatch_realization_method = separate_dispatchand the engine automatically runs a
<solve>__realized_dispatchsibling after the commitment solve: it pins the
committed online units (v_online) and fixes the committed capacity while
the flows re-optimize against the realized data, and the realized dispatch's
results are what the outputs report. Under a rolling-window solve it runs per
roll — each roll's commitment solve is followed by the realized dispatch of
that roll's realized steps, whose end-state carries into the next roll. The
defaultwithin_solvekeeps today's behaviour (operations solved inside the
stochastic solve) and is byte-identical. Supported for a single top-level
solve and for rolling-window solves; not yet for contained/nested solves. -
Rolling unit commitment is now continuous across roll boundaries. In a
rolling-window solve a unit's commitment is carried from one roll to the next
instead of being re-decided cold at each boundary: a unit still committed at
a roll's end is not charged a fresh start-up at the start of the next roll,
and a minimum up-time / down-time obligation opened near a boundary
constrains the following roll's early steps (within the same period).
Previously each roll started its unit commitment from scratch, over-counting
start-up costs and letting committable units cycle or break their minimum
up/down times across boundaries. Results of rolling solves with committable
(online) units change — typically lower start-up cost and fewer boundary
start-ups; non-rolling solves and models without unit commitment are
unchanged. -
Minimum load now applies once per unit, on the sum of its outputs.
min_load× online capacity is a floor on the sum of the outputs that
define the unit's capacity (outputs withconversion_flow_coeff ≠ 0; the
single flow of a one-input/one-output unit; the input of a unit without
outputs). Inputs no longer get a floor of their own. Behaviour changes for
every unit withmin_loadand online variables that has several inputs
or outputs (or a delay):- a unit with two inputs no longer has to burn
min_load× online of
each input — an expensive second input is no longer forced, and a
second input that cannot supply no longer keeps the unit offline; - a unit with an efficiency above 1 (heat-pump-like) can now run with
min_load(before, its input floor pushed the outputs above capacity); - an extraction CHP can run in pure-heat mode while online (before, the
input floor forced electricity out of it); capacity_min_coeffon an output no longer lowers or switches off the
unit's minimum load (setting it below 1, e.g. 0 on every output, used to
do that): the fullmin_loadapplies. Lowermin_loadinstead
(min_load = 0switches it off). The model warns about such units;- one-input/one-output units behave as before, except that an authored
capacity_min_coeffbelow 1 no longer softens their floor.
- a unit with two inputs no longer has to burn
-
capacity_min_coeffon an output is an optional per-output floor
(default 0, schema v70). While the unit is online, an output of a unit
with several outputs must be at leastcapacity_min_coeff × min_load × online capacity. 0 (the new default) means no separate floor; values
outside 0..1 are an error. The schema migration deletes stored output
values outside 0..1 (they were copied from old flow coefficients by an old
migration, e.g. 2 or −0.025) and values equal to the same output's
capacity_max_coeff(≠ 1, also left by that migration), and lists them;
it also lists the units whose kept values now act differently (all below
1: no longer lower the minimum load; > 0 on a unit with several outputs:
now a floor on that output). The listing is printed on the console only. -
New:
input_share_min(unit--inputNode, default 0). While the unit is
running, the input must supply at least this share of the unit's current
input energy (flow ×conversion_flow_coeff): e.g. 0.3 makes the input
supply 30 % of whatever the unit uses. It applies with or without online
variables. Shares on all inputs summing to 1 fix the blend. Errors: a share
outside 0..1, shares of a unit summing above 1, a share above the same
input'sinput_share_max, and a share on a unit with a negative input
conversion_flow_coeff; a share on an input with
conversion_flow_coeff = 0or on a unit with one input has no effect (a
warning). The schema migration deletes all values of the old
unit__inputNode.capacity_min_coeff(the model never applied them, and
their old meaning is unrelated) and prints the deleted values; re-author
them asinput_share_minwhere a mixing minimum is meant. -
Downward reserve can no longer take a unit below its floor. Downward
reserve at a node a unit feeds, and upward reserve at a node a unit
consumes from (a consumption cut), cannot exceed the unit's flow, and for
an online unit cannot take the sum of its outputs belowmin_load×
online capacity (nor an output withcapacity_min_coeffbelow its floor).
Before, a unit could offer its full capacity as downward reserve even
while off. Themax_sharelimit (× capacity) still applies on top.
Connections are not limited this way. -
Reserve shortfall can cover the whole requirement and is always
penalised. The reserve shortfall is now sized by the largest
requirement of the group —reservationfor timeseries requirements
(unchanged), Σ capacity ×increase_reserve_ratiofor dynamic and
Σ capacity ×large_failure_ratiofor large-failure requirements, summed
when a group combines methods. Before it was sized byreservationonly,
so a dynamic or large-failure group with a small or noreservationof its
own could leave its requirement unmet only up to that amount (or not at
all) and, without areservation, without penalty — a requirement that
could not be met made the model infeasible. The reserve shortfall outputs
use the same sizing. -
Fix:
penalty_reservedefaults to 5000 again (schema v70). A reserve
group without its ownpenalty_reservehad no shortfall penalty at all,
so its reserve requirement was in effect optional. The default of 5000
[CUR/MW per hour of shortfall] — the value of the 3.x model and of the
schema until an earlier migration cleared it — now applies to every
reserve group without a value; authored values are unchanged. Models with
such groups now procure the reserve (or pay for the shortfall), which can
raise the objective. -
Fix: connection
delayis ignored, with a warning. The reference
said it works withtransfer_method = no_losses_no_variable_cost; in fact
no connection delays its flow (neither did the 3.x model). Setting it
still changed the connection: its efficiency losses were dropped and a
regularconnection became one-way. Until connection delay is
implemented, a connectiondelayis ignored — the connection is modelled
exactly as without it — and the model warns once per such connection.
Use a unit for a delayed flow. The reference, the how-to and the
parameter description (schema v70 step) say so. -
Known limitations (follow-ups): upward reserve is still not limited by
the online capacity (an online unit that is off can offer upward reserve);
units consuming more to offer downward reserve are not limited by their
input capacity; connections are not limited by their flow when offering
reserve; the reserve balance does not yet convert input-side reserve of a
one-input/one-output unit by its efficiency; dynamic and large-failure
(n-1) reserves are only active when some group of the same reserve and
direction has a non-zeroreservation; the large-failure requirement sums
all failing processes into one requirement instead of one requirement per
failing process (over-procures reserve when several processes can fail);
connectiondelayis not applied (it is ignored with a warning). -
Fix: multi-input/multi-output (indirect) units with
conversion_method = min_load_efficiencynow burn the no-load fuel.
Their input wasslope × outputonly; thesection × online capacity
part of the efficiency curve was missing, so fuel use, commodity cost and
CO2 were understated (by 31 % at full load for a unit withmin_load0.2,
efficiency_at_min_load0.4 andefficiency0.9). The input is now
slope × output + section × online capacity, as for one-input/one-output
units. -
Fix: a unit with a constant
delayruns again. Only a map of delays
was read into the delay durations; with a constant the unit's inputs were
dropped from its conversion and its output was forced to zero without a
message. Rolling-window solves with a delayed unit (constant or map) no
longer stop with an error. The delay tables of a delayed unit are now
checked, and an inconsistency stops the run with a clear error instead of
zeroing the unit. Note:delayon connections is not applied; it is
ignored with a warning (see above). -
Fix:
input_share_maxon a delayed unit's input divides by the input's
conversion_flow_coeff. For delayed units the input limit used
min(conversion_flow_coeff, 1), so an input with a coefficient above 1
was allowed too much (e.g. coefficient 2 withinput_share_max0.6 let
the unit run at full output instead of 60 %). Delayed and undelayed
inputs now get the same limit. -
Input (fuel) limits of units follow
input_share_max. An input of a
multi-input/multi-output (indirect) unit may now supply at most
input_share_max× the input that runs the unit at full output
((slope × W + section) / conversion_flow_coeffper unit of capacity,W
being the most fuel the outputs can demand within the capacity and their
capacity_max_coeff), times availability and capacity. The default 1 is
no effective limit; e.g. two fuels with 0.6 each can run the unit at full
output only together. Units without investment in the solve get the limit
as a bound on the flow variable (fewer constraint rows); investing units
keep a constraint whose invested-capacity term now carries the same
multiplier (it used 1, so the fuel limit did not follow invested capacity
correctly). Direct units honour an authoredinput_share_maxas a cap on
their flow; source-side ramp limits of indirect inputs scale with the
input limit. The previous fuel bound summedconversion_flow_coeff × capacity_max_coeffover all outputs, ignoring that their sum is capped by
capacity (looser than needed). -
Rolling-window sub-solves now apply the same capacity coefficients as
single solves. Outputcapacity_max_coeff, the uncapped
conversion_flow_coeff = 0edges and the indirect input limits were not
carried into the roll sub-solves. -
Input-side capacity coefficients renamed to
input_share_max/
input_share_min(schema v70). Onunit__inputNode,
capacity_max_coeffis nowinput_share_max(values move unchanged) and
capacity_min_coeffisinput_share_min(see below: its old values are
deleted);unit__outputNodekeepscapacity_max_coeff/
capacity_min_coeff. Existing databases are migrated automatically. -
Multi-output indirect units (e.g. CHP) are no longer capped below their
capacity by their fuel input. The input flow of an indirect unit carries
only a loose solver bound; it was set toslope × capacity, i.e. enough fuel
for one output at full capacity. With several outputs, or an output
conversion_flow_coeffabove 1, that bound became binding and silently
limited the unit (e.g. an extraction CHP's electricity pinned well below its
cap). The bound now follows the conversion equation:
capacity × slope × Σ_outputs(conversion_flow_coeff × capacity_max_coeff) / input conversion_flow_coeff, scaled up for delayed units. -
capacity_max_coeffon the outputs of indirect units is enforced again.
Indirect output flows are bounded by existing capacity (plus investment), but
the per-outputcapacity_max_coefffactor was only folded in for direct
units, so an indirect output could run up to the full unit capacity. -
The capacity of an indirect unit now caps the sum of its outputs, as
documented ("the maximum sum of output flows"). Previously only each output
was capped individually, so a two-output unit (e.g. a CHP) could deliver up
to twice its capacity, and investment solves sized such units by their
largest single output. The newmaxOutputSumconstraint bounds
Σ outputs + upward reservesby existing plus invested capacity (times
availability); online units additionally boundΣ outputsby the online
capacity. Outputs withconversion_flow_coeff = 0stay excluded;
capacity_max_coeffremains a per-output cap. -
Results of models with multi-output indirect units can change (lower or
equal cost where the fuel bound was binding; respected output caps; higher
cost or more investment where the sum of outputs exceeded capacity). -
Capacity multipliers now scale invested capacity too.
capacity_max_coeff
andavailabilitymultiplied only the existing capacity; invested (and
retired) capacity entered the capacity constraints unscaled. An output with
capacity_max_coeff = 0.2on a unit that invested 900 MW could deliver
900 MW instead of 180 MW, and a unit withavailability = 0.5could use all
of its new capacity. The flow caps of direct units, indirect-unit outputs,
the output-sum cap and the reverse direction of two-way units now apply
capacity_max_coeff × availabilitytoexisting + invested − retired.
Investment models with such multipliers will build more capacity. -
Ramp limits now follow invested capacity and
capacity_max_coeff. The
ramp constraints used the existing capacity only, so a ramp-limited unit
built by the model could not change its output at all, and the edge's
capacity_max_coeffwas not applied (the documented behaviour). -
The reverse direction of a two-way connection can use invested capacity.
Reverse flow ofno_losses_no_variable_costconnections was capped at the
existing capacity only. -
capacity_max_coeff = 0is a zero cap, as documented. The engine
treated an edge withcapacity_max_coeff = 0as uncapped (bounded only by
max_flow_for_unconstrained_variables) in its structural flow bound — a
leftover from the parameter rename, when the old coefficient was the flow
coefficient. The uncapped (pass-through) edges are the
conversion_flow_coeff = 0ones; those are now also kept out of the online
flow cap, ramp limits and min-load constraints, and are no longer scaled by
availability. Themax_flow_for_unconstrained_variablesand
capacity_max_coeffdescriptions are corrected (schema v70). -
capacity_max_coeffnow applies to online (unit-commitment) units' flow
cap, and that cap no longer limits the fuel input of online multi-output
units (it is tied to the outputs by the conversion equation). -
invest_retire_no_limitlifts the investment limits like
invest_no_limit. It was not recognised as a no-limit method: the unit's
maximum capacity stayed at its existing capacity, so a unit with no existing
capacity could not invest at all. -
Solves no longer write an unused
flextool.solfile, and HiGHS no
longer warns "The cost row for the Glpsol solution file is reported last".
Thehighs.opttemplate carried GLPK-era solution-file options
(write_solution_to_file,solution_file,write_solution_style=2);
nothing has read that file since glpsol was retired. They are removed
from the template, and the same options in an existing project's
solver_config/highs.optare ignored (silently for the old template
lines, with a one-time warning for hand-edited values). To have HiGHS
write a solution file anyway, set these options in the solve's
solver_arguments. -
Dependency versions are capped at the tested series:
polars<3,
pandas>=2.2.3,<4,spinedb-api>=0.36.5,<0.37, andhighspy>=1.14.0,<1.15
(was exactly 1.14.0 — HiGHS patch releases are now allowed). A weekly CI
canary tests the newest releases and reports when a cap can be raised.