Repository navigation
Indirect-unit capacity, input shares, per-unit min load and reserve fixes - #346
Merged
Merged
Conversation
Collaborator
Author
|
It seems that the discrepancy on what the capacity of unit with multiple inputs means is in the tests as well. "Currently the documentation claims that the capacity limits the sum of outflows. |
…ct output capacity_max_coeff The loose bound on an indirect unit's input (fuel) arc is a solver-region bound only, but it was slope * capacity -- enough fuel for one output at full capacity -- so multi-output units (CHP) and outputs with conversion_flow_coeff > 1 were silently capped below their capacity. Derive it from conversion_indirect instead: cap * slope * sum_k(sink_conv_k * capacity_max_coeff_k) / src_conv, scaled by 1/max(delay weight) for delayed units and unconstrained when an output is uncapped (capacity_max_coeff = 0). This replaces the PR's sum-of-capacity-coefficients factor, which ignored conversion_flow_coeff, and drops the division by the input's capacity_max_coeff (backwards: a coefficient > 1 tightened the bound). Removing the fuel cap exposed a second bug: indirect OUTPUT arcs are bound by the existing-only maxToSink RHS, but p_arc_max_cap_coef only carried capacity_max_coeff for direct arcs, so indirect outputs ignored it. Include indirect output arcs (process_output_flows) in the producer. Regenerated goldens for the six affected scenarios (coal_chp, coal_chp_extraction, aggregate_outputs_network_coal_wind_chp, test_a_lot, test_a_lot_but_not_multi_year, 2_day_stochastic_dispatch; the last is a degenerate alternative optimum at identical objective). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
Member
|
working on this - more complicated than it seemed at first. |
Unit capacity is documented as "the maximum sum of output flows", but the
engine only capped each arc (maxFlow); for indirect units the sum was
enforced only accidentally by the fuel arc's loose bound in dispatch
solves, and not at all in investment solves.
New constraint per (p, d, t), p in process_indirect with output arcs
(process_output_flows; conversion_flow_coeff = 0 outputs excluded):
sum_k v_flow[p,p,k] + sum_k reserve_up[p,k] - invest + divest
<= existing/unitsize * availability
reusing maxFlow's invest/divest (p, d) terms once per row, RHS densified
(greenfield 0, availability default 1). capacity_max_coeff stays a
per-arc cap. Neg-cap (p, d) rows get only the flipped >= twin
(maxOutputSum_negCap). Online units additionally get
maxOutputSum_online_<kind>: sum_k v_flow <= v_online * availability.
Registered in autoscale CONSTRAINT_FAMILIES; dynamic names pinned.
Goldens NOT regenerated: coal_chp, coal_chp_extraction,
aggregate_outputs_network_coal_wind_chp, test_a_lot,
test_a_lot_but_not_multi_year, 2_day_stochastic_dispatch differ.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…=0 is a zero cap * maxFlow / maxFlow_negCap: capped arcs (direct + indirect outputs) now read v_flow - M*Σinvest + M*Σdivest <= M*existing/us, M = capacity_max_coeff x availability per (p,source,sink,d,t) — the coefficient and availability used to multiply only the existing part. RHS construction moved to _max_flow_rhs (capped / loose / uncapped arc classes); loose indirect input rows unchanged. * maxFlow_back (2-way 1-var reverse) now carries the invest/divest terms. * maxOutputSum: invest/divest scaled by availability. * maxFlow_online: capacity_max_coeff applied; restricted to capped arcs (no longer caps indirect fuel inputs at v_online). * ramp_*_constraint: RHS = ramp*60*step*coef*(existing + Σinv - Σdiv); the side's capacity_max_coeff and invested units were missing. * coeff_zero / uncapped edges re-keyed from capacity_max_coeff == 0 to conversion_flow_coeff == 0 (rename artefact of v35): native _process_source_sink_coeff_zero_lf and the CSV path derive_coeff_zero. New FlexData.process_source_sink_uncapped; such edges get the raw p_flow_upper (max_flow_for_unconstrained_variables) in maxFlow and are dropped from maxFlow_online, ramp and minFlow_minload. p_flow_upper_from_source: _out_uncapped special case removed. * invest_retire_no_limit recognised as a no-limit method (INVEST_METHODS_INVEST_NO_LIMIT) in p_entity_max_units and the CSV chain params. Root cause of "no investment": max_units stayed empty -> maxInvest_var_bound pinned v_invest <= 0. * tests: test_capacity_coefficients.py (14 cases); obsolete capacity_max_coeff=0-is-uncapped test removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
… descriptions Description-only migration: uncapped edges are conversion_flow_coeff = 0, capacity_max_coeff = 0 is a zero cap and scales existing + invested - retired capacity. Regenerated master template, canonical databases and test fixtures; CHANGELOG entries for the capacity-coefficient fixes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…/min Unit capacity is the maximum SUM of outputs; on an input the coefficient is the largest share of the unit's full-load fuel that the input may supply. Rename the input-side parameters accordingly: unit__inputNode.capacity_max_coeff -> input_share_max unit__inputNode.capacity_min_coeff -> input_share_min unit__outputNode keeps capacity_max_coeff / capacity_min_coeff. * db_migration v71: renames the two definitions (values follow by id, unchanged), new descriptions; idempotent. * Engine readers (_derived_params._arc_max_capacity_coef_lf, the _direct_params source-side reader, input_derivation CSV specs and the CSV-chain p_flow_max reader) read the new names; PARAMETER_TYPES carries old + new names for the migration window. * Regenerated master template, canonical databases and test fixtures. The old-FlexTool importer stays on its frozen v56 template (migration carries it forward). * tests/test_v71_migration.py: values move, output side untouched, idempotent re-run. No change in model behaviour in this commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
The branch carries exactly one schema step: the unit__inputNode rename (capacity_max_coeff -> input_share_max, capacity_min_coeff -> input_share_min, values moved, new descriptions) now lives in the v70 migration next to the description updates, and FLEXTOOL_DB_VERSION stays 70. The rename is idempotent: a definition is renamed only when the old one exists and the new one does not; otherwise only the description is refreshed. Master template, canonical databases and test fixtures regenerated from their v69 state with the documented commands; all verify commands pass. tests/test_v71_migration.py -> tests/test_v70_migration.py (v69 seed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
Unit capacity is the maximum SUM of outputs (maxOutputSum). Input arcs of indirect units now carry in_s <= input_share_max_s * L_s * availability * capacity L_s = (slope*W + section) * delay_factor / src_div_s W = most output-side fuel energy per unit of capacity under sum<=1 and out_k <= capacity_max_coeff_k (greedy by descending conversion_flow_coeff; replaces fuel_width = sum conv_k*capcoef_k, which ignored the sum cap). section only for min_load_efficiency units; delay_factor / src_div as before (min(src_conv,1) for delayed units, src_conv<=0 -> 1). Inputs with conversion_flow_coeff = 0 get no limit (keep the unconstrained safeguard). * New FlexData.p_indirect_input_cap (p,source,sink,d,t) = share * L (_derived_params.p_indirect_input_cap_from_source); p_flow_upper's indirect input branch = cap * capacity count. * model._max_flow_rhs: input arcs of units WITHOUT invest/divest vars in the solve -> per-element v_flow upper bound (polar-high Param bound), no maxFlow row; units WITH them keep the row v_flow - M*sum(invest) + M*sum(divest) <= M*existing/us, M = cap * availability (the invest term used multiplier 1). maxFlow_negCap still covers neg-capacity rows of every arc. * Direct units: an AUTHORED input_share_max caps the single flow (arc factor min(capacity_max_coeff_out, share)); sink-less units keep input_share_max as their capacity fraction (unchanged numbers). * Source-side ramp: indirect input arcs scale with p_indirect_input_cap, other arcs with input_share_max. * Synthetic sub-solves (rolling rolls, nested per-period solves) skipped apply_derived_b/d: wire_arc_capacity_params now sets p_arc_max_cap_coef, process_source_sink_uncapped and p_indirect_input_cap there too (rolls previously ignored output capacity_max_coeff and the conversion=0 uncapped edges). CSV-chain derive_p_flow_max mirrors the native formula. * Warm: new per-arc params are force-cold on any diff (bounds are not updated by WarmProblem). Region filter filters the new param. * Autoscale Layer 2: per-element bounds feed the bound statistics and are rescaled via Var.scale_bounds (scalar path byte-identical). * Tests: test_input_share.py (W greedy, single fuel conv 1.5/0.5, two fuels 0.6, share 0.3 bound path + col bounds, share 0.3 invest row path, multi-period invest_period, direct unit, rolling), updated test_indirect_fuel_arc_bound / test_emission_maxFlow_indirect. Requires polar-high per-element variable bounds (feat/var-param-bounds). Goldens NOT regenerated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…rve floors Minimum load applies once per unit, on the sum of its output measure (the conversion_flow_coeff != 0 outputs of an indirect unit, the single arc of a direct unit): minFlow_minload_<kind> is one row per (p, d, t) with no coefficient. Input arcs of indirect units lost their per-input floors, which forced an expensive second input to burn, kept a unit offline when an input could not supply, stopped heat-pump-like units (efficiency > 1) from running and forced electricity out of an extraction CHP in pure-heat mode. unit__outputNode.capacity_min_coeff becomes an optional per-output floor of multi-output units (minFlow_output_floor_<kind>); it no longer scales the unit floor. unit__inputNode.input_share_min: homogeneous mixing row minInputShare (conv_s * in_s >= share_s * sum conv * in) for indirect units with >= 2 inputs; applies with or without online variables. Downward reserve (and upward reserve at a unit's input node) can no longer take a unit below its floor: reserve terms on minFlow_minload / minFlow_output_floor, plus minFlow_reserve (per unit) and minFlow_reserve_arc (per arc) zero floors. Source-side reserve of a direct unit is converted by 1/slope (reserveBalance V1 convention; tripwire test). Connections are excluded. Validators (once per scenario, after derive_process_method) for input shares and output floors; build-time guards in the row builders. New params listed as warm-start deferred; new constraint families registered for autoscale. polar-high >= 3.8.0 (per-element variable bounds). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
vq_reserve (r, ud, g, d, t; bounded 0..1) was multiplied by the group's reservation in the reserve balances and the penalty. A dynamic / n-1 group of an active reserve without its own reservation had a zero (or 1 MW) shortfall and no penalty: a hard requirement the new downward-reserve floors could make infeasible. reserve_shortfall_scale(d) is now the multiplier everywhere (all balance rows, objective penalty, output reconstruction in calc_slacks): reservation (timeseries; the same Param when no dynamic / n-1 group exists, so timeseries LPs are byte-identical) + sum p_flow_upper * unitsize * increase_reserve_ratio (dynamic) + sum p_flow_upper * unitsize * large_failure_ratio (n-1, the same sum over failing processes as the V1 RHS). p_flow_upper carries the max reachable capacity incl. investment; unlimited investment uses max_flow_for_unconstrained_variables. Densified over reserve__upDown__group x dt with left joins. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
The (single, version-agnostic) input-share step now also: * renames unit__inputNode.capacity_min_coeff -> input_share_min with default 0 and a new description, deleting every old value (listed); * sets unit__outputNode.capacity_min_coeff default 1 -> 0 (per-output floor), deletes values outside [0, 1] (incl. maps with a bad element) and the v36 backfill leftovers (capacity_min_coeff == capacity_max_coeff != 1), listing them, and lists units whose kept values change meaning; * drops the word "fuel" from the input_share_max description. Idempotent: deletions and notices run only in the pre-step (rename) case. Generated files restored to their v69 state from main and regenerated with sync_master_json_template, test_fixtures migrate-all, canonical_databases migrate-all and generate_canonical; all verify commands pass. The stored input_share_min 0.0 in howto_hydro_reservoir_with_pump is deleted by the step. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
reference.md (min_load, capacity_min_coeff, input_share_min/max, the coefficient table, reserve provision and shortfall), how_to.md (extraction CHP modes, min-load text), CHANGELOG Unreleased (behaviour changes, migration deletions, reserve fixes, known limitations) and a docs/dev note with the formulation and follow-ups. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
Indirect units with conversion_method = min_load_efficiency burnt slope x output only; the section x online x unitsize term of the efficiency curve (flextool.mod conversion_indirect) was missing, so fuel, commodity cost and CO2 were understated (coal_chp at full load: 611.1 MW instead of 888.9 MW). Add the term (linear and integer online) to the right-hand side of conversion_indirect. That is the only place it belongs: an indirect unit's input is its own v_flow, so node balance, commodity cost, CO2 and reported inputs follow. For delayed units the section sits at the sink-side time with that time's online count. test_sum_cap_online_variant now asserts the full-load fuel 800 / 0.9. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…nits process_delayed__duration came from a map-only direct-params reader: a constant `delay` produced no rows, the unit's inputs were anti-joined out of the undelayed conversion and never put back, so its output was forced to zero with no message. In rolling (synthetic-solve) sub-solves the shift table and weights came from text CSV seeds while the durations were Float64, and the build crashed on the td join. Build dtt__delay_duration, p_process_delay_weight and process_delayed__duration together from one read of unit/connection `delay` (apply_delay_params), on the full and the synthetic path; the CSV-seed loader normalises td to Float64 and derives the durations from the weight keys. _delay.check_delay_tables raises DelayDataError when a delayed indirect unit lacks delayed arcs, durations, shift rows or weights, or when the td dtypes differ. Docs: unit `delay` is a constant or a map of delays (schema float|map), not a period value. Delayed connections ignore their delay (not in process_indirect; same gap as the .mod) — documented in the dev notes, unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…oeff The input-arc limit L_s used src_div = min(src_conv, 1) for delayed units, on the stale belief that the delayed conversion term ignores the input conversion_flow_coeff. It does apply it (_delay.delayed_input_expr), so with conv 2 and input_share_max 0.6 a delayed unit ran at full output (west 1000) instead of 600. Divide by src_conv for delayed inputs too, in both producers (_derived_params._indirect_input_cap_lf and the CSV-chain p_flow_max in _emit_arc_unions), and fix the stale model.py comment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…of-outputs cap; degenerate alt optimum) Other scenarios touched by earlier golden regenerations on this branch are restored to main's versions (values unchanged vs main). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
… + engine) A reserve group without an authored penalty_reserve had no shortfall penalty: the loader read explicit values only and add_objective_terms returned None, so vq_reserve was free and the requirement optional. Root cause: the GMPL model defaulted it to 5000 (reserveParam_defaults) and v18 set the same schema default, but the polars port read parameter_explicit only; the v56 wrong-default cleanup then cleared the schema default to match the engine instead of fixing the engine. - engine: PENALTY_RESERVE_DEFAULT = 5000; the loader densifies the penalty over every reserve__upDown__group entity (left join, default fill); _reserve.reserve_penalty densifies over reserve_upDown_group for the objective and the output reconstruction (read_parameters), so a group with a requirement but no authored penalty is priced at 5000. - schema: the v70 step restores the default (idempotent) with a clearer description; master template, canonical DBs and fixtures regenerated. - tests: default applied, authored value wins, densification; v70 migration test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…ription) docs/reference.md claimed connection `delay` works with transfer_method = no_losses_no_variable_cost. No connection delays its flow: the delayed input term exists only in conversion_indirect, which connections never enter. Probe (network chain, west_east, delay 3 h): for unidirectional, regular and no_losses_no_variable_cost the flow is not time-shifted, but the connection moves from the efficiency arcs to noEff (losses dropped; regular also becomes one-way), lowering the objective. The reference, the how-to and the v70 description of connection.delay now say it is not applied and should be left empty. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
docs/dev/unit_floors_and_reserves.md follow-ups (existing order kept): connection delay item now records that the delay drops the connection's losses; new items for dynamic / n-1 reserves activating only via a sibling non-zero reservation, and for the n-1 requirement summing all failing processes (the .mod had one constraint per failing process; the shortfall scale must become a max when split). CHANGELOG known limitations updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…c delay pass The synthetic-solve path now rebuilds the delay tables (apply_delay_params, 8902930), which reads unit/connection `delay` via InputSource.parameter. The A.20 _StubReader only implemented entities(), so test_synthetic_solve_skips_per_solve_overrides failed with AttributeError. The engine call is correct: skipping it would reintroduce the rolling delayed-unit td-join crash, and parameter() is a required InputSource protocol method. The stub now implements it (unknown class -> KeyError, like the real readers), and the test records apply_delay_params and asserts it runs on the synthetic path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…he connection Connection delay is not implemented (the delayed input term exists only in conversion_indirect, which connections never enter), but a connection `delay` still marked the connection delayed: it became fork_yes in the process-method classifier, its arcs moved from process_source_sink_eff to _noEff (efficiency losses dropped) and a `regular` connection became one-way (network chain, west_east, 3 h: objective 585.7M -> 507.5M). Every reader of `delay` now reads unit.delay only: the input_derivation delay tables (_specs) and process-method classifier, and the engine's _classify_process_method, _delay_distributions_from_source (delay weights, shift table, durations) and _projection_params.process_delayed. A connection with a delay is modelled exactly as without it, and _validators.validate_connection_delay warns once per connection with a non-zero delay. Unit delays are unchanged. Test: test_connection_delay_ignored.py (regular, unidirectional, no_losses_no_variable_cost constant 3 h, regular map) - same objective, flows and arc sets as without the delay, no delay-set rows, one warning. test_unit_delay::test_delayed_connection_builds now pins that the connection gets no delay tables. Reference, how-to, dev notes, CHANGELOG and the connection.delay description (v70 step, schema, canonical DBs, fixtures) say the delay is ignored with a warning. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
…s-join order) Warm rolling reuse pushes the new p_inflow into nodeBalance_eq as a positional vector built from compute_nodeBalance_dt. The constraint rows are in `over` sorted by its columns (add_cstr determinism wrapper), and the vector silently relied on the lazy cross join nodeBalance x dt coming out left-major. polars 2.0 no longer guarantees that: with a small left side it emits right-major order ((heat,t1),(west,t1),...), so every other (n, t) inflow landed on the wrong node. In test_delayed_input_share_rolling_csv_chain roll 1/2 (warm-reused) west demand became 0 in every other hour and the unit stopped there (objective 1.007e9 vs the correct 1.577e9) - CI-only, polars 2.0.0. - _warm: sort new_over by its columns before building the vector and keep the left join's order explicitly (maintain_order="left"). - _pdt_join: all six lazy cross-join helpers go through _cross(...) with maintain_order="left_right", restoring left-major (sorted) output on polars 2 - also keeps the determinism wrapper's already-sorted fast path for the big (entity, d, t) grids. - test_warm_rhs_alignment: both fail on the old code under polars 2. Works on polars 1.40 and 2.0 (maintain_order exists in both). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
CHANGELOG: this branch's Unreleased section kept above the 4.0.5 release section. pyproject version 4.0.5; polar-high>=3.8.0 kept. The warm-start RHS-order fix (_warm.py / _pdt_join.py) was already on this branch (3fb9988) and merged identically. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
- polars>=1.40,<3 and pandas>=2.2.3,<4 (next major capped; the pandas floor is spinedb-api's own requirement). - spinedb-api>=0.36.5,<0.37: 0.x whose minors break the API (0.33, 0.34, 0.36); 0.36.5 added the parameter_group table FlexTool uses. - highspy>=1.14.0,<1.15 (was exactly 1.14.0): patch releases allowed. env_check's older-Windows heal (pip install highspy==1.13.1) is unaffected - pip only warns about the conflicting requirement (verified). - polar-high stays uncapped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
.github/workflows/canary.yml (weekly + manual) lifts the pyproject caps via .github/scripts/dependency_caps.py, installs the newest versions (eager upgrade), reports installed versions against the caps, runs the ci.yml full-suite pytest command, and opens/updates a single "canary: dependency drift" issue on test failure or cap drift; a clean run closes it. CONTRIBUTING documents the caps policy and the canary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
highs.opt.template carried GLPK-era write_solution_to_file / solution_file=flextool.sol / write_solution_style=2, so every solve wrote a file nothing reads and HiGHS warned "The cost row for the Glpsol solution file is reported last". Remove the lines from the template and tests/highs.opt, and drop these keys from the highs.opt layer of the effective-options resolver so projects seeded from the old template stop writing it too (silently for the exact old lines; one-time warning for hand-edited values). A solution file requested via solver_arguments/CLI is still honoured, so _restore_space_mangled_names stays. The opt-in native parity sweep used flextool.sol as its "reference exists" marker; it now checks output_raw/v_obj parquets. Tests: legacy highs.opt solve writes no .sol and no Glpsol warning; explicitly requested solution file is written and a unit named "coal chp" keeps its space in v_flow outputs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q
jkiviluo
added a commit
that referenced
this pull request
Oct 12, 2026
Integrate main's indirect-unit capacity / input shares / per-unit min load / reserve fixes (#346) and the 4.0.5 release into feat/stochastics-reserve-v74. Schema version collision resolved: base was v69; main added one migration (v70); this branch added six (to v75). Both defined a different v70. Kept main's v70 (capacity-coefficient / input-share migration) at slot 70 and RENUMBERED the branch's six migrations to 71-76 (FLEXTOOL_DB_VERSION=76); the migrations are disjoint and order-insensitive. Renamed the six migration test files test_v70..v75 -> test_v71..v76. Canonical DBs + fixtures regenerated to a consistent v76 (sync_master_json_template / test_fixtures / canonical_databases / generate_canonical); the stochastics 2day retag survived migrate-all. Engine files model.py (+1543 from main), _derived_params, read_parameters, drop_levels auto-merged. Three genuine code conflicts resolved deliberately: - p_min_load: composed structurally — main's per-unit min-load semantics live in the minFlow_minload constraint (sum over a unit's outputs), our (d,t) map-reading capability is the producer shape; a (p,d,t) p_min_load flows through the per-unit constraint unchanged (both sides' tests pass). - calc_slacks reserve-slack penalty: combined main's shortfall_scale/reindex with our Option-A pdt_branch_weight; shortfall_scale subsumes the old reservation factor (single-apply, no double-count), matching the LP. - input.py synthetic-solve wiring: additive union (MINLOAD-1 + DELAY-1 + CAPCOEF-1). CHANGELOG: both release-note sets. Full suite green (4145 passed). Merge performed on scratch branch integrate/main-4.0.5-stochastics; feat/stochastics-reserve-v74 untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZpCPYcqig42z8JQW3Qj5c
This was referenced Oct 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Started by @ArttuTupala's observation that a multi-output indirect unit (e.g. a CHP) was limited by its fuel-input bound in dispatch-only solves but not in investment solves. Root-causing that uncovered a set of related capacity, min-load and reserve issues. They are fixed here together, plus a few things found along the way.
Capacity of indirect (multi-output) units
maxOutputSumconstraint: unit capacity (existing + invested − retired) caps the sum of outputs, as the docs define it. Previously this was enforced only by accident, through the fuel-input bound, and only in dispatch-only solves.coal_chp: 528 → 480 rows).capacity_max_coeffand availability now apply to invested capacity in every capacity-style constraint (maxFlow, reverse flow, ramps, maxOutputSum). Before, they scaled only existing capacity: an output with coefficient 0.2 on a unit investing 900 MW could deliver 900 MW. This also fixes reverse flow on newly built connections and ramping of newly built units.capacity_max_coeff = 0means a zero cap.conversion_flow_coeff = 0means an uncapped edge (the hydro pass-through pattern).Input shares (schema change)
unit__inputNode,capacity_max_coeff/capacity_min_coeffare renamed toinput_share_max/input_share_min.input_share_max(default 1): the largest share of the unit's full-load input this input may supply. A poorer heating value is handled byconversion_flow_coeff.input_share_min(default 0): while the unit runs, this input supplies at least this share of the unit's input.unit__outputNodekeepscapacity_max_coeff. Itscapacity_min_coeffbecomes an optional extra per-output floor (default 0).Minimum load and reserves
vq_reserve) keeps its 0–1 scaling, but its multiplier is now the largest possible requirement of the group. Before, dynamic and n-1 groups without areservationcould fall short by at most 1 MW, and that 1 MW was free.penalty_reservedefaults to 5000 again. The default was lost in the v56 cleanup, which left reserve requirements without an authored penalty effectively optional.Other bug fixes
min_load_efficiencyunits were missing the section term, so their input, cost and CO2 were about 31 % too low at full load.delayvalue silently stopped the unit. Rolling solves with any delayed unit crashed.input_share_maxwas not enforced on delayed inputs withconversion_flow_coeff> 1.delayon a connection silently changed the connection (it lost its losses, and a regular connection became one-way). Connection delay is not implemented, so it is now ignored with a warning.invest_retire_no_limitwas not recognised as "no limit", so the unit could not invest.capacity_max_coeffand uncapped edges.mainhere.Housekeeping
polars>=1.40,<3,pandas>=2.2.3,<4,spinedb-api>=0.36.5,<0.37,highspy>=1.14.0,<1.15(allows patch releases)..github/workflows/canary.yml): installs the newest versions with the caps lifted, runs the tests, and keeps a "canary: dependency drift" issue updated.flextool.sol/ glpsol warning: the GLPK-era solution file is no longer written. Old projecthighs.optlines are ignored.Schema / migration
input_share_*rename;capacity_min_coeffvalues;penalty_reservedefault.Results
coal_chp_extraction(electricity can now reach its 800 MW cap; before it was stuck at 470) andtest_a_lot(one hour differs at an identical objective). All other scenarios are unchanged.Follow-ups (not in this PR)
Listed in
docs/dev/unit_floors_and_reserves.md:🤖 Generated with Claude Code
https://claude.ai/code/session_01Cai1ke9MzbEMpY1jfScB6q