Skip to main content

cobre_sddp/setup/
accessors.rs

1//! Accessor methods and context builders for [`StudySetup`].
2
3use cobre_core::scenario::SamplingScheme;
4
5use crate::{
6    context::{StageContext, TrainingContext},
7    cut::FutureCostFunction,
8    energy_conversion::EnergyConversionSet,
9    indexer::StageIndexer,
10    simulation::SimulationConfig,
11    workspace::CapturedBasis,
12};
13
14use super::StudySetup;
15
16impl StudySetup {
17    // -------------------------------------------------------------------------
18    // Mutation setters — remain `pub` (called from cobre-cli or cobre-python)
19    // -------------------------------------------------------------------------
20
21    /// Replace the FCF with a pre-loaded policy.
22    pub fn replace_fcf(&mut self, fcf: FutureCostFunction) {
23        self.fcf = fcf;
24    }
25
26    /// Set the starting iteration for resumed training.
27    pub fn set_start_iteration(&mut self, iteration: u64) {
28        self.loop_params.start_iteration = iteration;
29    }
30
31    /// Seed the per-stage warm-start basis cache for warm-start / resume
32    /// training.
33    ///
34    /// `cache` carries one entry per stage (as built by
35    /// [`build_basis_cache_from_checkpoint`](crate::build_basis_cache_from_checkpoint)
36    /// from the checkpoint's stored solver bases). [`StudySetup::train`] takes
37    /// this out of `self` and replicates each stage's basis across every
38    /// forward-pass worker so iteration 1's cut-loaded LPs warm-start.
39    ///
40    /// Leave unset (the default `None`) for a fresh start.
41    pub fn set_warm_start_basis_cache(&mut self, cache: Vec<Option<CapturedBasis>>) {
42        self.warm_start_basis_cache = Some(cache);
43    }
44
45    /// Enable state archiving for export.
46    pub fn set_export_states(&mut self, export: bool) {
47        self.events.export_states = export;
48    }
49
50    /// Set the active-cut budget cap per stage.
51    pub fn set_budget(&mut self, budget: Option<u32>) {
52        self.cut_management.budget = budget;
53    }
54
55    // ─────────────────────────────────────────────────────────────────────
56    // Context builders — span multiple sub-structs
57    // ─────────────────────────────────────────────────────────────────────
58
59    /// Return the pre-computed [`EnergyConversionSet`] for this study.
60    ///
61    /// Provides `ρ_eq`, `V_ref`, `Q_ref`, and `ρ_acum` (accumulated cascade
62    /// productivity) for every `(hydro, stage)` pair. Consumed by the
63    /// energy-balance LP constraints and inflow-energy / stored-energy extraction.
64    #[must_use]
65    pub fn energy_conversion(&self) -> &EnergyConversionSet {
66        &self.energy_conversion
67    }
68
69    /// Return a reference to the simulation configuration.
70    #[must_use]
71    pub fn simulation_config(&self) -> &SimulationConfig {
72        &self.simulation_config
73    }
74
75    /// Return a reference to the per-stage LP column/row indexer.
76    ///
77    /// Provides LP layout constants — column and row ranges for every entity
78    /// class (storage, thermal, anticipated state, etc.) — so that callers can
79    /// locate specific primal or state-vector entries without hard-coding
80    /// offsets.
81    ///
82    /// The same indexer applies to every stage (the layout is uniform across
83    /// stages in a study).
84    #[must_use]
85    pub fn stage_indexer(&self) -> &StageIndexer {
86        &self.stage_data.indexer
87    }
88
89    /// Number of stages in the planning horizon.
90    ///
91    /// Used by the CLI summary to express the pool-level active-row total on a
92    /// per-stage basis, so it is directly comparable to the per-solve
93    /// rows-in-LP metric reported for Dynamic Cut Selection.
94    #[must_use]
95    pub fn num_stages(&self) -> usize {
96        self.methodology.horizon.num_stages()
97    }
98
99    /// Construct a [`StageContext`] borrowing from this setup.
100    #[must_use]
101    pub fn stage_ctx(&self) -> StageContext<'_> {
102        StageContext {
103            templates: &self.stage_data.stage_templates.templates,
104            base_rows: &self.stage_data.stage_templates.base_rows,
105            noise_scale: &self.stage_data.stage_templates.noise_scale,
106            n_hydros: self.stage_data.stage_templates.n_hydros,
107            n_load_buses: self.stage_data.stage_templates.n_load_buses,
108            load_balance_row_starts: &self.stage_data.stage_templates.load_balance_row_starts,
109            load_bus_indices: &self.stage_data.stage_templates.load_bus_indices,
110            block_counts_per_stage: &self.stage_data.block_counts_per_stage,
111            ncs_max_gen: &self.ncs_max_gen,
112            ncs_allow_curtailment: &self.ncs_allow_curtailment,
113            discount_factors: &self.stage_data.stage_templates.discount_factors,
114            cumulative_discount_factors: &self
115                .stage_data
116                .stage_templates
117                .cumulative_discount_factors,
118            stage_lag_transitions: &self.stage_data.stage_lag_transitions,
119            noise_group_ids: &self.stage_data.noise_group_ids,
120            downstream_par_order: self.downstream_par_order,
121        }
122    }
123
124    /// Construct a [`TrainingContext`] borrowing from this setup. Test-only.
125    #[cfg(test)]
126    #[must_use]
127    pub(crate) fn training_ctx(&self) -> TrainingContext<'_> {
128        let tr = &self.scenario_libraries.training;
129        TrainingContext {
130            horizon: &self.methodology.horizon,
131            indexer: &self.stage_data.indexer,
132            inflow_method: &self.methodology.inflow_method,
133            stochastic: &self.stochastic,
134            initial_state: &self.initial_state,
135            inflow_scheme: tr.inflow_scheme,
136            load_scheme: tr.load_scheme,
137            ncs_scheme: tr.ncs_scheme,
138            stages: &self.stage_data.stages,
139            historical_library: tr.historical.as_ref(),
140            external_inflow_library: tr.external_inflow.as_ref(),
141            external_load_library: tr.external_load.as_ref(),
142            external_ncs_library: tr.external_ncs.as_ref(),
143            recent_accum_seed: &self.recent_observation_seed.accum_seed,
144            recent_weight_seed: self.recent_observation_seed.weight_seed,
145            dcs: self
146                .cut_management
147                .cut_selection
148                .as_ref()
149                .and_then(crate::dcs::DcsParams::from_strategy),
150            noise_key_diag: None,
151        }
152    }
153
154    /// Build simulation [`TrainingContext`] with simulation-specific schemes and libraries.
155    ///
156    /// Reuses training libraries when simulation schemes match. Selects per-class
157    /// libraries in this order: simulation-specific, then training (shared).
158    #[must_use]
159    pub(crate) fn simulation_ctx(&self) -> TrainingContext<'_> {
160        let tr = &self.scenario_libraries.training;
161        let sim = &self.scenario_libraries.simulation;
162
163        // For each class, prefer the simulation-specific library when present;
164        // fall back to the training library when schemes are identical.
165        let historical_library =
166            sim.historical
167                .as_ref()
168                .or(if sim.inflow_scheme == SamplingScheme::Historical {
169                    tr.historical.as_ref()
170                } else {
171                    None
172                });
173        let external_inflow_library =
174            sim.external_inflow
175                .as_ref()
176                .or(if sim.inflow_scheme == SamplingScheme::External {
177                    tr.external_inflow.as_ref()
178                } else {
179                    None
180                });
181        let external_load_library =
182            sim.external_load
183                .as_ref()
184                .or(if sim.load_scheme == SamplingScheme::External {
185                    tr.external_load.as_ref()
186                } else {
187                    None
188                });
189        let external_ncs_library =
190            sim.external_ncs
191                .as_ref()
192                .or(if sim.ncs_scheme == SamplingScheme::External {
193                    tr.external_ncs.as_ref()
194                } else {
195                    None
196                });
197
198        TrainingContext {
199            horizon: &self.methodology.horizon,
200            indexer: &self.stage_data.indexer,
201            inflow_method: &self.methodology.inflow_method,
202            stochastic: &self.stochastic,
203            initial_state: &self.initial_state,
204            inflow_scheme: sim.inflow_scheme,
205            load_scheme: sim.load_scheme,
206            ncs_scheme: sim.ncs_scheme,
207            stages: &self.stage_data.stages,
208            historical_library,
209            external_inflow_library,
210            external_load_library,
211            external_ncs_library,
212            recent_accum_seed: &self.recent_observation_seed.accum_seed,
213            recent_weight_seed: self.recent_observation_seed.weight_seed,
214            // When the dynamic cut-selection method is configured, simulation
215            // solves each stage lazily against the cut pool (`Some` only for the
216            // dynamic variant); otherwise it uses the baked all-cuts path.
217            dcs: self
218                .cut_management
219                .cut_selection
220                .as_ref()
221                .and_then(crate::dcs::DcsParams::from_strategy),
222            // The backward `noise_key` diagnostic does not apply to simulation.
223            noise_key_diag: None,
224        }
225    }
226}