Skip to content

Releases: irena-flextool/flextool

v4.0.0b23 — commercial solvers handle spaces; opt-in Benders cut compaction

Choose a tag to compare

@jkiviluo jkiviluo released this 03 Jul 10:30

Pre-release. No schema change (v63); dependency floors unchanged (polar-high>=3.4.0).

Solvers — commercial CLIs (Gurobi / CPLEX / Xpress / COPT)

  • Commercial solvers now handle entity names containing spaces (e.g. Battery Farm), matching the whitespace-agnostic in-process HiGHS path. Fixes both the parent failed to read MPS back crash and the subsequent silent-wrong-answer: the MPS is written with generic, whitespace-safe names (emit_names=False) and the solution mapped back by index onto the real names held in memory. No parent-side readModel.
  • CPLEX/Xpress/COPT .sol parsing fixes (COPT objective; Xpress switched to MPS-like SLX + PrtSol), plus real-solve tests for all four solvers.

Decomposition (Benders) — opt-in, OFF by default

  • Periodic master cut compaction (FLEXTOOL_BENDERS_CUT_COMPACT_AT, default OFF) with slack/dominance policy — experimental.
  • Capability guard: needs polar-high>=3.5.0; if set on an older polar-high, compaction disables with a clear warning instead of crashing.

🤖 Generated with Claude Code

v4.0.0b22

v4.0.0b22 Pre-release
Pre-release

Choose a tag to compare

@jkiviluo jkiviluo released this 01 Jul 18:51

Release 4.0.0b22 (1.7.2026) — benders_in_out_weight solve parameter (schema v63)

Database migration v62 → v63 (automatic on load). Dependency floors
unchanged (polar-high>=3.4.0, polars>=1.40, highspy<=1.14.0). Additive and
OFF by default — the converged solution is unchanged on every model.

Decomposition (Benders)

  • The in-out separation weight is now a per-solve database parameter,
    solve.benders_in_out_weight
    (previously only the machine-local environment
    variable FLEXTOOL_BENDERS_IN_OUT_WEIGHT from b21). It is the weight λ in the
    in-out separation point f_sep = λ·centre + (1-λ)·f_out: 0.0 (the default)
    = off
    (exact Benders, byte-identical to before); values in (0, 1) turn the
    stabilisation on, larger = more. Only used when decomposition = 'benders'. The
    environment variable still works and, when set to a valid value, overrides
    the database parameter (machine-local wins), mirroring the worker-count knob;
    an out-of-range or malformed environment value is ignored with a warning and the
    database value is used. The v63 migration adds the parameter to the solve
    class (default 0.0, solve_advanced group) — no authored data changes, and a
    database with the parameter unset behaves exactly as before.

v4.0.0b21

v4.0.0b21 Pre-release
Pre-release

Choose a tag to compare

@jkiviluo jkiviluo released this 01 Jul 17:11

Release 4.0.0b21 (1.7.2026) — Benders in-out stabilization (degeneracy tail-off)

No database migration (schema stays v62). Dependency floor raised to
polar-high>=3.4.0 (polars>=1.40, highspy<=1.14.0 unchanged). Opt-in and
OFF by default — the converged solution is unchanged on every model.

Decomposition (Benders)

  • Optional in-out separation to speed up a slowly-converging (tailing-off)
    spatial-Benders solve.
    When storage (or any inter-temporal coupling) makes
    timesteps fungible, the region recourse is flat in the per-timestep coupling
    flow and the cut slopes become basis-dependent; the master then wanders among
    cost-equivalent flow schedules and the lower bound closes very slowly even
    though the best feasible solution is near-optimal early. In-out separation
    (Ben-Ameur & Neto 2007) generates each cut at an interior point
    f_sep = λ·centre + (1-λ)·f_out instead of the extreme master vertex — better-
    centred cuts, no wandering, faster bound closure, at zero extra subproblem
    solves
    (the region is solved once per iteration either way, just at a better
    point). Enabled per solve via the environment variable
    FLEXTOOL_BENDERS_IN_OUT_WEIGHT (the weight λ): 0.0 (the default) is OFF
    and byte-identical to before; λ ∈ (0, 1) turns it on, larger = more
    stabilisation. On a hydrogen-trade tail-off benchmark, λ ≈ 0.3–0.7 reached the
    practical optimality gaps about 30% faster and closed the bound tighter than the
    un-stabilised run (which plateaued above the gap it could otherwise reach). The
    stabilisation math is the domain-free polar_high.decomposition.InOutStabilizer
    (3.4.0); FlexTool drives one instance per node group. Correctness is preserved
    regardless of λ: a cut generated at any point is a valid supporting hyperplane
    (valid lower bound), the interior point is clamped to the chosen capacity (valid
    upper bound), and the moment a region's cut fails to separate the master vertex
    the method falls back to an exact-Benders step for that region — so the optimum
    is unchanged and convergence is guaranteed. A future release will promote the
    knob to a solve database parameter once a default is settled.

v4.0.0b20

v4.0.0b20 Pre-release
Pre-release

Choose a tag to compare

@jkiviluo jkiviluo released this 01 Jul 11:40

Release 4.0.0b20 (1.7.2026) — Benders stall guard (fail fast with a diagnostic)

No database migration (schema stays v62). Dependency floor raised to
polar-high>=3.3.0 (polars>=1.40, highspy<=1.14.0 unchanged) for the new
generic tail-off detector. Detection only — no converged solution changes.

Decomposition (Benders)

  • A stalled spatial-Benders solve now fails fast with a plain-English
    diagnostic instead of silently exhausting the iteration cap.
    When one node
    group is near-infeasible on its own (it can only meet its demand via the
    boundary/slack penalty), the master keeps proposing coupling flows that force
    the penalty regime, the best feasible cost freezes far above any sane value,
    and the run would otherwise burn all its iterations returning that garbage. The
    solver now detects this and aborts with a three-section message that names the
    worst-offender node group and recommends the fix (give that node group's
    import/boundary nodes a moderate import price rather than an extreme penalty —
    an over-large penalty is what inflates the recourse and freezes the bound). The
    detector is a domain-free tail-off monitor in polar-high 3.3.0
    (decomposition.StallMonitor); FlexTool supplies the reference scale (the sum
    of the node groups' stand-alone costs) and the node-group diagnosis. A run is
    flagged stalled only when the best feasible cost is frozen for a window of
    iterations AND still far above that reference AND the gap is far from the
    tolerance — a conjunction that never trips a slow-but-genuinely-converging
    solve (validated against solves that converge in 8–28 iterations). The window
    defaults to 8 and is overridable with FLEXTOOL_BENDERS_MAX_STALL.

v4.0.0b19

v4.0.0b19 Pre-release
Pre-release

Choose a tag to compare

@jkiviluo jkiviluo released this 01 Jul 08:07

Release 4.0.0b19 (1.7.2026) — retiring-unit existing map read + Benders cut-tolerance fix

No database migration (schema stays v62) and dependency floors unchanged
(polar-high>=3.2.0, polars>=1.40, highspy<=1.14.0). Two independent
fixes: one corrects an output/dispatch parameter for retiring units; the other
removes a spurious hard-failure in the spatial-Benders decomposition solver.

Outputs / dispatch

  • Read a retiring unit's existing capacity by the per-period MAX, not the
    last row.
    _entity_unitsize_lf gated the existing-map cascade input on
    "period" in columns, but Spine names a Map's index column with its
    silent-default "x", so the gate never matched and every period row fell
    through to a unique(keep="last") that kept the last period's value. For a
    unit whose existing map decays to 0 at expiry, that collapsed the cascade
    input to 0, defaulted unitsize to 1000, and turned
    existing_count = existing/unitsize into a spurious fraction — capping
    continuous online at that fraction and making integer online impossible to
    commit. The reader now takes the per-entity MAX over whatever period rows
    exist, index-name-agnostically (CLAUDE.md Invariant #2). Byte-identical for
    scalar existing (a single-row group-by is a no-op) and for every existing
    test fixture; corrects the reported online/count for retiring-unit models.
    Adds regression tests in the silent-default-index coverage file and a dev-doc
    audit of the readers that live outside the _param_shapes resolver.

Decomposition (Benders)

  • The spatial-Benders cut self-check no longer hard-fails on solver
    round-off.
    After each master solve, _check_cuts_satisfied asserts every
    just-appended optimality cut is honoured at the new master point. That check
    re-derives a row already present in the master LP, so it can only differ from
    the solved value by the solver's feasibility tolerance — which HiGHS measures
    on its internally-scaled matrix, making the unscaled slack scale with the
    cut row's coefficient magnitude, not its right-hand side. On early iterations
    a node group whose recourse cost overshoots produces large reduced-cost
    slopes, so cost_r and Σ slope·f̄ nearly cancel: the rhs collapses to O(1)
    while the coefficients stay at O(1e6). The old tolerance keyed off the
    cancelled rhs and demanded a precision the ill-conditioned row cannot deliver,
    aborting an otherwise-converging solve (observed on a 7-node-group hydrogen-
    trade model: a 7e-4 slack on a 2.66e6-scale row — 2.7e-10 relative). The
    tolerance is now keyed off the row magnitude, matching the fail-safe design of
    the sibling flow-clamp / lower-bound / sandwich guards: numerical noise is
    absorbed, a moderate gap is warned, and only a gross violation (a genuinely
    un-appended cut, whose recourse estimate sits near its large-negative floor)
    still hard-fails — now with the same three-section plain-English diagnostic as
    the other guards. The affected model converges to the monolith objective
    within tolerance.

v4.0.0b18 — fix zeroed existing capacity (scalar+Map entity classes)

Choose a tag to compare

@jkiviluo jkiviluo released this 30 Jun 18:24

Release 4.0.0b18 (30.6.2026) — fix zeroed existing capacity for scalar+Map entity classes

No database migration (schema stays v62) and dependency floors unchanged
(polar-high>=3.2.0, polars>=1.40, highspy<=1.14.0). Output-only fix — the
converged solution (dispatch, investments, optimal objective) is unchanged.

Outputs

  • Fix zeroed existing capacity for a unit whose constant value shares an
    entity class with a period-Map sibling.
    When one unit carries a scalar
    (constant) existing capacity while another unit of the same class carries a
    period-indexed Map, the Spine reader returns a single frame with one shared
    index column (named x by Spine's silent default) in which the constant
    unit's row has a null index. The per-entity resolver classified rows by the
    presence of that column rather than per-row, so the null-index scalar became
    an explicit (entity, period=null) row that never joined the period grid and
    was silently filled with 0. Downstream this zeroed the output-facing
    existing/total capacity for such units — surfacing most visibly as negative
    VRE curtailment
    (potential − flow, with potential computed from a 0
    capacity), and as 0 in the capacity, capacity-factor, and per-entity
    pre-existing fixed-cost reports. The resolver now detects scalars per row (a
    null index broadcasts the constant across the period universe), in both mirror
    implementations. The LP itself was never affected — its flow bound reads a
    separate, scalar-correct existing-count parameter, and existing fixed cost
    enters the objective only as an opt-in constant term — so this is purely a
    reporting fix; re-running output processing on an existing solve produces
    correct reports without re-solving.

v4.0.0b17 — autoscale centres the objective

Choose a tag to compare

@jkiviluo jkiviluo released this 30 Jun 10:48

No database migration (schema stays v62). Dependency floor raised to polar-high>=3.2.0. The converged solution is unchanged on every model.

The "Problem has some excessively small costs" warning is fixed at the source

FlexTool multiplies the whole objective by the legacy scale_the_objective = 1e-6, dragging every cost coefficient ~6 decades down — a typical operational-cost band lands around [5e-5, 6e-1], below HiGHS' 1e-4 floor. polar-high 3.2.0's autoscale Layer 3 now geometrically centres the cost band over HiGHS' comfort zone via a power-of-two user_objective_scale: the band straddles 1.0, the warning clears, and the cheapest costs gain several decades of headroom above the dual-feasibility tolerance — which helps duals (node/reserve prices) converge on degenerate dispatch LPs.

The exponent is a power of two and HiGHS unscales the objective and duals on output, so the reported solution is byte-for-byte unchanged — only the magnitudes the simplex pivots on. Bands already in HiGHS' zone are left untouched.

Housekeeping

  • Genericized a handful of test fixtures and source comments; no behavioural change.

v4.0.0b16 — Benders fail-safe numerical guards + clearer failure diagnostics

Choose a tag to compare

@jkiviluo jkiviluo released this 30 Jun 09:24

Release 4.0.0b16 (30.6.2026) — Benders fail-safe numerical guards + clearer failure diagnostics

No database migration (schema stays v62) and dependency floors unchanged
(polar-high>=3.1.0, polars>=1.40, highspy<=1.14.0). Robustness fixes to the
Benders regional-decomposition driver plus a docs note; a model that does not use
regional decomposition is unaffected, and the converged solution is unchanged.

Benders regional decomposition

  • Clamp the master coupling flow to capacity instead of tuning the tolerance.
    The post-master-solve f <= cap self-check kept tripping on benign solver
    slack: HiGHS enforces feasibility on the internally-scaled master, so the
    unscaled slack exceeds both the nominal tolerance and the reported
    max_primal_infeasibility, and it grows as cuts accumulate (~3e-6 at iter 4,
    ~1.2e-5 at iter 8) — a fixed tolerance chases a moving target (this supersedes
    the b14 tolerance-from-feasibility approach). Instead, the upper bound is now
    evaluated at the strictly capacity-feasible point (C, min(f, cap)): clamping
    flow down can only raise region recourse cost, so it stays a valid whole-problem
    UB and the optimality cut stays valid (a subgradient inequality holds at any
    linearization point). The clamped flow is used consistently downstream (region
    pin, next cuts, incumbent); only a gross overshoot (>1e-2 relative, or 1e3×
    the solver's reported infeasibility) still hard-fails as a genuine read/stale
    bug. Clamp magnitude logged at DEBUG.
  • Fail-safe LB guards. The LB-monotonicity (1e-6) and LB <= best_UB
    sandwich (1e-9) self-checks no longer hard-crash the whole run on benign
    numerical noise (discarding a good feasible incumbent). Both now separate noise
    from a genuine invalid-bound bug via a unified gross band
    max(tol, 1e-3): a small LB dip pins LB back to the previous valid bound and
    continues; a small LB/UB crossing is treated as converged (stop on the
    incumbent); a gross dip or overshoot still hard-fails (stale basis / corrupted
    cut / invalid-bound pathology).
  • Plain-English failure diagnostics. Every hard-failure site (master /
    node-group not optimal, LB dip, LB > UB, coupling overshoot) now emits a
    one-line summary plus "What this means" and "How to avoid it", using only
    FlexTool class vocabulary (node, group, connection, flow) — no model-instance
    terms — pinned by a static AST regression test.

Documentation

  • Recommend setting an explicit Map index_name (period or time) for
    parameters that accept both a period map and a time series (e.g. flowGroup
    min_instant_flow / max_instant_flow), so the value-domain fallback never has
    to guess; cross-linked from the flowGroup flow-limit reference.

v4.0.0b15 — flowGroup instant-flow obligation fix

Choose a tag to compare

@jkiviluo jkiviluo released this 30 Jun 07:43

Fixes a flowGroup min_instant_flow / max_instant_flow limit being silently ignored — or crashing at load — depending on how the limit was authored. No database migration (schema stays v62) and dependency floors are unchanged (polar-high>=3.1.0, polars>=1.40, highspy<=1.14.0). Models that set no instant-flow limit on a flowGroup are unaffected.

flowGroup flow limits

  • Emit the instant-flow constraint for every authored shape. The min_instant_flow / max_instant_flow obligation's RHS cap was resolved correctly for all shapes (constant, period map, time map, period+time — including Spine's silent-default "x" Map index_name), but the constraint's (g, d, t) support was built by a separate raw-source projection that detected the index axis by column name. That projection returned an empty support for an "x"-labelled period map, a constant, or a time map — so the >= / <= constraint was never emitted and the limit was silently ignored — and it crashed (ColumnNotFoundError: "t") on a pure period map. The support is now derived directly from the resolved cap and broadcast against the active (d, t) grid, exactly as the cumulative-flow limits already are, so every authoring shape binds correctly. A regression test covers each cap shape plus the reported field case (an "x"-indexed period map with a zero later period under a single-period solve).

v4.0.0b14 — Benders master coupling self-check tolerance fix

Choose a tag to compare

@jkiviluo jkiviluo released this 29 Jun 18:43

Release 4.0.0b14 (29.6.2026) — Benders master coupling self-check tolerance fix

Raises the solver-backend floor to polar-high>=3.1.0 (the Benders master
coupling self-check now reads Solution.max_primal_infeasibility, added in
3.1.0). No database migration (schema stays v62); other floors unchanged
(polars>=1.40, highspy<=1.14.0). A model that does not use regional
(Benders) decomposition is unaffected.

Benders regional decomposition

  • Size the master coupling self-check from the solver's achieved feasibility.
    The post-master-solve self-check that the chosen flow is supported by the
    chosen capacity (f <= cap) used a hard-coded 1e-6 absolute tolerance. For a
    unitsize-normalised coupling row C - f >= 0 with small capacity, HiGHS
    enforces primal feasibility on the internally-scaled row (default 1e-7),
    which maps to a larger unscaled slack on f <= cap — a normal solver
    artifact, not an invalid bound. On a full-scale model this tripped at iteration
    4 (slack ~3e-6 > tol 1e-6) and aborted an otherwise-valid solve. The
    tolerance is now derived from the master solve's own
    Solution.max_primal_infeasibility (with a small relative floor), and the
    error message reports the actual slack vs. tolerance so a genuine,
    orders-of-magnitude coupling violation still surfaces loudly.