1#[derive(Debug, Clone, Copy, PartialEq, Default)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct HydroBlockOverride {
26 pub min_turbined_m3s: Option<f64>,
28 pub max_turbined_m3s: Option<f64>,
30 pub min_outflow_m3s: Option<f64>,
32 pub max_outflow_m3s: Option<f64>,
34 pub min_generation_mw: Option<f64>,
36 pub max_generation_mw: Option<f64>,
38 pub min_diversion_m3s: Option<f64>,
40 pub max_diversion_m3s: Option<f64>,
42 pub min_spillage_m3s: Option<f64>,
44 pub max_spillage_m3s: Option<f64>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Default)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct ThermalBlockOverride {
56 pub min_generation_mw: Option<f64>,
58 pub max_generation_mw: Option<f64>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Default)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct LineBlockOverride {
66 pub direct_mw: Option<f64>,
68 pub reverse_mw: Option<f64>,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Default)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78pub struct ContractBlockOverride {
79 pub min_mw: Option<f64>,
81 pub max_mw: Option<f64>,
83 pub price_per_mwh: Option<f64>,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Default)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90pub struct PumpingBlockOverride {
91 pub min_flow_m3s: Option<f64>,
93 pub max_flow_m3s: Option<f64>,
95}
96
97#[derive(Debug, Clone)]
101pub struct BlockBoundsCountsSpec {
102 pub n_hydros: usize,
104 pub n_thermals: usize,
106 pub n_lines: usize,
108 pub n_pumping: usize,
110 pub n_contracts: usize,
112 pub n_stages: usize,
114 pub max_blocks: usize,
116}
117
118#[derive(Debug, Clone, PartialEq)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141pub struct ResolvedBlockBounds {
142 n_stages: usize,
143 max_blocks: usize,
144 n_hydros: usize,
145 n_thermals: usize,
146 n_lines: usize,
147 n_pumping: usize,
148 n_contracts: usize,
149 hydro: Vec<HydroBlockOverride>,
150 thermal: Vec<ThermalBlockOverride>,
151 line: Vec<LineBlockOverride>,
152 pumping: Vec<PumpingBlockOverride>,
153 contract: Vec<ContractBlockOverride>,
154}
155
156impl Default for ResolvedBlockBounds {
157 fn default() -> Self {
158 Self::empty()
159 }
160}
161
162impl ResolvedBlockBounds {
163 #[must_use]
176 pub fn empty() -> Self {
177 Self {
178 n_stages: 0,
179 max_blocks: 0,
180 n_hydros: 0,
181 n_thermals: 0,
182 n_lines: 0,
183 n_pumping: 0,
184 n_contracts: 0,
185 hydro: Vec::new(),
186 thermal: Vec::new(),
187 line: Vec::new(),
188 pumping: Vec::new(),
189 contract: Vec::new(),
190 }
191 }
192
193 #[must_use]
198 pub fn new(counts: &BlockBoundsCountsSpec) -> Self {
199 Self {
200 n_stages: counts.n_stages,
201 max_blocks: counts.max_blocks,
202 n_hydros: counts.n_hydros,
203 n_thermals: counts.n_thermals,
204 n_lines: counts.n_lines,
205 n_pumping: counts.n_pumping,
206 n_contracts: counts.n_contracts,
207 hydro: Vec::new(),
208 thermal: Vec::new(),
209 line: Vec::new(),
210 pumping: Vec::new(),
211 contract: Vec::new(),
212 }
213 }
214
215 fn flat_index(&self, entity_idx: usize, stage_idx: usize, block_idx: usize) -> Option<usize> {
216 if stage_idx >= self.n_stages || block_idx >= self.max_blocks {
217 return None;
218 }
219 Some((entity_idx * self.n_stages + stage_idx) * self.max_blocks + block_idx)
220 }
221
222 #[inline]
226 #[must_use]
227 pub fn hydro_override(
228 &self,
229 hydro_idx: usize,
230 stage_idx: usize,
231 block_idx: usize,
232 ) -> HydroBlockOverride {
233 self.flat_index(hydro_idx, stage_idx, block_idx)
234 .and_then(|idx| self.hydro.get(idx))
235 .copied()
236 .unwrap_or_default()
237 }
238
239 #[inline]
243 pub fn hydro_override_mut(
244 &mut self,
245 hydro_idx: usize,
246 stage_idx: usize,
247 block_idx: usize,
248 ) -> Option<&mut HydroBlockOverride> {
249 let idx = self.flat_index(hydro_idx, stage_idx, block_idx)?;
250 if self.hydro.is_empty() {
251 self.hydro = vec![
252 HydroBlockOverride::default();
253 self.n_hydros * self.n_stages * self.max_blocks
254 ];
255 }
256 self.hydro.get_mut(idx)
257 }
258
259 #[inline]
263 #[must_use]
264 pub fn thermal_override(
265 &self,
266 thermal_idx: usize,
267 stage_idx: usize,
268 block_idx: usize,
269 ) -> ThermalBlockOverride {
270 self.flat_index(thermal_idx, stage_idx, block_idx)
271 .and_then(|idx| self.thermal.get(idx))
272 .copied()
273 .unwrap_or_default()
274 }
275
276 #[inline]
280 pub fn thermal_override_mut(
281 &mut self,
282 thermal_idx: usize,
283 stage_idx: usize,
284 block_idx: usize,
285 ) -> Option<&mut ThermalBlockOverride> {
286 let idx = self.flat_index(thermal_idx, stage_idx, block_idx)?;
287 if self.thermal.is_empty() {
288 self.thermal = vec![
289 ThermalBlockOverride::default();
290 self.n_thermals * self.n_stages * self.max_blocks
291 ];
292 }
293 self.thermal.get_mut(idx)
294 }
295
296 #[inline]
300 #[must_use]
301 pub fn line_override(
302 &self,
303 line_idx: usize,
304 stage_idx: usize,
305 block_idx: usize,
306 ) -> LineBlockOverride {
307 self.flat_index(line_idx, stage_idx, block_idx)
308 .and_then(|idx| self.line.get(idx))
309 .copied()
310 .unwrap_or_default()
311 }
312
313 #[inline]
317 pub fn line_override_mut(
318 &mut self,
319 line_idx: usize,
320 stage_idx: usize,
321 block_idx: usize,
322 ) -> Option<&mut LineBlockOverride> {
323 let idx = self.flat_index(line_idx, stage_idx, block_idx)?;
324 if self.line.is_empty() {
325 self.line =
326 vec![LineBlockOverride::default(); self.n_lines * self.n_stages * self.max_blocks];
327 }
328 self.line.get_mut(idx)
329 }
330
331 #[inline]
335 #[must_use]
336 pub fn pumping_override(
337 &self,
338 pumping_idx: usize,
339 stage_idx: usize,
340 block_idx: usize,
341 ) -> PumpingBlockOverride {
342 self.flat_index(pumping_idx, stage_idx, block_idx)
343 .and_then(|idx| self.pumping.get(idx))
344 .copied()
345 .unwrap_or_default()
346 }
347
348 #[inline]
352 pub fn pumping_override_mut(
353 &mut self,
354 pumping_idx: usize,
355 stage_idx: usize,
356 block_idx: usize,
357 ) -> Option<&mut PumpingBlockOverride> {
358 let idx = self.flat_index(pumping_idx, stage_idx, block_idx)?;
359 if self.pumping.is_empty() {
360 self.pumping = vec![
361 PumpingBlockOverride::default();
362 self.n_pumping * self.n_stages * self.max_blocks
363 ];
364 }
365 self.pumping.get_mut(idx)
366 }
367
368 #[inline]
372 #[must_use]
373 pub fn contract_override(
374 &self,
375 contract_idx: usize,
376 stage_idx: usize,
377 block_idx: usize,
378 ) -> ContractBlockOverride {
379 self.flat_index(contract_idx, stage_idx, block_idx)
380 .and_then(|idx| self.contract.get(idx))
381 .copied()
382 .unwrap_or_default()
383 }
384
385 #[inline]
389 pub fn contract_override_mut(
390 &mut self,
391 contract_idx: usize,
392 stage_idx: usize,
393 block_idx: usize,
394 ) -> Option<&mut ContractBlockOverride> {
395 let idx = self.flat_index(contract_idx, stage_idx, block_idx)?;
396 if self.contract.is_empty() {
397 self.contract = vec![
398 ContractBlockOverride::default();
399 self.n_contracts * self.n_stages * self.max_blocks
400 ];
401 }
402 self.contract.get_mut(idx)
403 }
404
405 #[inline]
407 #[must_use]
408 pub fn is_empty(&self) -> bool {
409 self.hydro.is_empty()
410 && self.thermal.is_empty()
411 && self.line.is_empty()
412 && self.pumping.is_empty()
413 && self.contract.is_empty()
414 }
415}
416
417#[cfg(test)]
420mod tests {
421 use super::{
422 BlockBoundsCountsSpec, ContractBlockOverride, HydroBlockOverride, LineBlockOverride,
423 PumpingBlockOverride, ResolvedBlockBounds, ThermalBlockOverride,
424 };
425
426 #[test]
427 fn test_empty_block_bounds_returns_all_none_and_never_panics() {
428 let table = ResolvedBlockBounds::empty();
429 let result = table.thermal_override(3, 7, 2);
430 assert_eq!(result, ThermalBlockOverride::default());
431 assert_eq!(result.min_generation_mw, None);
432 assert_eq!(result.max_generation_mw, None);
433 assert!(table.is_empty());
434 }
435
436 #[test]
437 fn test_block_override_write_is_visible_at_its_own_triple_only() {
438 let mut table = ResolvedBlockBounds::new(&BlockBoundsCountsSpec {
439 n_hydros: 0,
440 n_thermals: 2,
441 n_lines: 0,
442 n_pumping: 0,
443 n_contracts: 0,
444 n_stages: 3,
445 max_blocks: 3,
446 });
447
448 table
449 .thermal_override_mut(1, 2, 0)
450 .unwrap()
451 .max_generation_mw = Some(100.0);
452
453 assert_eq!(
454 table.thermal_override(1, 2, 0).max_generation_mw,
455 Some(100.0)
456 );
457 assert_eq!(table.thermal_override(1, 2, 1).max_generation_mw, None);
458 assert_eq!(table.thermal_override(0, 2, 0).max_generation_mw, None);
459 assert_eq!(table.thermal_override(1, 0, 0).max_generation_mw, None);
460
461 table
466 .thermal_override_mut(0, 1, 0)
467 .unwrap()
468 .max_generation_mw = Some(55.0);
469 assert_eq!(
470 table.thermal_override(0, 1, 0).max_generation_mw,
471 Some(55.0)
472 );
473 assert_eq!(table.thermal_override(1, 0, 0).max_generation_mw, None);
474 }
475
476 #[test]
477 fn test_out_of_range_block_override_read_returns_default() {
478 let mut table = ResolvedBlockBounds::new(&BlockBoundsCountsSpec {
479 n_hydros: 2,
480 n_thermals: 0,
481 n_lines: 0,
482 n_pumping: 0,
483 n_contracts: 0,
484 n_stages: 3,
485 max_blocks: 3,
486 });
487 table.hydro_override_mut(0, 0, 0).unwrap().max_turbined_m3s = Some(42.0);
488
489 assert_eq!(
490 table.hydro_override(99, 0, 0),
491 HydroBlockOverride::default()
492 );
493 assert_eq!(
494 table.hydro_override(0, 0, 99),
495 HydroBlockOverride::default()
496 );
497 assert!(table.hydro_override_mut(99, 0, 0).is_none());
498 assert!(table.hydro_override_mut(0, 0, 99).is_none());
499 }
500
501 #[test]
502 fn test_block_override_structs_pin_the_cost_price_asymmetry() {
503 let hydro = HydroBlockOverride {
504 min_turbined_m3s: Some(1.0),
505 max_turbined_m3s: Some(2.0),
506 min_outflow_m3s: Some(3.0),
507 max_outflow_m3s: Some(4.0),
508 min_generation_mw: Some(5.0),
509 max_generation_mw: Some(6.0),
510 min_diversion_m3s: Some(7.0),
511 max_diversion_m3s: Some(8.0),
512 min_spillage_m3s: Some(9.0),
513 max_spillage_m3s: Some(10.0),
514 };
515 assert_eq!(hydro.min_turbined_m3s, Some(1.0));
516 assert_eq!(hydro.min_diversion_m3s, Some(7.0));
517 assert_eq!(hydro.max_diversion_m3s, Some(8.0));
518 assert_eq!(hydro.min_spillage_m3s, Some(9.0));
519 assert_eq!(hydro.max_spillage_m3s, Some(10.0));
520
521 let thermal = ThermalBlockOverride {
522 min_generation_mw: Some(1.0),
523 max_generation_mw: Some(2.0),
524 };
525 assert_eq!(thermal.max_generation_mw, Some(2.0));
526
527 let contract = ContractBlockOverride {
528 min_mw: Some(1.0),
529 max_mw: Some(2.0),
530 price_per_mwh: Some(3.0),
531 };
532 assert_eq!(contract.price_per_mwh, Some(3.0));
533 }
534
535 #[test]
536 fn test_round_trip_writes_a_distinct_value_per_family() {
537 let mut table = ResolvedBlockBounds::new(&BlockBoundsCountsSpec {
538 n_hydros: 1,
539 n_thermals: 1,
540 n_lines: 1,
541 n_pumping: 1,
542 n_contracts: 1,
543 n_stages: 2,
544 max_blocks: 2,
545 });
546
547 table.hydro_override_mut(0, 0, 0).unwrap().max_generation_mw = Some(11.0);
548 table
549 .thermal_override_mut(0, 0, 0)
550 .unwrap()
551 .max_generation_mw = Some(22.0);
552 table.line_override_mut(0, 0, 0).unwrap().direct_mw = Some(33.0);
553 table.pumping_override_mut(0, 0, 0).unwrap().max_flow_m3s = Some(44.0);
554 table.contract_override_mut(0, 0, 0).unwrap().price_per_mwh = Some(55.0);
555
556 assert_eq!(table.hydro_override(0, 0, 0).max_generation_mw, Some(11.0));
557 assert_eq!(
558 table.thermal_override(0, 0, 0).max_generation_mw,
559 Some(22.0)
560 );
561 assert_eq!(table.line_override(0, 0, 0).direct_mw, Some(33.0));
562 assert_eq!(table.pumping_override(0, 0, 0).max_flow_m3s, Some(44.0));
563 assert_eq!(table.contract_override(0, 0, 0).price_per_mwh, Some(55.0));
564
565 let l = LineBlockOverride::default();
566 assert_eq!(l.reverse_mw, None);
567 let p = PumpingBlockOverride::default();
568 assert_eq!(p.min_flow_m3s, None);
569 }
570}