1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! Layer 5 — Semantic validation: hydro, thermal, stage, penalty, and scenario rules.
//!
//! Validates all domain-specific business rules after Layers 2-4 have
//! ensured schema correctness, referential integrity, and dimensional
//! consistency.
//!
//! ## Layer 5a rules (hydro and thermal domain) — `validate_semantic_hydro_thermal`
//!
//! | # | Rule | Source file | `ErrorKind` |
//! |---|---------------------------------------------------|---------------------------------------|------------------------|
//! | 1 | Hydro cascade graph must be acyclic | `system/hydros.json` | `CycleDetected` |
//! | 2 | `min_storage_hm3 <= max_storage_hm3` | `system/hydros.json` | `InvalidValue` |
//! | 3 | `min_turbined_m3s <= max_turbined_m3s` | `system/hydros.json` | `InvalidValue` |
//! | 4 | `min_outflow_m3s <= max_outflow_m3s` (when Some) | `system/hydros.json` | `InvalidValue` |
//! | 5 | `min_generation_mw <= max_generation_mw` (hydro) | `system/hydros.json` | `InvalidValue` |
//! | 6 | `entry_stage_id < exit_stage_id` (when both Some) | hydros/lines/thermals | `InvalidValue` |
//! | 7 | Filling `start_stage_id` in study stage set | `system/hydros.json` | `InvalidValue` |
//! | 8 | Geometry `volume_hm3` strictly increasing | `system/hydro_geometry.parquet` | `BusinessRuleViolation`|
//! | 9 | Geometry `height_m` non-decreasing | `system/hydro_geometry.parquet` | `BusinessRuleViolation`|
//! |10 | Geometry `area_km2` non-decreasing | `system/hydro_geometry.parquet` | `BusinessRuleViolation`|
//! |11 | FPHA: at least 1 plane per (hydro, stage) | `system/fpha_hyperplanes.parquet` | `BusinessRuleViolation`|
//! |12 | FPHA: `gamma_v >= 0`, `gamma_s <= 0` | `system/fpha_hyperplanes.parquet` | `BusinessRuleViolation`|
//! |13 | `min_generation_mw <= max_generation_mw` (thermal)| `system/thermals.json` | `InvalidValue` |
//! |14 | Anticipated thermal `lead_stages` within study horizon and lifecycle bounds | `system/thermals.json` | `BusinessRuleViolation` |
//! |15 | Anticipated thermals bijection with `past_anticipated_commitments` entries | `initial_conditions.json` | `BusinessRuleViolation` |
//! |16 | Thermal `thermal_bounds.parquet` override `stage_id` within `[0, n_stages)` | `constraints/thermal_bounds.parquet` | `BusinessRuleViolation` |
//! |17 | `anticipated_decision(N)` in generic constraint targets an anticipated thermal | `constraints/generic_constraints.json` | `BusinessRuleViolation` |
//! |18 | `thermal_generation(N)` in generic constraint when `N` is anticipated (warn) | `constraints/generic_constraints.json` | `SemanticAmbiguity` (warning) |
//!
//! ## Layer 5b rules (stages, penalties, and scenario domain) — `validate_semantic_stages_penalties_scenarios`
//!
//! | # | Rule | Source file | `ErrorKind` |
//! |----|-------------------------------------------------------------------------|------------------------------------------------|--------------------------|
//! | 1 | Every transition `source_id`/`target_id` must refer to an existing stage| `stages.json` | `InvalidValue` |
//! | 2 | Outgoing transition probabilities sum to 1.0 (±1e-6) per source stage | `stages.json` | `InvalidValue` |
//! | 3 | Cyclic graph: `annual_discount_rate > 0.0` | `stages.json` | `InvalidValue` |
//! | 4 | Every `Block.duration_hours > 0.0` | `stages.json` | `InvalidValue` |
//! | 5 | CVaR: `alpha` in (0, 1], `lambda` in [0, 1] | `stages.json` | `InvalidValue` |
//! | 6 | `filling_target_violation_cost > storage_violation_below_cost` | `penalties.json` | `ModelQuality` (warning) |
//! | 7 | `storage_violation_below_cost > max(deficit_segment_costs)` | `penalties.json` | `ModelQuality` (warning) |
//! | 8 | `max(deficit_segment_costs) > max(constraint_violation_costs)` | `penalties.json` | `ModelQuality` (warning) |
//! | 9 | `min(constraint_violation_costs) > max(resource_costs)` | `penalties.json` | `ModelQuality` (warning) |
//! |10 | `min(resource_costs) > 0` | `penalties.json` | `ModelQuality` (warning) |
//! |11 | FPHA hydros: `turbined_cost >= 0` | `penalties.json` | `BusinessRuleViolation` |
//! |12 | `std_m3s >= 0.0`; warn when `== 0.0` (deterministic inflow) | `scenarios/inflow_seasonal_stats.parquet` | `ModelQuality` (warning) |
//! |13 | `residual_std_ratio` consistent across all lag rows of same group | `scenarios/inflow_ar_coefficients.parquet` | `InvalidValue` |
//! |14 | Correlation matrix symmetry (`matrix[i][j] == matrix[j][i]` ±1e-9) | `scenarios/correlation.json` | `BusinessRuleViolation` |
//! |15 | Correlation matrix diagonal entries equal 1.0 (±1e-9) | `scenarios/correlation.json` | `BusinessRuleViolation` |
//! |16 | Correlation off-diagonal entries in [-1.0, 1.0] | `scenarios/correlation.json` | `BusinessRuleViolation` |
//! |17 | Each `block_factors[j].block_id` matches a `Block.index` in its stage | `scenarios/load_factors.json` | `BusinessRuleViolation` |
//! |18 | Load-factors entry for `(bus_id, stage_id)` with `std_mw == 0.0` | `scenarios/load_factors.json` | `ModelQuality` (warning) |
//! |19 | `season_definitions` required in `stages.json` when estimating | `scenarios/inflow_history.parquet` | `BusinessRuleViolation` |
//! |20 | Minimum observations per `(hydro, season)` group for estimation | `scenarios/inflow_history.parquet` | `ModelQuality` (warning) |
//! |21 | All hydros in `hydros.json` must have observations in history | `scenarios/inflow_history.parquet` | `BusinessRuleViolation` |
//! |22 | `inflow_lags: true` with PAR order > 0 requires non-empty `past_inflows` | `initial_conditions.json` | `BusinessRuleViolation` |
//! |23 | Each hydro with PAR order `p` must have a `past_inflows` entry with `values_m3s.len() >= p` | `initial_conditions.json` | `BusinessRuleViolation` |
//! |24 | All hydro IDs in `past_inflows` must exist in the hydro registry | `initial_conditions.json` | `BusinessRuleViolation` |
//! |25 | Sobol stages: `branching_factor` should be a power of 2 | `stages.json` | `ModelQuality` (warning) |
//! |26 | `simulation.sampling_scheme.type` must be a known scheme string | `config.json` | `InvalidValue` |
//! |27 | Every stage `season_id` must reference a season defined in `season_definitions` | `stages.json` | `BusinessRuleViolation` |
//! |28 | Season with zero observations when inflow scheme is not External | `stages.json` | `ModelQuality` (warning) |
//! |29 | All stages sharing a `season_id` must have compatible durations (within 7d) | `stages.json` | `BusinessRuleViolation` |
//! |30 | Season defined in `season_definitions` but not referenced by any stage | `stages.json` | `ModelQuality` (warning) |
//! |31 | Observation resolution must not be finer than season resolution | `scenarios/inflow_history.parquet` | `BusinessRuleViolation` |
//! |32 | Each `season_id` in `past_inflows[i].season_ids` must exist in `SeasonMap` | `initial_conditions.json` | `BusinessRuleViolation` |
use ;
pub
// ── validate_semantic_stages_penalties_scenarios ──────────────────────────────
/// Performs Layer 5b semantic validation: stage structure, penalty ordering,
/// and scenario model rules.
///
/// All 21 rules are checked regardless of failures in earlier rules — every
/// violation is collected before returning. This function is infallible; it
/// never returns a `Result`. Errors are pushed to `ctx` as
/// [`ErrorKind::InvalidValue`] or [`ErrorKind::BusinessRuleViolation`] entries;
/// penalty ordering warnings use [`ErrorKind::ModelQuality`].
///
/// # Arguments
///
/// * `data` — fully parsed case data produced by [`super::schema::validate_schema`].
/// * `ctx` — mutable validation context that accumulates diagnostics.
///
/// # Conditional checks
///
/// Rules 12-13 are only checked when `data.inflow_seasonal_stats` is non-empty.
/// Rules 14-16 are only checked when `data.correlation` is `Some`.
/// Rules 17-18 are only checked when `data.load_factors` is non-empty.
pub
// ── Tolerances ────────────────────────────────────────────────────────────────
/// Tolerance for floating-point probability sum comparisons.
const PROB_TOLERANCE: f64 = 1e-6;
/// Tolerance for floating-point correlation matrix comparisons.
const CORR_TOLERANCE: f64 = 1e-9;