Skip to content

Configuration

All runtime parameters for cobre run are controlled by config.json in the case directory. This page documents every section and field.


{
"training": {
"selection": { "method": "sampled", "forward_passes": 50 },
"stopping_rules": [{ "type": "iteration_limit", "limit": 100 }]
}
}

All other sections are optional with defaults documented below.


Controls the SDDP training phase.

FieldTypeDescription
selectionobjectScenario-selection method for the training forward pass — sampled{forward_passes} or enumerated{} (see training.selection below). No default: a missing training.selection is a hard load error.
stopping_rulesarrayAt least one stopping rule (see below). The rule set must contain at least one iteration_limit rule.

Chooses how many trajectories the training forward pass runs per iteration, or whether it exhaustively enumerates the scenario openings instead of sampling. The object is internally tagged on method; each variant accepts only its own fields — pairing forward_passes with "enumerated" is a load-time error (deny_unknown_fields).

FieldTypeRequiredDescription
methodstringYes"sampled" or "enumerated"
forward_passesintegersampled onlyNumber of scenario trajectories per iteration. Larger values reduce variance in each iteration’s cut but increase cost per iteration. Rejected under "enumerated".

There is no default forward-pass count: an absent training.selection is a hard load error (LoadError::SchemaError, naming the field training.selection, message “a forward-pass count is required via training.selection”). The removed root-level training.forward_passes field from pre-v0.14.0 releases is rejected as an unknown field (deny_unknown_fields) rather than silently accepted.

Example — sampled:

{ "method": "sampled", "forward_passes": 50 }

Example — enumerated (the forward pass exhaustively walks the scenario openings instead of drawing a fixed count):

{ "method": "enumerated" }
FieldTypeDefaultDescription
enabledbooleantrueSet to false to skip training and proceed directly to simulation (requires a pre-trained policy).
tree_seedintegernullRandom seed for the opening scenario tree. When null, a default seed of 42 is used (deterministic but arbitrary). See Stochastic Modeling for the dual-seed architecture.
stopping_mode"any" or "all""any"How multiple stopping rules combine: "any" stops when the first rule is satisfied; "all" requires all rules to be satisfied simultaneously.

For the per-class scenario_source configuration, see the scenario_source sub-section below and Stochastic Modeling.

Controls where the forward-pass noise comes from for each entity class during training. When absent, all classes default to in_sample (reusing the pre-generated opening tree).

FieldTypeDefaultDescription
seedinteger or nullnullShared forward-pass seed for out_of_sample, historical, and external schemes.
inflowobjectin_sampleSampling scheme for hydro inflow. Object with "scheme" key.
loadobjectin_sampleSampling scheme for bus load. Object with "scheme" key.
ncsobjectin_sampleSampling scheme for NCS availability. Object with "scheme" key.
historical_yearsarray or objectauto-discoverRestrict the pool of historical windows. List ([1940, 1953]) or range ({"from": 1940, "to": 2010}).

Valid values for "scheme": "in_sample", "out_of_sample", "historical", "external".

Under the "external" scheme a class’s mean and standard deviation are derived from its external scenario file itself, so that class’s seasonal-statistics input (scenarios/inflow_seasonal_stats.parquet, load_seasonal_stats.parquet, or non_controllable_stats.parquet) is optional — it is not consulted when the class is external everywhere. A constant (σ = 0) external column is accepted for load, NCS, and an order-0 inflow model; for an autoregressive inflow model of order > 0 it is rejected (see Error Codes).

Example — out-of-sample inflow with in-sample load and NCS:

{
"training": {
"tree_seed": 42,
"selection": { "method": "sampled", "forward_passes": 50 },
"stopping_rules": [{ "type": "iteration_limit", "limit": 200 }],
"scenario_source": {
"seed": 99,
"inflow": { "scheme": "out_of_sample" },
"load": { "scheme": "in_sample" },
"ncs": { "scheme": "in_sample" }
}
}
}

See Stochastic Modeling — Sampling Schemes for a full description of each scheme and the historical_years field.

Each entry in stopping_rules is a JSON object with a "type" discriminator.

Stop after a fixed number of training iterations.

{ "type": "iteration_limit", "limit": 200 }
FieldTypeDescription
limitintegerMaximum number of SDDP iterations to run.

Stop after a wall-clock time budget is exhausted.

{ "type": "time_limit", "seconds": 3600.0 }
FieldTypeDescription
secondsfloatMaximum training time in seconds.

Stop when the relative improvement in the lower bound falls below a threshold.

{ "type": "bound_stalling", "iterations": 20, "tolerance": 0.0001 }
FieldTypeDescription
iterationsintegerWindow size: the number of past iterations over which to compute the relative improvement.
tolerancefloatRelative improvement threshold. Training stops when the improvement over the window is below this value.

Stop when the exact upper bound has closed to within tolerance of the lower bound. Requires an enumerated training forward pass, so the upper bound being compared is exact rather than a statistical estimate.

{ "type": "gap", "tolerance": 1000.0, "relative_tolerance": 0.01 }
FieldTypeDescription
tolerancefloatOptional. Absolute gap tolerance, in canonical R$.
relative_tolerancefloatOptional. Relative gap tolerance in percent0.01 means 0.01%, compared against 100 · gap / max(1, |lower_bound|).

At least one of tolerance / relative_tolerance must be present; a gap rule with neither field is a load-time error: “gap stopping rule requires at least one of tolerance / relative_tolerance to be present”. When both are present, the two arms combine by disjunction — training stops as soon as either arm is satisfied, not both. The gap itself is max(0, upper_bound − lower_bound).

Admissibility — the gap rule is accepted only when both hold:

  • training.selection.method is "enumerated" (the forward pass exhaustively enumerates the scenario tree, so the upper bound is exact rather than a sampled estimate).
  • The risk measure is uniform across every stage: either risk_measure: "expectation" at every stage, or the same cvar measure (identical lambda and alpha) at every stage in stages.json (a cvar with lambda: 0 is expectation-equivalent). Under a uniform cvar the exact upper bound is computed as a nested, time-consistent risk recursion over the enumerated tree, so it still brackets the risk-averse lower bound.

Under sampled selection, or a stage-varying measure (the risk measure differs across stages), the gap rule is rejected at load with a validation error — the exact-bound comparison it depends on is unavailable in those cases.

When multiple stopping rules are listed, stopping_mode controls how they combine:

  • "any" (default): stop when any one rule is satisfied.
  • "all": stop only when every rule is satisfied simultaneously.
{
"training": {
"selection": { "method": "sampled", "forward_passes": 50 },
"stopping_mode": "all",
"stopping_rules": [
{ "type": "iteration_limit", "limit": 500 },
{ "type": "bound_stalling", "iterations": 20, "tolerance": 0.0001 }
]
}
}

Groups the result-preserving worker-scheduling knobs that shape how backward-pass work is distributed across workers, alongside the algorithm-semantics fields at the training root. This is a pure addition — no existing training field moved.

backward_scheduler selects how backward-pass work units are claimed by workers. It is tagged on method; supplying a field that does not belong to the selected method is a load-time error (deny_unknown_fields), not a silently ignored key.

  • "by_scenario" (default) — each parallel work unit is one whole trial point, matching the byte-neutral pre-v0.12.0 behavior.

    { "method": "by_scenario" }
  • "by_node" — each parallel work unit is one (trial point, opening block) pair, claimed dynamically off a shared counter.

    FieldTypeDefaultDescription
    block_sizeinteger or nullnullOpenings per block. Absent resolves per stage to ⌈|Ω_s|/2⌉ (half the openings, rounded up); a set value is silently clamped to min(|Ω_s|, block_size) — no error, no warning.

    Two configurations are load-time errors, not silently accepted:

    • block_size supplied under "by_scenario" — rejected (deny_unknown_fields).
    • block_size: 0 — rejected (minimum 1).

Within a stage, opening-blocks are claimed hardest-first: blocks are ranked by the previous iteration’s mean simplex-pivot cost per (stage, block), descending (ties broken by ascending block index; the first iteration, and any block with no prior data, sort last). This claim order changes only which worker processes which block, and when — it is result-neutral: the produced cut set and the training lower bound are identical to canonical-order claiming.

Example — default (by_scenario; no parallelism block needed):

{
"training": {
"selection": { "method": "sampled", "forward_passes": 50 },
"stopping_rules": [{ "type": "iteration_limit", "limit": 100 }]
}
}

Example — by_node with an explicit block_size:

{
"training": {
"selection": { "method": "sampled", "forward_passes": 50 },
"stopping_rules": [{ "type": "iteration_limit", "limit": 100 }],
"parallelism": {
"backward_scheduler": { "method": "by_node", "block_size": 4 }
}
}
}

For the backward pass this schedules work within, see SDDP Algorithm §3.4 (Execution Model and Performance Considerations).


Controls the optional post-training simulation phase.

FieldTypeDefaultDescription
enabledbooleanfalseEnable the simulation phase after training.
selectionobjectnullScenario-selection method for the simulation phase — sampled{num_scenarios} or enumerated{} (see simulation.selection below). Absent resolves to a sampled count of 2000.

Chooses how many trajectories the post-training simulation runs, or whether it exhaustively enumerates the scenario set instead of sampling. Internally tagged on method, mirroring training.selection above. Unlike training.selection, this field is optional: an absent simulation.selection resolves to a sampled count of 2000 — the same default the removed flat num_scenarios field carried before this field existed.

FieldTypeRequiredDescription
methodstringYes (if present)"sampled" or "enumerated"
num_scenariosintegersampled onlyNumber of independent Monte Carlo simulation scenarios to evaluate. Rejected under "enumerated".

When simulation.enabled is false, or the sampled num_scenarios count resolves to 0, the simulation phase is skipped entirely.

simulation.solver (optional, default null) overrides the LP solver profile used during the simulation phase. It takes the identical PhaseSolverProfileConfig shape as training.solver.backward and training.solver.forward — see training.solver under Advanced Fields for the full field table, caveats, and validation ranges.

Example:

{
"simulation": {
"enabled": true,
"selection": { "method": "sampled", "num_scenarios": 1000 }
}
}

Controls where the forward-pass noise comes from during the simulation phase. When absent, simulation falls back to the scheme configured under training.scenario_source. This allows you to train with in-sample noise and simulate with a different scheme (for example, out-of-sample or historical) without modifying the training configuration.

The fields are identical to training.scenario_source:

FieldTypeDefaultDescription
seedinteger or nullnullShared forward-pass seed for out_of_sample, historical, and external schemes.
inflowobjectin_sampleSampling scheme for hydro inflow. Object with "scheme" key.
loadobjectin_sampleSampling scheme for bus load. Object with "scheme" key.
ncsobjectin_sampleSampling scheme for NCS availability. Object with "scheme" key.
historical_yearsarray or objectauto-discoverRestrict the pool of historical windows. List ([1940, 1953]) or range ({"from": 1940, "to": 2010}).

Example — simulate with out-of-sample inflow while training uses in-sample:

{
"training": {
"selection": { "method": "sampled", "forward_passes": 50 },
"stopping_rules": [{ "type": "iteration_limit", "limit": 200 }]
},
"simulation": {
"enabled": true,
"selection": { "method": "sampled", "num_scenarios": 2000 },
"scenario_source": {
"seed": 77,
"inflow": { "scheme": "out_of_sample" },
"load": { "scheme": "in_sample" },
"ncs": { "scheme": "in_sample" }
}
}
}

Controls physical modeling options.

FieldTypeDefaultDescription
inflow_non_negativityobjectsee belowStrategy for handling negative PAR model inflow draws.
cost_scale_factornumber1_000_000.0Divisor on every non-θ objective coefficient (objective conditioning; does not alter the model, unlike this section’s other fields). See below.
FieldTypeDefaultDescription
methodstring"penalty"One of "none", "penalty", "truncation", or "truncation_with_penalty".
  • "none" — no treatment; negative inflows are passed through to the LP.
  • "penalty" — adds a slack variable to the LP that absorbs negative inflow realisations. The slack carries a per-hydro objective cost from penalties.json::hydro.inflow_nonnegativity_cost.
  • "truncation" — clamps negative PAR model draws to zero before applying noise.
  • "truncation_with_penalty" — combines both: clamps the inflow to zero and adds a bounded slack variable penalised by penalties.json::hydro.inflow_nonnegativity_cost, providing a smooth backstop for extreme tail realisations.

Example:

{
"modeling": {
"inflow_non_negativity": {
"method": "penalty"
}
}
}

Divisor applied to every non-θ objective coefficient at LP template build time; every cost-domain output (objective value, duals, cut coefficients) is multiplied back by the same factor at every reporting boundary. This is objective conditioning, not a physical modeling choice — unlike inflow_non_negativity above, results are identical in exact arithmetic; changing it never alters the model.

Defaults to 1_000_000.0 when absent or null (byte-identical to the pre-v0.12.0 hard-coded behaviour). Must be finite and > 0; a value outside [1.0, 1e12] is accepted but logs an advisory warning — it is not rejected.

Example:

{
"modeling": {
"cost_scale_factor": 1000000.0
}
}

For the methodology, see LP Scaling §12.1 (Cost Scaling).


Controls the row management pipeline for managing row pool growth. The pipeline has up to two stages: strategy-based selection and budget enforcement. Row management periodically scans the row pool and deactivates rows that are unlikely to improve the policy, reducing LP size without sacrificing convergence quality. For a detailed explanation of each stage, see Performance Accelerators.

The block has two always-on knobs at the top level plus a selection sub-object that chooses the method and carries only that method’s parameters. Omitting selection (or setting it to null) disables row selection — that is the default.

FieldTypeDefaultDescription
row_activity_tolerancefloat0.0Minimum dual-multiplier magnitude for a constraint row to count as binding at a solution point. Rows whose dual falls below this are treated as inactive in tracking.
max_active_per_stageintegernullHard cap on active rows per stage LP, enforced after the selection method runs. null = no cap.
selectionobjectnullThe active selection method and its parameters (see below). null (the default) disables row selection.

selection.method is the discriminator; each method exposes only its own parameters. Supplying a parameter that belongs to a different method is a config load error, and a misspelled method is rejected with the list of valid methods.

  • "level1" — evaluates all populated rows at every visited state and retains any row whose value is within tie_tolerance of the per-state maximum at some state. Least aggressive; preserves the convergence guarantee.

    FieldTypeDefaultDescription
    tie_tolerancefloat1e-10A row is active at a state when within this of the best row value there.
    check_frequencyinteger5Iterations between periodic pruning checks. Must be > 0.
  • "lml1" — at each visited state, retains only the oldest eligible row within tie_tolerance of the per-state maximum; the selected set is the union of those per-state survivors. More aggressive than "level1". Same fields as "level1" (tie_tolerance, check_frequency).

  • "domination" — removes rows dominated at all visited states.

    FieldTypeDefaultDescription
    domination_tolerancefloatA row survives if within this of the maximum at any visited state. Required.
    check_frequencyinteger5Iterations between periodic pruning checks. Must be > 0.
  • "dynamic" — a per-solve lazy loop that loads only a small resident subset of rows per solve while retaining the full pool. The resident set is seeded from the most recent iterations, and each lazy-solve round adds the most-violated candidate rows.

    FieldTypeDefaultDescription
    start_iterationinteger2First 1-based iteration at which the lazy loop becomes active. Must be >= 1.
    seed_windowinteger5Number of most-recent iterations whose rows seed the initial resident set. 0 is valid (seeds only the current iteration).
    candidate_recencyintegernullOnly rows generated within the last candidate_recency iterations are scored. null (the default) is unbounded — every pool row is a candidate, which preserves exactness. Some(n) (must be >= 1) makes the loop deliberately inexact: rows older than the window are never added.
    max_added_per_roundinteger10Maximum rows added per lazy-solve round. Must be >= 1.
    violation_tolerancefloat1e-10Violation tolerance for accepting a candidate row. Must be > 0.

The dynamic method is mutually exclusive with the periodic-pruning methods by construction — choosing it from the tagged selection block means none of level1 / lml1 / domination can run.

Example with the dynamic method:

{
"training": {
"cut_selection": {
"row_activity_tolerance": 1e-6,
"max_active_per_stage": 4000,
"selection": {
"method": "dynamic",
"start_iteration": 2,
"seed_window": 5,
"max_added_per_round": 10,
"violation_tolerance": 1e-10
}
}
}
}

Example with the level1 method and a per-stage budget:

{
"training": {
"cut_selection": {
"row_activity_tolerance": 1e-6,
"max_active_per_stage": 500,
"selection": {
"method": "level1",
"tie_tolerance": 1e-10,
"check_frequency": 5
}
}
}
}

Controls the PAR(p) model estimation pipeline. When the case provides inflow_history.parquet, Cobre can automatically estimate AR coefficients instead of requiring pre-computed inflow_ar_coefficients.parquet.

FieldTypeDefaultDescription
max_orderinteger6Maximum lag order considered during autoregressive model fitting.
order_selectionstring"pacf"Order selection criterion: "pacf" (PACF-based) or "pacf_annual" (PACF with annual component).
min_observations_per_seasoninteger30Minimum observations per (entity, season) group to proceed with estimation.
max_coefficient_magnitudefloatnullSafety net: reduce to order 0 if any coefficient exceeds this magnitude.

Example:

{
"estimation": {
"max_order": 6,
"order_selection": "pacf",
"min_observations_per_season": 30
}
}

Setting "order_selection": "pacf_annual" activates the annual component extension. When enabled, the estimation pipeline performs four additional steps beyond the classical PAR path: (1) the Yule-Walker system is extended to include a cross-correlation term between the current-season inflow and the rolling 12-month average; (2) per-season sample statistics (mean and standard deviation) of that rolling average are computed for each hydro plant; (3) the coefficient, mean, and standard deviation are written to inflow_annual_component.parquet in the output directory; and (4) the lag stride used when building the LP noise columns is widened to accommodate the extra annual term. Use this option when your inflow series shows persistence that extends beyond the standard seasonal lag window.


Controls policy persistence (checkpoint saving and warm-start loading).

FieldTypeDefaultDescription
pathstring"./policy"Directory where policy data (cuts, states) is stored.
mode"fresh", "warm_start", or "resume""fresh"Initialization mode. "fresh" starts from scratch; "warm_start" loads cuts from a previous run; "resume" continues an interrupted run.
boundaryobject or nullnullTerminal boundary cut configuration for coupling with an outer model’s FCF. See below.
FieldTypeDefaultDescription
enabledbooleanfalseEnable periodic checkpointing during training.
initial_iterationintegernullFirst iteration to write a checkpoint.
interval_iterationsintegernullIterations between checkpoints.
store_basisbooleanfalseInclude LP basis in checkpoints for warm-start.
compressbooleanfalseCompress checkpoint files.

Optional configuration for loading terminal-stage boundary cuts from a different Cobre policy checkpoint. When present, the solver loads cuts from the source checkpoint and injects them as fixed boundary conditions at the terminal stage of the current study. The imported cuts are not updated by training — they remain fixed throughout.

This enables Cobre-to-Cobre model coupling: a monthly study produces a policy checkpoint, and a weekly+monthly coupled study loads that checkpoint’s cuts as its terminal-stage future cost function.

FieldTypeDescription
pathstringPath to the source policy checkpoint directory.
source_stageinteger0-based stage index in the source checkpoint to load cuts from.

Example — load stage 2’s cuts from a monthly policy as terminal boundary:

{
"policy": {
"mode": "fresh",
"boundary": {
"path": "../monthly_study/policy",
"source_stage": 2
}
}
}

See Policy Management — Boundary Cuts for a full explanation of the coupling workflow.


Cobre does not have dedicated config.json fields for temporal resolution. The resolution of each stage is determined entirely by the date boundaries in stages.json. However, when stages.json defines stages at different temporal resolutions — for example, four weekly stages within a month followed by monthly stages, or monthly stages transitioning to quarterly stages — three mechanisms activate automatically that users should understand.

When multiple SDDP stages share the same season_id within the same calendar period (for example, four weekly stages all assigned season_id: 0 for January), they receive identical PAR noise draws. This ensures that sub-monthly stages present an inflow trajectory consistent with the monthly PAR model they were fitted from, rather than fabricating independent weekly variability that the historical record does not support.

When the study includes stages at different resolutions (for example, monthly and quarterly), Cobre automatically aggregates fine-grained historical observations into coarser season buckets before PAR fitting. A user supplying monthly inflow_history.parquet for a study that includes quarterly stages does not need to pre-aggregate the data; Cobre derives one observation per (entity, season, year) at the appropriate coarser resolution. Aggregating in the opposite direction (disaggregating coarser observations to a finer resolution) is not supported and will produce a validation error at case load time.

For studies that transition from monthly to quarterly stages, the PAR lag state changes resolution at the boundary. During the monthly phase, each monthly inflow is accumulated into a ring buffer indexed by the downstream (quarterly) lag. When the first quarterly stage is reached, the ring buffer contains a complete set of duration-weighted monthly contributions, and the lag state is rebuilt automatically. This transition is transparent to the LP and the cut representation; it introduces no additional LP variables.

The following stages.json excerpt shows four weekly stages within January (stages 0-3, all with season_id: 0) followed by a normal monthly stage for February (season_id: 1). Stages 0-3 share the same season_id and will therefore receive identical PAR noise draws during training:

[
{
"id": 0,
"start_date": "2024-01-01",
"end_date": "2024-01-08",
"season_id": 0,
"num_openings": 50
},
{
"id": 1,
"start_date": "2024-01-08",
"end_date": "2024-01-15",
"season_id": 0,
"num_openings": 50
},
{
"id": 2,
"start_date": "2024-01-15",
"end_date": "2024-01-22",
"season_id": 0,
"num_openings": 50
},
{
"id": 3,
"start_date": "2024-01-22",
"end_date": "2024-02-01",
"season_id": 0,
"num_openings": 50
},
{
"id": 4,
"start_date": "2024-02-01",
"end_date": "2024-03-01",
"season_id": 1,
"num_openings": 50
}
]
Section titled “Recommended Alternative: Weekly Blocks Within a Monthly Stage”

When weekly dispatch granularity is needed but true weekly-resolution noise data is unavailable, the recommended approach is to use a single monthly SDDP stage with chronological blocks rather than four separate weekly SDDP stages. This provides weekly LP granularity while keeping one noise realization per month — consistent with the data resolution — and avoids the lag-accumulation complications that arise with multiple independent weekly stages. See Block Formulation Variants for the chronological formulation behind this pattern and Case Directory Format for the stages.json block_mode field.

  • Multi-Resolution Studies — mixed-resolution mechanics: same-season noise groups, duration-weighted observation aggregation, and PAR fitting on aggregated statistics
  • Weekly+Monthly Coupled Studies — the two-study coupling alternative when true weekly-resolution noise data exists

Controls which outputs are written to the results directory.

FieldTypeDefaultDescription
statesbooleanfalseWrite visited forward-pass trial points to the policy checkpoint (FlatBuffers).
stochasticbooleanfalseExport stochastic preprocessing artifacts to output/stochastic/.
fpha_deviation_pointsbooleanfalseExport the per-grid-point computed-FPHA fit-deviation table to output/hydro_models/fpha_deviation_points.parquet. Opt-in because it emits one row per (hydro, stage, V, Q) sample point at spillage = 0.

{
"$schema": "https://docs.cobre-rs.dev/schemas/config.schema.json",
"training": {
"tree_seed": 42,
"selection": { "method": "sampled", "forward_passes": 50 },
"stopping_rules": [
{ "type": "iteration_limit", "limit": 200 },
{ "type": "bound_stalling", "iterations": 20, "tolerance": 0.0001 }
],
"stopping_mode": "any",
"scenario_source": {
"seed": 99,
"inflow": { "scheme": "out_of_sample" },
"load": { "scheme": "in_sample" },
"ncs": { "scheme": "in_sample" }
},
"cut_selection": {
"row_activity_tolerance": 1e-6,
"max_active_per_stage": null,
"selection": {
"method": "level1",
"tie_tolerance": 1e-10,
"check_frequency": 5
}
}
},
"modeling": {
"inflow_non_negativity": {
"method": "penalty"
}
},
"simulation": {
"enabled": true,
"selection": { "method": "sampled", "num_scenarios": 2000 }
},
"policy": {
"path": "./policy",
"mode": "fresh"
},
"exports": {
"states": false,
"stochastic": false
}
}

The Config struct supports additional sections not documented on this page. These fields are deserialized from config.json when present but are intended for advanced use cases and may change between releases:

SectionPurpose
upper_bound_evaluationInner approximation upper-bound evaluation settings
training.solverLP solver retry policy and optional per-phase profile overrides (backward, forward) — see training.solver below; for the performance-tuning treatment of the same profiles, see Solver Safeguards
simulation.io_channel_capacityAsync I/O channel buffer size for simulation output writing

All fields have defaults and can be omitted. Every JSON input file rejects unknown keys, so misspelled fields raise a parse error rather than being silently ignored. For the complete list of fields and their types, see the Config struct in the cobre-io API docs.

LP solver retry policy plus optional per-phase profile overrides. retry_max_attempts and retry_time_budget_seconds are pre-existing fields; backward and forward are new in v0.12.0.

FieldTypeDefaultDescription
retry_max_attemptsinteger5Maximum solver retry attempts before propagating a hard error.
retry_time_budget_secondsfloat30.0Total time budget in seconds across all retry attempts for one solve.
backwardobject or nullnullPer-phase LP solver profile override applied during the backward pass.
forwardobject or nullnullPer-phase LP solver profile override applied during the forward pass.

simulation.solver (see simulation above) is a third, independent sibling that takes the identical shape — training.solver.backward, training.solver.forward, and simulation.solver all resolve against one shared PhaseSolverProfileConfig, documented once below.

Every field is optional and defaults to null. An absent field leaves the corresponding option at the phase’s built-in tuned profile — not the underlying solver’s own default profile; the two differ (for example, all three phase profiles set price to "row_hyper_sparse", while HiGHS’s own default pricing is "row"). A study with no solver-profile configuration at all (no backward, forward, or simulation.solver) resolves byte-identically to the prior, unconfigurable per-phase defaults. Resolution happens once at setup — before any LP template is built — and the result is broadcast identically to every rank.

FieldType / values
dual_edge_weight"devex" | "steepest_edge" | "dantzig"
scale"off" | "solver_scaling"
price"row" | "row_hyper_sparse"
presolve"on" | "off" | "choose"
primal_feasibility_tolerancenumber
dual_feasibility_tolerancenumber
simplex_update_limituint32
cost_perturbationnumber
refactor_error_tolerancenumber
factor_pivot_thresholdnumber
use_warm_startboolean
steepest_edge_devex_fallback_thresholdnumber
  • presolve affects only a genuinely cold solve — a warm-started solve skips presolve regardless of the setting.
  • use_warm_start is a diagnostic override, not an intended production setting: setting it to false forces every solve in the phase cold.
  • dual_edge_weight: "steepest_edge" is a request, not a guarantee — HiGHS silently falls back to Devex pricing once the dual steepest-edge weight log-error exceeds steepest_edge_devex_fallback_threshold.

Seven fields are range-checked; a value outside its range is rejected at config-load time with an error naming the phase and the offending field:

  • primal_feasibility_tolerance — must be finite.
  • dual_feasibility_tolerance — must be finite and >= 1e-10.
  • simplex_update_limit — must be <= 2147483647 (i32::MAX).
  • cost_perturbation — must be finite and >= 0.
  • refactor_error_tolerance — must be finite and >= 0.
  • factor_pivot_threshold — must be in [8e-4, 0.5].
  • steepest_edge_devex_fallback_threshold — must be finite and >= 1.0.

The closed enums (dual_edge_weight, scale, price, presolve) and use_warm_start are unconditionally valid — every variant is supported.

Per-phase solver-profile overrides are HiGHS-only. On the CLP backend, any field set under backward, forward, or simulation.solver is rejected at setup — deterministically, on every rank, before any LP template is built — with a named error identifying the phase and the field. An empty override block (for example "solver": {} or "backward": {}) and an absent block are both legal on CLP; only a field that is actually set triggers rejection. See Installation — Choosing a Backend for backend selection and CLP’s other limitations.

Example — training.solver.backward with two overrides:

{
"training": {
"solver": {
"retry_max_attempts": 5,
"retry_time_budget_seconds": 30.0,
"backward": {
"dual_edge_weight": "steepest_edge",
"factor_pivot_threshold": 0.05
}
}
}
}