{
  "$defs": {
    "BoundaryPolicy": {
      "additionalProperties": false,
      "description": "Boundary-row configuration for terminal-stage FCF coupling.\n\nWhen present, the solver loads rows from a source Cobre policy\ncheckpoint and injects them as fixed boundary conditions at the\nterminal stage of the current study.",
      "properties": {
        "path": {
          "description": "Path to the source policy checkpoint directory.",
          "type": "string"
        },
        "source_stage": {
          "description": "0-based stage index in the source checkpoint to load rows from.",
          "format": "uint32",
          "minimum": 0,
          "type": "integer"
        }
      },
      "required": [
        "path",
        "source_stage"
      ],
      "type": "object"
    },
    "CheckpointingConfig": {
      "additionalProperties": false,
      "description": "Checkpoint settings (`config.json → policy.checkpointing`).",
      "properties": {
        "compress": {
          "default": null,
          "description": "Compress checkpoint files.",
          "type": [
            "boolean",
            "null"
          ]
        },
        "enabled": {
          "default": null,
          "description": "Enable periodic checkpointing.",
          "type": [
            "boolean",
            "null"
          ]
        },
        "initial_iteration": {
          "default": null,
          "description": "First iteration to write a checkpoint.",
          "format": "uint32",
          "minimum": 0,
          "type": [
            "integer",
            "null"
          ]
        },
        "interval_iterations": {
          "default": null,
          "description": "Iterations between checkpoints.",
          "format": "uint32",
          "minimum": 0,
          "type": [
            "integer",
            "null"
          ]
        },
        "store_basis": {
          "default": null,
          "description": "Include LP basis in checkpoints for warm-start.",
          "type": [
            "boolean",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "EstimationConfig": {
      "additionalProperties": false,
      "description": "Time series estimation settings (`config.json → estimation`).\n\nControls automatic parameter estimation when historical inflow data is\nprovided without explicit model statistics or coefficients.",
      "properties": {
        "max_coefficient_magnitude": {
          "default": null,
          "description": "Maximum allowed absolute magnitude for any AR coefficient.\n\nWhen set, any (entity, season) pair with `|coefficient| > threshold`\nis immediately reduced to order 0 before the contribution analysis\nruns. This acts as a fast-path safety net for the most extreme\nexplosive models. Defaults to `None` (disabled; contribution analysis\nis the primary guard).",
          "format": "double",
          "type": [
            "number",
            "null"
          ]
        },
        "max_order": {
          "default": 6,
          "description": "Maximum lag order considered during autoregressive model fitting.",
          "format": "uint32",
          "minimum": 0,
          "type": "integer"
        },
        "min_observations_per_season": {
          "default": 30,
          "description": "Minimum number of observations required per (entity, season) group\nto proceed with estimation. Groups below this threshold are skipped.",
          "format": "uint32",
          "minimum": 0,
          "type": "integer"
        },
        "order_selection": {
          "$ref": "#/$defs/OrderSelectionMethod",
          "default": "pacf",
          "description": "Order selection criterion. Accepts `\"pacf\"` (classical PACF, default)\nor `\"pacf_annual\"` (PACF augmented with an annual component, PAR(p)-A)."
        }
      },
      "type": "object"
    },
    "ExportsConfig": {
      "additionalProperties": false,
      "description": "Export flags controlling which outputs are written to disk\n(`config.json → exports`).\n\nOnly the active fields below are accepted. Legacy keys (`training`, `cuts`,\n`vertices`, `simulation`, `forward_detail`, `backward_detail`,\n`compression`) must be removed from existing `config.json` files before\nloading — they are now rejected as unknown fields.",
      "properties": {
        "fpha_deviation_points": {
          "default": false,
          "description": "Export the per-sampled-point computed-FPHA fit-deviation table to\n`output/hydro_models/fpha_deviation_points.parquet`.\n\nOpt-in (default `false`) purely for size: it emits one row per\n`(hydro, stage, V, Q)` grid point at spillage = 0. Off ⇒ no file and a\nbyte-identical run; the table is additive and never enters the parity\nhash. The values are deterministic (a pure function of geometry + config),\nso the file is reproducible when emitted.",
          "type": "boolean"
        },
        "states": {
          "default": false,
          "description": "Export visited forward-pass trial points to the policy checkpoint.",
          "type": "boolean"
        },
        "stochastic": {
          "default": false,
          "description": "Export stochastic preprocessing artifacts to `output/stochastic/`.",
          "type": "boolean"
        }
      },
      "type": "object"
    },
    "InflowNonNegativityConfig": {
      "additionalProperties": false,
      "description": "Inflow non-negativity treatment settings.",
      "properties": {
        "method": {
          "$ref": "#/$defs/InflowNonNegativityMethod",
          "default": "penalty",
          "description": "Method: `\"none\"`, `\"truncation\"`, `\"penalty\"`, or `\"truncation_with_penalty\"`.\n\nDefault: `\"penalty\"`. The penalty objective coefficient is always sourced from\n`penalties.json → hydro.inflow_nonnegativity_cost` (default 1000.0 when absent)."
        }
      },
      "type": "object"
    },
    "InflowNonNegativityMethod": {
      "description": "Method string for inflow non-negativity enforcement.\n\nAccepted values in `config.json → modeling.inflow_non_negativity.method`:\n\n- `\"none\"` — no enforcement; PAR(p) inflows may be negative.\n- `\"truncation\"` — clamp negative PAR(p) inflows to zero before LP patching.\n- `\"penalty\"` — add slack columns with `inflow_nonnegativity_cost` objective\n  coefficient (sourced from `penalties.json → hydro.inflow_nonnegativity_cost`).\n- `\"truncation_with_penalty\"` — combine both: clamp noise *and* add slack columns.",
      "oneOf": [
        {
          "const": "none",
          "description": "No inflow non-negativity enforcement.",
          "type": "string"
        },
        {
          "const": "truncation",
          "description": "Truncation-based enforcement only (no slack columns).",
          "type": "string"
        },
        {
          "const": "penalty",
          "description": "Penalty-based enforcement via slack columns.\n\nObjective coefficient is `penalties.json → hydro.inflow_nonnegativity_cost`.",
          "type": "string"
        },
        {
          "const": "truncation_with_penalty",
          "description": "Combined truncation and penalty enforcement.",
          "type": "string"
        }
      ]
    },
    "LipschitzConfig": {
      "additionalProperties": false,
      "description": "Lipschitz constant settings for inner approximation.",
      "properties": {
        "fallback_value": {
          "default": null,
          "description": "Fallback value when automatic computation fails.",
          "format": "double",
          "type": [
            "number",
            "null"
          ]
        },
        "mode": {
          "default": null,
          "description": "Computation mode: `\"auto\"`.",
          "type": [
            "string",
            "null"
          ]
        },
        "scale_factor": {
          "default": null,
          "description": "Multiplicative safety margin applied to computed Lipschitz constants.",
          "format": "double",
          "type": [
            "number",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "ModelingConfig": {
      "additionalProperties": false,
      "description": "Modeling options (`config.json → modeling`).",
      "properties": {
        "inflow_non_negativity": {
          "$ref": "#/$defs/InflowNonNegativityConfig",
          "default": {
            "method": "penalty"
          },
          "description": "Strategy for handling non-negative inflow constraints."
        }
      },
      "type": "object"
    },
    "OrderSelectionMethod": {
      "description": "Order selection criterion for autoregressive model fitting.\n\nControls how the lag order is chosen when fitting a time series model.\nTwo variants are accepted:\n\n- `\"pacf\"` — classical periodic Yule-Walker with PACF-based order\n  selection. Default.\n- `\"pacf_annual\"` — extends `\"pacf\"` with an annual component (PAR(p)-A),\n  adding one extra coefficient ψ per (entity, season) that multiplies\n  the rolling 12-month average of past observations.",
      "oneOf": [
        {
          "const": "pacf",
          "description": "Periodic Yule-Walker partial autocorrelation method (PACF).",
          "type": "string"
        },
        {
          "const": "pacf_annual",
          "description": "Periodic Yule-Walker order selection augmented with an annual component.\n\nWhen selected, the estimation pipeline performs four steps beyond the\nclassical [`Self::Pacf`] path:\n\n1. **Extended Yule-Walker fitting** — the system is augmented with a\n   cross-correlation term between the current-season inflow and the\n   rolling 12-month average, yielding the annual coefficient ψ\n   alongside the classical AR coefficients.\n2. **Annual-stats computation** — per-season sample mean μ^A and\n   Bessel-corrected standard deviation σ^A of the rolling 12-month\n   average are computed for each hydro plant.\n3. **Parquet emission** — the triple (ψ, μ^A, σ^A) is written to\n   `inflow_annual_component.parquet` in the output directory.\n4. **Widened LP lag stride** — the noise-column layout in the LP is\n   extended to accommodate the annual term alongside the classical lags.",
          "type": "string"
        }
      ]
    },
    "PolicyConfig": {
      "additionalProperties": false,
      "description": "Policy directory settings (`config.json → policy`).",
      "properties": {
        "boundary": {
          "anyOf": [
            {
              "$ref": "#/$defs/BoundaryPolicy"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional boundary-row policy for terminal-stage coupling."
        },
        "checkpointing": {
          "$ref": "#/$defs/CheckpointingConfig",
          "default": {
            "compress": null,
            "enabled": null,
            "initial_iteration": null,
            "interval_iterations": null,
            "store_basis": null
          },
          "description": "Checkpoint settings."
        },
        "mode": {
          "$ref": "#/$defs/PolicyMode",
          "default": "fresh",
          "description": "Initialization mode: `\"fresh\"`, `\"warm_start\"`, or `\"resume\"`."
        },
        "path": {
          "default": "./policy",
          "description": "Directory for policy data (rows, states, vertices, basis).",
          "type": "string"
        },
        "validate_compatibility": {
          "default": true,
          "description": "Verify state dimension and entity compatibility when loading.",
          "type": "boolean"
        }
      },
      "type": "object"
    },
    "PolicyMode": {
      "description": "Policy initialization mode (`config.json → policy.mode`).\n\nControls whether the training phase starts from scratch, warm-starts from\na prior policy's rows, or resumes a checkpointed training run.",
      "oneOf": [
        {
          "const": "fresh",
          "description": "Start training from an empty future-cost function.",
          "type": "string"
        },
        {
          "const": "warm_start",
          "description": "Load rows from a prior policy checkpoint and continue training.",
          "type": "string"
        },
        {
          "const": "resume",
          "description": "Resume a previously interrupted training run from its checkpoint.",
          "type": "string"
        }
      ]
    },
    "RawClassConfigEntry": {
      "additionalProperties": false,
      "description": "Intermediate serde type for a single per-class scenario scheme in `config.json`.",
      "properties": {
        "scheme": {
          "description": "Scheme string: `\"in_sample\"`, `\"out_of_sample\"`, `\"external\"`, or `\"historical\"`.",
          "type": "string"
        }
      },
      "required": [
        "scheme"
      ],
      "type": "object"
    },
    "RawHistoricalYearsConfig": {
      "anyOf": [
        {
          "description": "Explicit list of year integers.",
          "items": {
            "format": "int32",
            "type": "integer"
          },
          "type": "array"
        },
        {
          "description": "Inclusive range shorthand.",
          "properties": {
            "from": {
              "description": "First year (inclusive).",
              "format": "int32",
              "type": "integer"
            },
            "to": {
              "description": "Last year (inclusive).",
              "format": "int32",
              "type": "integer"
            }
          },
          "required": [
            "from",
            "to"
          ],
          "type": "object"
        }
      ],
      "description": "Intermediate serde type for `historical_years` in `config.json`.\n\nHandles two JSON representations via `#[serde(untagged)]`:\n- Array: `[1940, 1953, 1971]` → [`RawHistoricalYearsConfig::List`]\n- Object: `{\"from\": 1940, \"to\": 2010}` → [`RawHistoricalYearsConfig::Range`]\n\nThe `List` variant must be declared first so serde tries it before `Range`\n(an integer array is tried before an object)."
    },
    "RawScenarioSourceConfig": {
      "additionalProperties": false,
      "description": "Intermediate serde type for per-class scenario source configuration in `config.json`.\n\nScoped to `config.json` fields (`training.scenario_source` /\n`simulation.scenario_source`).",
      "properties": {
        "historical_years": {
          "anyOf": [
            {
              "$ref": "#/$defs/RawHistoricalYearsConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Historical year pool. Absent means `None` (auto-discover at validation time)."
        },
        "inflow": {
          "anyOf": [
            {
              "$ref": "#/$defs/RawClassConfigEntry"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Inflow class scenario config. Absent defaults to `in_sample`."
        },
        "load": {
          "anyOf": [
            {
              "$ref": "#/$defs/RawClassConfigEntry"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Load class scenario config. Absent defaults to `in_sample`."
        },
        "ncs": {
          "anyOf": [
            {
              "$ref": "#/$defs/RawClassConfigEntry"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "NCS class scenario config. Absent defaults to `in_sample`."
        },
        "seed": {
          "default": null,
          "description": "Optional random seed for reproducible scenario generation.",
          "format": "int64",
          "type": [
            "integer",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "RowSelectionConfig": {
      "additionalProperties": false,
      "description": "Row-selection settings (`config.json → training.cut_selection`).\n\nRow selection bounds the per-solve LP size by limiting how many constraint\nrows from the row pool are carried into each solve. `selection` chooses the\nmethod and carries only that method's parameters; omitting it (the default)\ndisables row selection.",
      "properties": {
        "max_active_per_stage": {
          "default": null,
          "description": "Hard cap on active rows per stage LP, enforced after the selection\nmethod runs. Rows are evicted least-recently-active first, tie-broken by\nleast-frequently-active; rows added in the current iteration are never\nevicted. `None` (default) = no cap.",
          "format": "uint32",
          "minimum": 0,
          "type": [
            "integer",
            "null"
          ]
        },
        "row_activity_tolerance": {
          "default": null,
          "description": "Minimum dual-multiplier magnitude for a constraint row to count as\nbinding at a solution point. Rows whose dual value falls below this are\ntreated as inactive in activity tracking. Default `0.0` when absent.",
          "format": "double",
          "type": [
            "number",
            "null"
          ]
        },
        "selection": {
          "anyOf": [
            {
              "$ref": "#/$defs/SelectionMethod"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Active selection method and its parameters. Absent/`null` (default)\ndisables row selection."
        }
      },
      "type": "object"
    },
    "SelectionMethod": {
      "description": "Row-selection method and its method-specific parameters.\n\nInternally tagged on `method`; each variant carries only the fields it uses,\nso supplying a parameter that does not belong to the chosen method is a\nload-time error under `deny_unknown_fields`, and a misspelled `method` is an\n`unknown variant` error at parse time.",
      "oneOf": [
        {
          "additionalProperties": false,
          "description": "Level-1: retain any row near-optimal at some visited state.",
          "properties": {
            "check_frequency": {
              "default": 5,
              "description": "Iterations between periodic pruning checks. Must be `> 0`. Default `5`.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "method": {
              "const": "level1",
              "type": "string"
            },
            "tie_tolerance": {
              "default": 1e-10,
              "description": "Tie tolerance: a row is active at a state when within this of the\nbest row value there. Default `1e-10`.",
              "format": "double",
              "type": "number"
            }
          },
          "required": [
            "method"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "description": "Limited-memory Level-1: retain only the oldest eligible near-optimal row\nper visited state.",
          "properties": {
            "check_frequency": {
              "default": 5,
              "description": "Iterations between periodic pruning checks. Must be `> 0`. Default `5`.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "method": {
              "const": "lml1",
              "type": "string"
            },
            "tie_tolerance": {
              "default": 1e-10,
              "description": "Tie tolerance: a row is active at a state when within this of the\nbest row value there. Default `1e-10`.",
              "format": "double",
              "type": "number"
            }
          },
          "required": [
            "method"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "description": "Domination: remove rows dominated at all visited states.",
          "properties": {
            "check_frequency": {
              "default": 5,
              "description": "Iterations between periodic pruning checks. Must be `> 0`. Default `5`.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "domination_tolerance": {
              "description": "Activity tolerance: a row survives if within this of the maximum at\nany visited state. Required (no default).",
              "format": "double",
              "type": "number"
            },
            "method": {
              "const": "domination",
              "type": "string"
            }
          },
          "required": [
            "method",
            "domination_tolerance"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "description": "Dynamic: a per-solve lazy loop that loads only a small resident subset of\nrows per solve while retaining the full pool.",
          "properties": {
            "candidate_recency": {
              "default": null,
              "description": "Only rows generated within the last `candidate_recency` iterations are\nscored. `None` (default) = unbounded: every pool row is a candidate,\nwhich preserves exactness. `Some(n)` (must be `>= 1`) makes the loop\ndeliberately inexact — rows older than the window are never added.",
              "format": "uint32",
              "minimum": 0,
              "type": [
                "integer",
                "null"
              ]
            },
            "max_added_per_round": {
              "default": 10,
              "description": "Maximum rows added per lazy-solve round. Must be `>= 1`. Default `10`.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "method": {
              "const": "dynamic",
              "type": "string"
            },
            "seed_window": {
              "default": 5,
              "description": "Number of most-recent iterations whose rows seed the initial resident\nset. `0` is valid (seeds only the current iteration). Default `5`.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "start_iteration": {
              "default": 2,
              "description": "First 1-based iteration at which the lazy loop becomes active.\nMust be `>= 1`. Default `2`.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "violation_tolerance": {
              "default": 1e-10,
              "description": "Violation tolerance for accepting a candidate row. Must be `> 0`.\nDefault `1e-10`.",
              "format": "double",
              "type": "number"
            }
          },
          "required": [
            "method"
          ],
          "type": "object"
        }
      ]
    },
    "SimulationConfig": {
      "additionalProperties": false,
      "description": "Post-training simulation settings (`config.json → simulation`).",
      "properties": {
        "enabled": {
          "default": false,
          "description": "Enable post-training simulation.",
          "type": "boolean"
        },
        "io_channel_capacity": {
          "default": 64,
          "description": "Bounded channel capacity between simulation threads and the I/O writer thread.",
          "format": "uint32",
          "minimum": 0,
          "type": "integer"
        },
        "num_scenarios": {
          "default": 2000,
          "description": "Number of simulation scenarios.",
          "format": "uint32",
          "minimum": 0,
          "type": "integer"
        },
        "scenario_source": {
          "anyOf": [
            {
              "$ref": "#/$defs/RawScenarioSourceConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Scenario source configuration for the post-training simulation forward pass.\nWhen absent, falls back to the training scenario source."
        }
      },
      "type": "object"
    },
    "StoppingRuleConfig": {
      "description": "Deserialized configuration for one entry in `training.stopping_rules[]`.\n\nUses a `\"type\"` discriminator field (internally tagged) with `snake_case`\nvariant names matching the JSON schema.\n\nThe `GracefulShutdown` rule has no JSON representation — it is injected at\nruntime by `StoppingRuleSet` construction and is never deserialized.\n\n# Examples\n\n```\nuse cobre_io::config::StoppingRuleConfig;\n\nlet json = r#\"{\"type\": \"iteration_limit\", \"limit\": 100}\"#;\nlet rule: StoppingRuleConfig = serde_json::from_str(json).unwrap();\nassert!(matches!(rule, StoppingRuleConfig::IterationLimit { limit: 100 }));\n```",
      "oneOf": [
        {
          "description": "Stop after a fixed number of iterations. **Mandatory** — every rule set must\ncontain at least one `iteration_limit` rule.",
          "properties": {
            "limit": {
              "description": "Maximum iteration count $k_{max}$.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "type": {
              "const": "iteration_limit",
              "type": "string"
            }
          },
          "required": [
            "type",
            "limit"
          ],
          "type": "object"
        },
        {
          "description": "Stop after a wall-clock time limit.",
          "properties": {
            "seconds": {
              "description": "Time limit in seconds.",
              "format": "double",
              "type": "number"
            },
            "type": {
              "const": "time_limit",
              "type": "string"
            }
          },
          "required": [
            "type",
            "seconds"
          ],
          "type": "object"
        },
        {
          "description": "Stop when the lower bound stalls (relative improvement falls below tolerance).",
          "properties": {
            "iterations": {
              "description": "Window size $\\tau$ (number of past iterations to compare).",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "tolerance": {
              "description": "Relative improvement threshold.",
              "format": "double",
              "type": "number"
            },
            "type": {
              "const": "bound_stalling",
              "type": "string"
            }
          },
          "required": [
            "type",
            "iterations",
            "tolerance"
          ],
          "type": "object"
        },
        {
          "description": "Stop when both the bound and simulated policy costs have stabilized.",
          "properties": {
            "bound_tol": {
              "description": "Relative tolerance for bound stability.",
              "format": "double",
              "type": "number"
            },
            "bound_window": {
              "description": "Number of past iterations for bound stability check.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "distance_tol": {
              "description": "Normalized distance threshold between consecutive simulation results.",
              "format": "double",
              "type": "number"
            },
            "period": {
              "description": "Iterations between checks.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "replications": {
              "description": "Number of Monte Carlo forward simulations per check.",
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "type": {
              "const": "simulation",
              "type": "string"
            }
          },
          "required": [
            "type",
            "replications",
            "period",
            "bound_window",
            "distance_tol",
            "bound_tol"
          ],
          "type": "object"
        }
      ]
    },
    "TrainingConfig": {
      "additionalProperties": false,
      "description": "Training parameters (`config.json → training`).\n\n`forward_passes` and `stopping_rules` are mandatory — the loader returns\n[`crate::LoadError::SchemaError`] if either is absent.",
      "properties": {
        "cut_selection": {
          "$ref": "#/$defs/RowSelectionConfig",
          "default": {
            "max_active_per_stage": null,
            "row_activity_tolerance": null,
            "selection": null
          },
          "description": "Row-selection settings."
        },
        "enabled": {
          "default": true,
          "description": "Enable the training phase. When `false`, skip directly to simulation.",
          "type": "boolean"
        },
        "forward_passes": {
          "description": "Number of forward-pass scenario trajectories $M$ per iteration.\n\n**Mandatory** — no default. The loader rejects any config that omits this field.",
          "format": "uint32",
          "minimum": 0,
          "type": [
            "integer",
            "null"
          ]
        },
        "scenario_source": {
          "anyOf": [
            {
              "$ref": "#/$defs/RawScenarioSourceConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Scenario source configuration for the training forward pass.\nWhen absent, all classes default to `in_sample`."
        },
        "solver": {
          "$ref": "#/$defs/TrainingSolverConfig",
          "default": {
            "retry_max_attempts": 5,
            "retry_time_budget_seconds": 30.0
          },
          "description": "LP solver retry settings."
        },
        "stopping_mode": {
          "default": "any",
          "description": "How multiple stopping rules combine: `\"any\"` (OR) or `\"all\"` (AND).",
          "type": "string"
        },
        "stopping_rules": {
          "description": "List of stopping rule configurations.\n\n**Mandatory** — no default. Must contain at least one `iteration_limit` rule.",
          "items": {
            "$ref": "#/$defs/StoppingRuleConfig"
          },
          "type": [
            "array",
            "null"
          ]
        },
        "tree_seed": {
          "default": null,
          "description": "Random seed for the opening scenario tree (reproducible training).",
          "format": "int64",
          "type": [
            "integer",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "TrainingSolverConfig": {
      "additionalProperties": false,
      "description": "LP solver retry settings (`config.json → training.solver`).",
      "properties": {
        "retry_max_attempts": {
          "default": 5,
          "description": "Maximum solver retry attempts before propagating a hard error.",
          "format": "uint32",
          "minimum": 0,
          "type": "integer"
        },
        "retry_time_budget_seconds": {
          "default": 30.0,
          "description": "Total time budget in seconds across all retry attempts for one solve.",
          "format": "double",
          "type": "number"
        }
      },
      "type": "object"
    },
    "UpperBoundEvaluationConfig": {
      "additionalProperties": false,
      "description": "Upper-bound evaluation settings (`config.json → upper_bound_evaluation`).",
      "properties": {
        "enabled": {
          "default": null,
          "description": "Enable vertex-based inner approximation for upper bound computation.",
          "type": [
            "boolean",
            "null"
          ]
        },
        "initial_iteration": {
          "default": null,
          "description": "First iteration to compute the upper bound.",
          "format": "uint32",
          "minimum": 0,
          "type": [
            "integer",
            "null"
          ]
        },
        "interval_iterations": {
          "default": null,
          "description": "Iterations between upper-bound evaluations.",
          "format": "uint32",
          "minimum": 0,
          "type": [
            "integer",
            "null"
          ]
        },
        "lipschitz": {
          "$ref": "#/$defs/LipschitzConfig",
          "default": {
            "fallback_value": null,
            "mode": null,
            "scale_factor": null
          },
          "description": "Lipschitz constant settings."
        }
      },
      "type": "object"
    }
  },
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": false,
  "description": "Top-level deserialized representation of `config.json`.\n\nAll sections except `training` are optional; their defaults are applied by\nserde when the section is absent from the JSON.",
  "properties": {
    "$schema": {
      "description": "JSON schema URI — informational, not validated.",
      "type": [
        "string",
        "null"
      ]
    },
    "estimation": {
      "$ref": "#/$defs/EstimationConfig",
      "default": {
        "max_coefficient_magnitude": null,
        "max_order": 6,
        "min_observations_per_season": 30,
        "order_selection": "pacf"
      },
      "description": "Time series estimation settings for automatic model parameter fitting."
    },
    "exports": {
      "$ref": "#/$defs/ExportsConfig",
      "default": {
        "fpha_deviation_points": false,
        "states": false,
        "stochastic": false
      },
      "description": "Export flags controlling which outputs are written to disk."
    },
    "modeling": {
      "$ref": "#/$defs/ModelingConfig",
      "default": {
        "inflow_non_negativity": {
          "method": "penalty"
        }
      },
      "description": "Modeling options (inflow non-negativity treatment)."
    },
    "policy": {
      "$ref": "#/$defs/PolicyConfig",
      "default": {
        "boundary": null,
        "checkpointing": {
          "compress": null,
          "enabled": null,
          "initial_iteration": null,
          "interval_iterations": null,
          "store_basis": null
        },
        "mode": "fresh",
        "path": "./policy",
        "validate_compatibility": true
      },
      "description": "Policy directory settings (warm-start / resume)."
    },
    "simulation": {
      "$ref": "#/$defs/SimulationConfig",
      "default": {
        "enabled": false,
        "io_channel_capacity": 64,
        "num_scenarios": 2000,
        "scenario_source": null
      },
      "description": "Post-training simulation settings."
    },
    "training": {
      "$ref": "#/$defs/TrainingConfig",
      "description": "Training parameters — contains mandatory fields."
    },
    "upper_bound_evaluation": {
      "$ref": "#/$defs/UpperBoundEvaluationConfig",
      "default": {
        "enabled": null,
        "initial_iteration": null,
        "interval_iterations": null,
        "lipschitz": {
          "fallback_value": null,
          "mode": null,
          "scale_factor": null
        }
      },
      "description": "Upper-bound evaluation via inner approximation."
    }
  },
  "required": [
    "training"
  ],
  "title": "Config",
  "type": "object"
}