Skip to main content

cobre_io/
lib.rs

1//! # cobre-io
2//!
3//! Case directory loading, validation, and result writing for the
4//! [Cobre](https://github.com/cobre-rs/cobre) power systems ecosystem.
5//!
6//! This crate provides two top-level entry points:
7//!
8//! - [`load_case`] — reads a case directory and produces a fully-validated
9//!   [`cobre_core::System`] ready for use by the solver.
10//! - [`write_results`] — accepts aggregate result types and writes all output
11//!   artifacts to a specified root directory.
12//!
13//! ## Loading pipeline
14//!
15//! [`load_case`] executes a six-layer validation pipeline:
16//!
17//! 1. **Structural validation** — checks that required files exist on disk and records
18//!    which optional files are present ([`validation::structural`]).
19//! 2. **Schema validation** — verifies required fields, types, and value ranges.
20//! 3. **Referential integrity** — checks all entity ID cross-references are resolvable.
21//! 4. **Dimensional consistency** — cross-file coverage checks (e.g., inflow params
22//!    cover all hydros).
23//! 5. **Semantic validation** — domain business rules (acyclic cascade, penalty ordering,
24//!    PAR stationarity, etc.).
25//! 6. **Cross-file resolution and cross-validation** — multi-file consistency checks that
26//!    span the parsed data assembled by earlier layers (productivity source conflict
27//!    detection, scalar-parameter hydro-ID existence, per-stage length checks).
28//!
29//! All validation diagnostics are collected by [`validation::ValidationContext`] before
30//! failing, so users see every problem in a single report.  Final errors are reported
31//! via [`LoadError`], which carries enough context for diagnostic messages without
32//! re-reading input files.
33//!
34//! ## Status
35//!
36//! This crate is in early development. The API **will** change.
37//!
38//! See the [repository](https://github.com/cobre-rs/cobre) for the current status.
39
40// Internal (unpublished) workspace crate: public items intra-doc-link their
41// pub(crate) collaborators as a maintainer aid (docs read with
42// --document-private-items); the public-only doc gate flags these intentional links.
43#![allow(rustdoc::private_intra_doc_links)]
44
45#[cfg(feature = "schema")]
46pub mod schema;
47
48pub mod broadcast;
49pub mod config;
50pub mod constraints;
51pub mod error;
52pub mod extensions;
53pub mod initial_conditions;
54pub mod output;
55pub(crate) mod parquet_helpers;
56pub mod penalties;
57pub(crate) mod pipeline;
58pub mod post_study_stages;
59pub mod report;
60pub mod resolution;
61pub mod scenarios;
62pub mod stage_resolve;
63pub mod stages;
64pub mod system;
65pub mod validation;
66pub(crate) mod windowed_history;
67
68pub use broadcast::{
69    BroadcastComputedParameter, BroadcastParameterKind, BroadcastScalarParameter,
70    deserialize_parameters, deserialize_system, serialize_parameters, serialize_system,
71};
72pub use config::{
73    BoundaryPolicy, Config, EstimationConfig, OrderSelectionMethod, PolicyMode, parse_config,
74};
75pub use constraints::{
76    BusPenaltyOverrideRow, ContractBoundsRow, GenericConstraintBoundsRow, HydroBoundsRow,
77    HydroPenaltyOverrideRow, LineBoundsRow, LineBusPairIndex, LinePenaltyOverrideRow,
78    NcsPenaltyOverrideRow, PumpingBoundsRow, ThermalBoundsRow, build_line_bus_pair_index,
79    load_contract_bounds, load_generic_constraint_bounds, load_generic_constraints,
80    load_hydro_bounds, load_line_bounds, load_penalty_overrides_bus, load_penalty_overrides_hydro,
81    load_penalty_overrides_line, load_penalty_overrides_ncs, load_pumping_bounds,
82    load_thermal_bounds, parse_contract_bounds, parse_generic_constraint_bounds,
83    parse_generic_constraints, parse_hydro_bounds, parse_line_bounds, parse_penalty_overrides_bus,
84    parse_penalty_overrides_hydro, parse_penalty_overrides_line, parse_penalty_overrides_ncs,
85    parse_pumping_bounds, parse_thermal_bounds,
86};
87pub use error::LoadError;
88pub use extensions::{
89    EvaporationModelRow, FittingWindow, FphaColumnLayout, FphaDeviationPointRow, FphaHyperplaneRow,
90    HydroEnergyProductivityRow, HydroGeometryRow, HydroReferenceVolumeFractions,
91    PlaneReductionConfig, ProductionModelConfig, ProductionModelFile, SeasonConfig, SelectionMode,
92    StageRange, build_hydro_reference_volumes_resolved, load_fpha_hyperplanes,
93    load_hydro_energy_productivity, load_hydro_geometry, load_production_models,
94    load_scalar_parameters_json, parse_evaporation_models, parse_fpha_deviation_points,
95    parse_fpha_hyperplanes, parse_hydro_energy_productivity, parse_hydro_geometry,
96    parse_production_models, parse_scalar_parameters_json,
97};
98pub use initial_conditions::parse_initial_conditions;
99pub use output::policy::codec::{deserialize_checkpoint_manifest, serialize_checkpoint_manifest};
100pub use output::policy::records::CheckpointManifest;
101pub use output::policy::{
102    ENTITY_SLOT_DELIVERY_DATE_SENTINEL, EntitySlot, FORMAT_VERSION, GraphManifest, ManifestEdge,
103    ManifestNode, OwnedPolicyBasisRecord, OwnedPolicyCutRecord, PolicyBasisRecord,
104    PolicyCheckpoint, PolicyCutRecord, ProducerBlock, STAGE_CUTS_GRAPH_STAGE_ID_SENTINEL,
105    STAGE_CUTS_NODE_ID_SENTINEL, STAGE_STATES_NODE_ID_SENTINEL, StageCutsPayload,
106    StageCutsReadResult, StageStatesPayload, StageStatesReadResult, StateFamily,
107    deserialize_stage_basis, deserialize_stage_cuts, deserialize_stage_states,
108    read_policy_checkpoint, serialize_stage_basis, serialize_stage_cuts, serialize_stage_states,
109    write_policy_checkpoint,
110};
111pub use output::{
112    ConvergenceSummary, DeviationSummary, DeviationWorstEntry, DistributionInfo, FixedDeliveryRow,
113    GenericConstraintEchoRow, HostLayout, IterationRecord, MetadataBounds, MetadataConfiguration,
114    MetadataConvergence, MetadataCost, MetadataIterations, MetadataProblemDimensions,
115    MetadataRowPool, MetadataScenarios, MetadataSimulationSolveStats, MetadataTrainingSolveStats,
116    OutputContext, OutputError, ParquetWriterConfig, RowPoolStatistics, RowSelectionRecord,
117    SetupTimings, SimulationMetadata, SimulationOutput, SolverStatsRow, TrainingMetadata,
118    TrainingOutput, TrainingParquetWriter, WorkerTimingRecord, get_hostname, now_iso8601,
119    read_convergence_summary, read_hydro_model_summary, read_provenance_report,
120    read_simulation_metadata, read_training_metadata, write_dictionaries, write_evaporation_models,
121    write_fixed_delivery, write_fpha_deviation_points, write_fpha_hyperplanes,
122    write_generic_constraint_echo, write_hydro_model_summary, write_provenance_report,
123    write_results, write_row_selection_records, write_scaling_report, write_simulation_metadata,
124    write_simulation_results, write_simulation_solver_stats, write_solver_stats,
125    write_training_metadata, write_training_results,
126};
127pub use penalties::parse_penalties;
128pub use post_study_stages::parse_post_study_stages;
129pub use report::{ReportEntry, ValidationReport, generate_report};
130pub use resolution::{resolve_bounds, resolve_penalties};
131pub use scenarios::{
132    BlockFactor, ExternalLoadRow, ExternalNcsRow, ExternalScenarioRow, InflowArCoefficientRow,
133    InflowHistoryRow, InflowSeasonalStatsRow, LoadFactorEntry, LoadSeasonalStatsRow,
134    NoiseOpeningRow, ScenarioData, assemble_inflow_models, assemble_load_models, load_correlation,
135    load_external_inflow_scenarios, load_external_load_scenarios, load_external_ncs_scenarios,
136    load_inflow_ar_coefficients, load_inflow_history, load_inflow_seasonal_stats,
137    load_load_factors, load_load_seasonal_stats, load_noise_openings, load_scenarios,
138    parse_correlation, parse_external_inflow_scenarios, parse_external_load_scenarios,
139    parse_external_ncs_scenarios, parse_inflow_ar_coefficients, parse_inflow_history,
140    parse_inflow_seasonal_stats, parse_load_factors, parse_load_seasonal_stats,
141};
142pub use stage_resolve::StageIdResolver;
143pub use stages::{StagesData, build_season_stage_map, parse_stages};
144pub use system::{
145    load_energy_contracts, load_non_controllable_sources, load_pumping_stations, parse_buses,
146    parse_energy_contracts, parse_hydros, parse_lines, parse_non_controllable_sources,
147    parse_pumping_stations, parse_thermals,
148};
149pub use validation::scalar_parameters::validate_scalar_parameters;
150pub use validation::semantic::seed_lag_state_depth;
151pub use validation::structural::{FileManifest, validate_structure};
152pub use validation::{ErrorKind, Severity, ValidationContext, ValidationEntry};
153
154use cobre_core::{ScalarParameter, System};
155use std::path::Path;
156
157/// Auxiliary rows produced by the load pipeline alongside [`System`].
158///
159/// `CaseArtifacts` is the single-source delivery of the already-parsed-and-validated
160/// parquet/JSON rows, so downstream solver crates do not re-open the same files
161/// from disk after [`load_case`] returns.
162///
163/// Fields are owned `Vec`s in deterministic (canonical) order. Empty vectors
164/// indicate the optional file was absent on disk.
165#[derive(Debug, Clone, Default)]
166pub struct CaseArtifacts {
167    /// File-presence manifest produced by Layer 1 (structural). Lets
168    /// downstream code avoid re-running `validate_structure` to check
169    /// optional-file presence.
170    pub file_manifest: FileManifest,
171
172    /// Rows from `system/hydro_geometry.parquet`.
173    pub hydro_geometry: Vec<extensions::HydroGeometryRow>,
174
175    /// Entries from `system/hydro_production_models.json`.
176    pub production_models: Vec<extensions::ProductionModelConfig>,
177
178    /// File-level FPHA plane-reduction block from
179    /// `system/hydro_production_models.json`. `None` when the file is absent or
180    /// carries no `fpha_plane_reduction` key. Carried for the post-fit
181    /// plane-reduction pass; no behavior depends on it yet.
182    pub plane_reduction: Option<extensions::PlaneReductionConfig>,
183
184    /// Rows from `system/hydro_energy_productivity.parquet`.
185    pub hydro_energy_productivity: Vec<extensions::HydroEnergyProductivityRow>,
186
187    /// Rows from `system/fpha_hyperplanes.parquet`.
188    pub fpha_hyperplanes: Vec<extensions::FphaHyperplaneRow>,
189
190    /// Assembled scalar parameters from `constraints/generic_parameters.json`.
191    pub scalar_parameters: Vec<ScalarParameter>,
192
193    /// Rows from `system/tailrace_curves.parquet`.
194    pub tailrace_curves: Vec<extensions::TailraceCurveRow>,
195}
196
197/// Fully-loaded case bundle: the validated [`System`] plus the auxiliary
198/// row sets that downstream consumers need without re-reading the case
199/// directory.
200#[derive(Debug)]
201pub struct LoadedCase {
202    /// Validated, ready-to-solve system.
203    pub system: System,
204    /// Auxiliary rows (parsed and validated by the load pipeline).
205    pub artifacts: CaseArtifacts,
206}
207
208/// Load a case directory and return a fully-validated [`System`].
209///
210/// `path` must point to the root case directory containing `config.json` and the
211/// standard subdirectories (`system/`, `scenarios/`, `constraints/`, `policy/`).
212///
213/// The function executes a six-layer validation pipeline; see the
214/// [crate-level docs](crate) for the layer-by-layer breakdown.
215///
216/// After all layers pass, three-tier penalty/bound resolution and scenario assembly
217/// are performed before constructing the [`System`].
218///
219/// Warnings collected during validation are silently discarded. Use [`validate_case`]
220/// when you need to inspect or display warnings alongside the loaded [`System`].
221///
222/// Prefer [`load_case_with_artifacts`] when the downstream consumer also needs the
223/// auxiliary parquet/JSON rows this pipeline already parsed: it returns them as a
224/// [`CaseArtifacts`] bundle so downstream code can skip the duplicate disk reads.
225///
226/// # Errors
227///
228/// - [`LoadError::IoError`] — a required file is missing or cannot be read.
229/// - [`LoadError::ParseError`] — a file contains malformed JSON or invalid Parquet.
230/// - [`LoadError::SchemaError`] — a domain constraint violation detected
231///   post-deserialization (e.g., AR coefficient count mismatch).
232/// - [`LoadError::ConstraintError`] — one or more validation errors collected
233///   across Layers 1-5, or `SystemBuilder` rejected the assembled data.
234pub fn load_case(path: &Path) -> Result<System, LoadError> {
235    pipeline::run_pipeline(path)
236}
237
238/// Load a case directory and return the validated [`System`] together with
239/// the [`CaseArtifacts`] bundle of pre-parsed auxiliary rows.
240///
241/// This is the preferred entry point for solver pipelines that need the
242/// production-model / hydro-geometry / FPHA hyperplane / scalar-parameter
243/// rows: returning them here avoids the duplicate disk re-reads and parallel
244/// validation paths in downstream crates.
245///
246/// The function runs the six-layer validation pipeline described in [`load_case`].
247///
248/// # Errors
249///
250/// Same error conditions as [`load_case`].
251pub fn load_case_with_artifacts(path: &Path) -> Result<LoadedCase, LoadError> {
252    pipeline::run_pipeline_with_artifacts(path).map(|(loaded, _report)| loaded)
253}
254
255/// Load a case directory and return both the fully-validated [`System`] and a
256/// [`ValidationReport`] containing all warnings collected during the pipeline.
257///
258/// This function runs the same six-layer validation pipeline as [`load_case`] but
259/// preserves warnings so that callers can display them to the user. Errors still
260/// cause the function to return `Err`; warnings never block loading.
261///
262/// # Errors
263///
264/// Same error conditions as [`load_case`].
265pub fn validate_case(path: &Path) -> Result<(System, ValidationReport), LoadError> {
266    pipeline::run_pipeline_with_report(path)
267}
268
269/// Load a case directory and return the validated [`LoadedCase`] together with a
270/// [`ValidationReport`] containing all warnings collected during the pipeline.
271///
272/// This is the preferred entry point for callers that need both the auxiliary
273/// [`CaseArtifacts`] bundle (for downstream prep phases such as
274/// `prepare_hydro_models_from_artifacts`) **and** the warning report.
275///
276/// The function runs the same six-layer validation pipeline as [`load_case`]. Errors
277/// still cause the function to return `Err`; warnings never block loading.
278///
279/// # Errors
280///
281/// Same error conditions as [`load_case`].
282pub fn validate_case_with_artifacts(
283    path: &Path,
284) -> Result<(LoadedCase, ValidationReport), LoadError> {
285    pipeline::run_pipeline_with_artifacts(path)
286}
287
288#[cfg(test)]
289mod tests {
290    use crate::CaseArtifacts;
291
292    #[test]
293    fn case_artifacts_plane_reduction_defaults_to_none() {
294        let artifacts = CaseArtifacts::default();
295        assert!(artifacts.plane_reduction.is_none());
296    }
297}