use std::collections::HashMap;
use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HydroStagePenalties {
pub spillage_cost: f64,
pub diversion_cost: f64,
pub fpha_turbined_cost: f64,
pub storage_violation_below_cost: f64,
pub filling_target_violation_cost: f64,
pub turbined_violation_below_cost: f64,
pub outflow_violation_below_cost: f64,
pub outflow_violation_above_cost: f64,
pub generation_violation_below_cost: f64,
pub evaporation_violation_cost: f64,
pub water_withdrawal_violation_cost: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BusStagePenalties {
pub excess_cost: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LineStagePenalties {
pub exchange_cost: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NcsStagePenalties {
pub curtailment_cost: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HydroStageBounds {
pub min_storage_hm3: f64,
pub max_storage_hm3: f64,
pub min_turbined_m3s: f64,
pub max_turbined_m3s: f64,
pub min_outflow_m3s: f64,
pub max_outflow_m3s: Option<f64>,
pub min_generation_mw: f64,
pub max_generation_mw: f64,
pub max_diversion_m3s: Option<f64>,
pub filling_inflow_m3s: f64,
pub water_withdrawal_m3s: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ThermalStageBounds {
pub min_generation_mw: f64,
pub max_generation_mw: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LineStageBounds {
pub direct_mw: f64,
pub reverse_mw: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PumpingStageBounds {
pub min_flow_m3s: f64,
pub max_flow_m3s: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContractStageBounds {
pub min_mw: f64,
pub max_mw: f64,
pub price_per_mwh: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResolvedPenalties {
n_stages: usize,
hydro: Vec<HydroStagePenalties>,
bus: Vec<BusStagePenalties>,
line: Vec<LineStagePenalties>,
ncs: Vec<NcsStagePenalties>,
}
impl ResolvedPenalties {
#[must_use]
pub fn empty() -> Self {
Self {
n_stages: 0,
hydro: Vec::new(),
bus: Vec::new(),
line: Vec::new(),
ncs: Vec::new(),
}
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
n_hydros: usize,
n_buses: usize,
n_lines: usize,
n_ncs: usize,
n_stages: usize,
hydro_default: HydroStagePenalties,
bus_default: BusStagePenalties,
line_default: LineStagePenalties,
ncs_default: NcsStagePenalties,
) -> Self {
Self {
n_stages,
hydro: vec![hydro_default; n_hydros * n_stages],
bus: vec![bus_default; n_buses * n_stages],
line: vec![line_default; n_lines * n_stages],
ncs: vec![ncs_default; n_ncs * n_stages],
}
}
#[inline]
#[must_use]
pub fn hydro_penalties(&self, hydro_index: usize, stage_index: usize) -> HydroStagePenalties {
self.hydro[hydro_index * self.n_stages + stage_index]
}
#[inline]
#[must_use]
pub fn bus_penalties(&self, bus_index: usize, stage_index: usize) -> BusStagePenalties {
self.bus[bus_index * self.n_stages + stage_index]
}
#[inline]
#[must_use]
pub fn line_penalties(&self, line_index: usize, stage_index: usize) -> LineStagePenalties {
self.line[line_index * self.n_stages + stage_index]
}
#[inline]
#[must_use]
pub fn ncs_penalties(&self, ncs_index: usize, stage_index: usize) -> NcsStagePenalties {
self.ncs[ncs_index * self.n_stages + stage_index]
}
#[inline]
pub fn hydro_penalties_mut(
&mut self,
hydro_index: usize,
stage_index: usize,
) -> &mut HydroStagePenalties {
let idx = hydro_index * self.n_stages + stage_index;
&mut self.hydro[idx]
}
#[inline]
pub fn bus_penalties_mut(
&mut self,
bus_index: usize,
stage_index: usize,
) -> &mut BusStagePenalties {
let idx = bus_index * self.n_stages + stage_index;
&mut self.bus[idx]
}
#[inline]
pub fn line_penalties_mut(
&mut self,
line_index: usize,
stage_index: usize,
) -> &mut LineStagePenalties {
let idx = line_index * self.n_stages + stage_index;
&mut self.line[idx]
}
#[inline]
pub fn ncs_penalties_mut(
&mut self,
ncs_index: usize,
stage_index: usize,
) -> &mut NcsStagePenalties {
let idx = ncs_index * self.n_stages + stage_index;
&mut self.ncs[idx]
}
#[inline]
#[must_use]
pub fn n_stages(&self) -> usize {
self.n_stages
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResolvedBounds {
n_stages: usize,
hydro: Vec<HydroStageBounds>,
thermal: Vec<ThermalStageBounds>,
line: Vec<LineStageBounds>,
pumping: Vec<PumpingStageBounds>,
contract: Vec<ContractStageBounds>,
}
impl ResolvedBounds {
#[must_use]
pub fn empty() -> Self {
Self {
n_stages: 0,
hydro: Vec::new(),
thermal: Vec::new(),
line: Vec::new(),
pumping: Vec::new(),
contract: Vec::new(),
}
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
n_hydros: usize,
n_thermals: usize,
n_lines: usize,
n_pumping: usize,
n_contracts: usize,
n_stages: usize,
hydro_default: HydroStageBounds,
thermal_default: ThermalStageBounds,
line_default: LineStageBounds,
pumping_default: PumpingStageBounds,
contract_default: ContractStageBounds,
) -> Self {
Self {
n_stages,
hydro: vec![hydro_default; n_hydros * n_stages],
thermal: vec![thermal_default; n_thermals * n_stages],
line: vec![line_default; n_lines * n_stages],
pumping: vec![pumping_default; n_pumping * n_stages],
contract: vec![contract_default; n_contracts * n_stages],
}
}
#[inline]
#[must_use]
pub fn hydro_bounds(&self, hydro_index: usize, stage_index: usize) -> &HydroStageBounds {
&self.hydro[hydro_index * self.n_stages + stage_index]
}
#[inline]
#[must_use]
pub fn thermal_bounds(&self, thermal_index: usize, stage_index: usize) -> ThermalStageBounds {
self.thermal[thermal_index * self.n_stages + stage_index]
}
#[inline]
#[must_use]
pub fn line_bounds(&self, line_index: usize, stage_index: usize) -> LineStageBounds {
self.line[line_index * self.n_stages + stage_index]
}
#[inline]
#[must_use]
pub fn pumping_bounds(&self, pumping_index: usize, stage_index: usize) -> PumpingStageBounds {
self.pumping[pumping_index * self.n_stages + stage_index]
}
#[inline]
#[must_use]
pub fn contract_bounds(
&self,
contract_index: usize,
stage_index: usize,
) -> ContractStageBounds {
self.contract[contract_index * self.n_stages + stage_index]
}
#[inline]
pub fn hydro_bounds_mut(
&mut self,
hydro_index: usize,
stage_index: usize,
) -> &mut HydroStageBounds {
let idx = hydro_index * self.n_stages + stage_index;
&mut self.hydro[idx]
}
#[inline]
pub fn thermal_bounds_mut(
&mut self,
thermal_index: usize,
stage_index: usize,
) -> &mut ThermalStageBounds {
let idx = thermal_index * self.n_stages + stage_index;
&mut self.thermal[idx]
}
#[inline]
pub fn line_bounds_mut(
&mut self,
line_index: usize,
stage_index: usize,
) -> &mut LineStageBounds {
let idx = line_index * self.n_stages + stage_index;
&mut self.line[idx]
}
#[inline]
pub fn pumping_bounds_mut(
&mut self,
pumping_index: usize,
stage_index: usize,
) -> &mut PumpingStageBounds {
let idx = pumping_index * self.n_stages + stage_index;
&mut self.pumping[idx]
}
#[inline]
pub fn contract_bounds_mut(
&mut self,
contract_index: usize,
stage_index: usize,
) -> &mut ContractStageBounds {
let idx = contract_index * self.n_stages + stage_index;
&mut self.contract[idx]
}
#[inline]
#[must_use]
pub fn n_stages(&self) -> usize {
self.n_stages
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedGenericConstraintBounds {
index: HashMap<(usize, i32), Range<usize>>,
entries: Vec<(Option<i32>, f64)>,
}
#[cfg(feature = "serde")]
mod serde_generic_bounds {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::ResolvedGenericConstraintBounds;
#[derive(Serialize, Deserialize)]
struct WireEntry {
constraint_idx: usize,
stage_id: i32,
pairs: Vec<(Option<i32>, f64)>,
}
impl Serialize for ResolvedGenericConstraintBounds {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut keys: Vec<(usize, i32)> = self.index.keys().copied().collect();
keys.sort_unstable();
let wire: Vec<WireEntry> = keys
.into_iter()
.map(|(constraint_idx, stage_id)| {
let range = self.index[&(constraint_idx, stage_id)].clone();
WireEntry {
constraint_idx,
stage_id,
pairs: self.entries[range].to_vec(),
}
})
.collect();
wire.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ResolvedGenericConstraintBounds {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = Vec::<WireEntry>::deserialize(deserializer)?;
let mut index = std::collections::HashMap::new();
let mut entries = Vec::new();
for entry in wire {
let start = entries.len();
entries.extend_from_slice(&entry.pairs);
let end = entries.len();
if end > start {
index.insert((entry.constraint_idx, entry.stage_id), start..end);
}
}
Ok(ResolvedGenericConstraintBounds { index, entries })
}
}
}
impl ResolvedGenericConstraintBounds {
#[must_use]
pub fn empty() -> Self {
Self {
index: HashMap::new(),
entries: Vec::new(),
}
}
pub fn new<I>(constraint_id_to_idx: &HashMap<i32, usize>, raw_bounds: I) -> Self
where
I: Iterator<Item = (i32, i32, Option<i32>, f64)>,
{
let mut index: HashMap<(usize, i32), Range<usize>> = HashMap::new();
let mut entries: Vec<(Option<i32>, f64)> = Vec::new();
let mut current_key: Option<(usize, i32)> = None;
let mut range_start: usize = 0;
for (constraint_id, stage_id, block_id, bound) in raw_bounds {
let Some(&constraint_idx) = constraint_id_to_idx.get(&constraint_id) else {
continue;
};
let key = (constraint_idx, stage_id);
if current_key != Some(key) {
if let Some(prev_key) = current_key {
let range_end = entries.len();
if range_end > range_start {
index.insert(prev_key, range_start..range_end);
}
}
range_start = entries.len();
current_key = Some(key);
}
entries.push((block_id, bound));
}
if let Some(last_key) = current_key {
let range_end = entries.len();
if range_end > range_start {
index.insert(last_key, range_start..range_end);
}
}
Self { index, entries }
}
#[inline]
#[must_use]
pub fn is_active(&self, constraint_idx: usize, stage_id: i32) -> bool {
self.index.contains_key(&(constraint_idx, stage_id))
}
#[inline]
#[must_use]
pub fn bounds_for_stage(&self, constraint_idx: usize, stage_id: i32) -> &[(Option<i32>, f64)] {
match self.index.get(&(constraint_idx, stage_id)) {
Some(range) => &self.entries[range.clone()],
None => &[],
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResolvedLoadFactors {
factors: Vec<f64>,
n_stages: usize,
max_blocks: usize,
}
impl ResolvedLoadFactors {
#[must_use]
pub fn empty() -> Self {
Self {
factors: Vec::new(),
n_stages: 0,
max_blocks: 0,
}
}
#[must_use]
pub fn new(n_buses: usize, n_stages: usize, max_blocks: usize) -> Self {
Self {
factors: vec![1.0; n_buses * n_stages * max_blocks],
n_stages,
max_blocks,
}
}
pub fn set(&mut self, bus_idx: usize, stage_idx: usize, block_idx: usize, value: f64) {
let idx = (bus_idx * self.n_stages + stage_idx) * self.max_blocks + block_idx;
self.factors[idx] = value;
}
#[inline]
#[must_use]
pub fn factor(&self, bus_idx: usize, stage_idx: usize, block_idx: usize) -> f64 {
if self.factors.is_empty() {
return 1.0;
}
let idx = (bus_idx * self.n_stages + stage_idx) * self.max_blocks + block_idx;
self.factors.get(idx).copied().unwrap_or(1.0)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResolvedExchangeFactors {
data: Vec<(f64, f64)>,
n_stages: usize,
max_blocks: usize,
}
impl ResolvedExchangeFactors {
#[must_use]
pub fn empty() -> Self {
Self {
data: Vec::new(),
n_stages: 0,
max_blocks: 0,
}
}
#[must_use]
pub fn new(n_lines: usize, n_stages: usize, max_blocks: usize) -> Self {
Self {
data: vec![(1.0, 1.0); n_lines * n_stages * max_blocks],
n_stages,
max_blocks,
}
}
pub fn set(
&mut self,
line_idx: usize,
stage_idx: usize,
block_idx: usize,
direct_factor: f64,
reverse_factor: f64,
) {
let idx = (line_idx * self.n_stages + stage_idx) * self.max_blocks + block_idx;
self.data[idx] = (direct_factor, reverse_factor);
}
#[inline]
#[must_use]
pub fn factors(&self, line_idx: usize, stage_idx: usize, block_idx: usize) -> (f64, f64) {
if self.data.is_empty() {
return (1.0, 1.0);
}
let idx = (line_idx * self.n_stages + stage_idx) * self.max_blocks + block_idx;
self.data.get(idx).copied().unwrap_or((1.0, 1.0))
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResolvedNcsBounds {
data: Vec<f64>,
n_stages: usize,
}
impl ResolvedNcsBounds {
#[must_use]
pub fn empty() -> Self {
Self {
data: Vec::new(),
n_stages: 0,
}
}
#[must_use]
pub fn new(n_ncs: usize, n_stages: usize, default_mw: &[f64]) -> Self {
assert!(
default_mw.len() == n_ncs,
"default_mw length ({}) must equal n_ncs ({n_ncs})",
default_mw.len()
);
let mut data = vec![0.0; n_ncs * n_stages];
for (ncs_idx, &mw) in default_mw.iter().enumerate() {
for stage_idx in 0..n_stages {
data[ncs_idx * n_stages + stage_idx] = mw;
}
}
Self { data, n_stages }
}
pub fn set(&mut self, ncs_idx: usize, stage_idx: usize, value: f64) {
let idx = ncs_idx * self.n_stages + stage_idx;
self.data[idx] = value;
}
#[inline]
#[must_use]
pub fn available_generation(&self, ncs_idx: usize, stage_idx: usize) -> f64 {
if self.data.is_empty() {
return 0.0;
}
let idx = ncs_idx * self.n_stages + stage_idx;
self.data.get(idx).copied().unwrap_or(0.0)
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResolvedNcsFactors {
factors: Vec<f64>,
n_stages: usize,
max_blocks: usize,
}
impl ResolvedNcsFactors {
#[must_use]
pub fn empty() -> Self {
Self {
factors: Vec::new(),
n_stages: 0,
max_blocks: 0,
}
}
#[must_use]
pub fn new(n_ncs: usize, n_stages: usize, max_blocks: usize) -> Self {
Self {
factors: vec![1.0; n_ncs * n_stages * max_blocks],
n_stages,
max_blocks,
}
}
pub fn set(&mut self, ncs_idx: usize, stage_idx: usize, block_idx: usize, value: f64) {
let idx = (ncs_idx * self.n_stages + stage_idx) * self.max_blocks + block_idx;
self.factors[idx] = value;
}
#[inline]
#[must_use]
pub fn factor(&self, ncs_idx: usize, stage_idx: usize, block_idx: usize) -> f64 {
if self.factors.is_empty() {
return 1.0;
}
let idx = (ncs_idx * self.n_stages + stage_idx) * self.max_blocks + block_idx;
self.factors.get(idx).copied().unwrap_or(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_hydro_penalties() -> HydroStagePenalties {
HydroStagePenalties {
spillage_cost: 0.01,
diversion_cost: 0.02,
fpha_turbined_cost: 0.03,
storage_violation_below_cost: 1000.0,
filling_target_violation_cost: 5000.0,
turbined_violation_below_cost: 500.0,
outflow_violation_below_cost: 400.0,
outflow_violation_above_cost: 300.0,
generation_violation_below_cost: 200.0,
evaporation_violation_cost: 150.0,
water_withdrawal_violation_cost: 100.0,
}
}
fn make_hydro_bounds() -> HydroStageBounds {
HydroStageBounds {
min_storage_hm3: 10.0,
max_storage_hm3: 200.0,
min_turbined_m3s: 0.0,
max_turbined_m3s: 500.0,
min_outflow_m3s: 5.0,
max_outflow_m3s: None,
min_generation_mw: 0.0,
max_generation_mw: 100.0,
max_diversion_m3s: None,
filling_inflow_m3s: 0.0,
water_withdrawal_m3s: 0.0,
}
}
#[test]
fn test_hydro_stage_penalties_copy() {
let p = make_hydro_penalties();
let q = p;
let r = p;
assert_eq!(q, r);
assert!((q.spillage_cost - p.spillage_cost).abs() < f64::EPSILON);
}
#[test]
fn test_all_penalty_structs_are_copy() {
let bp = BusStagePenalties { excess_cost: 1.0 };
let lp = LineStagePenalties { exchange_cost: 2.0 };
let np = NcsStagePenalties {
curtailment_cost: 3.0,
};
assert_eq!(bp, bp);
assert_eq!(lp, lp);
assert_eq!(np, np);
let bp2 = bp;
let lp2 = lp;
let np2 = np;
assert!((bp2.excess_cost - 1.0).abs() < f64::EPSILON);
assert!((lp2.exchange_cost - 2.0).abs() < f64::EPSILON);
assert!((np2.curtailment_cost - 3.0).abs() < f64::EPSILON);
}
#[test]
fn test_all_bound_structs_are_copy() {
let hb = make_hydro_bounds();
let tb = ThermalStageBounds {
min_generation_mw: 0.0,
max_generation_mw: 100.0,
};
let lb = LineStageBounds {
direct_mw: 500.0,
reverse_mw: 500.0,
};
let pb = PumpingStageBounds {
min_flow_m3s: 0.0,
max_flow_m3s: 20.0,
};
let cb = ContractStageBounds {
min_mw: 0.0,
max_mw: 50.0,
price_per_mwh: 80.0,
};
let hb2 = hb;
let tb2 = tb;
let lb2 = lb;
let pb2 = pb;
let cb2 = cb;
assert_eq!(hb, hb2);
assert_eq!(tb, tb2);
assert_eq!(lb, lb2);
assert_eq!(pb, pb2);
assert_eq!(cb, cb2);
}
#[test]
fn test_resolved_penalties_construction() {
let hp = make_hydro_penalties();
let bp = BusStagePenalties { excess_cost: 100.0 };
let lp = LineStagePenalties { exchange_cost: 5.0 };
let np = NcsStagePenalties {
curtailment_cost: 50.0,
};
let table = ResolvedPenalties::new(2, 1, 1, 1, 3, hp, bp, lp, np);
for hydro_idx in 0..2 {
for stage_idx in 0..3 {
let p = table.hydro_penalties(hydro_idx, stage_idx);
assert!((p.spillage_cost - 0.01).abs() < f64::EPSILON);
assert!((p.storage_violation_below_cost - 1000.0).abs() < f64::EPSILON);
}
}
assert!((table.bus_penalties(0, 0).excess_cost - 100.0).abs() < f64::EPSILON);
assert!((table.line_penalties(0, 1).exchange_cost - 5.0).abs() < f64::EPSILON);
assert!((table.ncs_penalties(0, 2).curtailment_cost - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_resolved_penalties_indexed_access() {
let hp = make_hydro_penalties();
let bp = BusStagePenalties { excess_cost: 10.0 };
let lp = LineStagePenalties { exchange_cost: 1.0 };
let np = NcsStagePenalties {
curtailment_cost: 5.0,
};
let table = ResolvedPenalties::new(3, 0, 0, 0, 5, hp, bp, lp, np);
assert_eq!(table.n_stages(), 5);
let p = table.hydro_penalties(1, 3);
assert!((p.diversion_cost - 0.02).abs() < f64::EPSILON);
assert!((p.filling_target_violation_cost - 5000.0).abs() < f64::EPSILON);
}
#[test]
fn test_resolved_penalties_mutable_update() {
let hp = make_hydro_penalties();
let bp = BusStagePenalties { excess_cost: 10.0 };
let lp = LineStagePenalties { exchange_cost: 1.0 };
let np = NcsStagePenalties {
curtailment_cost: 5.0,
};
let mut table = ResolvedPenalties::new(2, 2, 1, 1, 3, hp, bp, lp, np);
table.hydro_penalties_mut(0, 1).spillage_cost = 99.0;
assert!((table.hydro_penalties(0, 1).spillage_cost - 99.0).abs() < f64::EPSILON);
assert!((table.hydro_penalties(0, 0).spillage_cost - 0.01).abs() < f64::EPSILON);
assert!((table.hydro_penalties(1, 1).spillage_cost - 0.01).abs() < f64::EPSILON);
table.bus_penalties_mut(1, 2).excess_cost = 999.0;
assert!((table.bus_penalties(1, 2).excess_cost - 999.0).abs() < f64::EPSILON);
assert!((table.bus_penalties(0, 2).excess_cost - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_resolved_bounds_construction() {
let hb = make_hydro_bounds();
let tb = ThermalStageBounds {
min_generation_mw: 50.0,
max_generation_mw: 400.0,
};
let lb = LineStageBounds {
direct_mw: 1000.0,
reverse_mw: 800.0,
};
let pb = PumpingStageBounds {
min_flow_m3s: 0.0,
max_flow_m3s: 20.0,
};
let cb = ContractStageBounds {
min_mw: 0.0,
max_mw: 100.0,
price_per_mwh: 80.0,
};
let table = ResolvedBounds::new(1, 2, 1, 1, 1, 3, hb, tb, lb, pb, cb);
let b = table.hydro_bounds(0, 2);
assert!((b.min_storage_hm3 - 10.0).abs() < f64::EPSILON);
assert!((b.max_storage_hm3 - 200.0).abs() < f64::EPSILON);
assert!(b.max_outflow_m3s.is_none());
assert!(b.max_diversion_m3s.is_none());
let t0 = table.thermal_bounds(0, 0);
let t1 = table.thermal_bounds(1, 2);
assert!((t0.max_generation_mw - 400.0).abs() < f64::EPSILON);
assert!((t1.min_generation_mw - 50.0).abs() < f64::EPSILON);
assert!((table.line_bounds(0, 1).direct_mw - 1000.0).abs() < f64::EPSILON);
assert!((table.pumping_bounds(0, 0).max_flow_m3s - 20.0).abs() < f64::EPSILON);
assert!((table.contract_bounds(0, 2).price_per_mwh - 80.0).abs() < f64::EPSILON);
}
#[test]
fn test_resolved_bounds_mutable_update() {
let hb = make_hydro_bounds();
let tb = ThermalStageBounds {
min_generation_mw: 0.0,
max_generation_mw: 200.0,
};
let lb = LineStageBounds {
direct_mw: 500.0,
reverse_mw: 500.0,
};
let pb = PumpingStageBounds {
min_flow_m3s: 0.0,
max_flow_m3s: 30.0,
};
let cb = ContractStageBounds {
min_mw: 0.0,
max_mw: 50.0,
price_per_mwh: 60.0,
};
let mut table = ResolvedBounds::new(2, 1, 1, 1, 1, 3, hb, tb, lb, pb, cb);
let cell = table.hydro_bounds_mut(1, 0);
cell.min_storage_hm3 = 25.0;
cell.max_outflow_m3s = Some(1000.0);
assert!((table.hydro_bounds(1, 0).min_storage_hm3 - 25.0).abs() < f64::EPSILON);
assert_eq!(table.hydro_bounds(1, 0).max_outflow_m3s, Some(1000.0));
assert!((table.hydro_bounds(0, 0).min_storage_hm3 - 10.0).abs() < f64::EPSILON);
assert!(table.hydro_bounds(1, 1).max_outflow_m3s.is_none());
table.thermal_bounds_mut(0, 2).max_generation_mw = 150.0;
assert!((table.thermal_bounds(0, 2).max_generation_mw - 150.0).abs() < f64::EPSILON);
assert!((table.thermal_bounds(0, 0).max_generation_mw - 200.0).abs() < f64::EPSILON);
}
#[test]
fn test_hydro_stage_bounds_has_eleven_fields() {
let b = HydroStageBounds {
min_storage_hm3: 1.0,
max_storage_hm3: 2.0,
min_turbined_m3s: 3.0,
max_turbined_m3s: 4.0,
min_outflow_m3s: 5.0,
max_outflow_m3s: Some(6.0),
min_generation_mw: 7.0,
max_generation_mw: 8.0,
max_diversion_m3s: Some(9.0),
filling_inflow_m3s: 10.0,
water_withdrawal_m3s: 11.0,
};
assert!((b.min_storage_hm3 - 1.0).abs() < f64::EPSILON);
assert!((b.water_withdrawal_m3s - 11.0).abs() < f64::EPSILON);
assert_eq!(b.max_outflow_m3s, Some(6.0));
assert_eq!(b.max_diversion_m3s, Some(9.0));
}
#[test]
#[cfg(feature = "serde")]
fn test_resolved_penalties_serde_roundtrip() {
let hp = make_hydro_penalties();
let bp = BusStagePenalties { excess_cost: 100.0 };
let lp = LineStagePenalties { exchange_cost: 5.0 };
let np = NcsStagePenalties {
curtailment_cost: 50.0,
};
let original = ResolvedPenalties::new(2, 1, 1, 1, 3, hp, bp, lp, np);
let json = serde_json::to_string(&original).expect("serialize");
let restored: ResolvedPenalties = serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, restored);
}
#[test]
#[cfg(feature = "serde")]
fn test_resolved_bounds_serde_roundtrip() {
let hb = make_hydro_bounds();
let tb = ThermalStageBounds {
min_generation_mw: 0.0,
max_generation_mw: 100.0,
};
let lb = LineStageBounds {
direct_mw: 500.0,
reverse_mw: 500.0,
};
let pb = PumpingStageBounds {
min_flow_m3s: 0.0,
max_flow_m3s: 20.0,
};
let cb = ContractStageBounds {
min_mw: 0.0,
max_mw: 50.0,
price_per_mwh: 80.0,
};
let original = ResolvedBounds::new(1, 1, 1, 1, 1, 3, hb, tb, lb, pb, cb);
let json = serde_json::to_string(&original).expect("serialize");
let restored: ResolvedBounds = serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, restored);
}
#[test]
fn test_generic_bounds_empty() {
let t = ResolvedGenericConstraintBounds::empty();
assert!(!t.is_active(0, 0));
assert!(!t.is_active(99, -1));
assert!(t.bounds_for_stage(0, 0).is_empty());
assert!(t.bounds_for_stage(99, 5).is_empty());
}
#[test]
fn test_generic_bounds_sparse_active() {
let id_map: HashMap<i32, usize> = [(0, 0), (1, 1)].into_iter().collect();
let rows = vec![(0i32, 0i32, None::<i32>, 100.0f64)];
let t = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
assert!(t.is_active(0, 0), "constraint 0 at stage 0 must be active");
assert!(
!t.is_active(1, 0),
"constraint 1 at stage 0 must not be active"
);
assert!(
!t.is_active(0, 1),
"constraint 0 at stage 1 must not be active"
);
}
#[test]
fn test_generic_bounds_single_block_none() {
let id_map: HashMap<i32, usize> = [(0, 0)].into_iter().collect();
let rows = vec![(0i32, 0i32, None::<i32>, 100.0f64)];
let t = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
let slice = t.bounds_for_stage(0, 0);
assert_eq!(slice.len(), 1);
assert_eq!(slice[0], (None, 100.0));
}
#[test]
fn test_generic_bounds_multiple_blocks() {
let id_map: HashMap<i32, usize> = [(0, 0)].into_iter().collect();
let rows = vec![
(0i32, 2i32, None::<i32>, 50.0f64),
(0i32, 2i32, Some(0i32), 60.0f64),
(0i32, 2i32, Some(1i32), 70.0f64),
];
let t = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
assert!(t.is_active(0, 2));
let slice = t.bounds_for_stage(0, 2);
assert_eq!(slice.len(), 3);
assert_eq!(slice[0], (None, 50.0));
assert_eq!(slice[1], (Some(0), 60.0));
assert_eq!(slice[2], (Some(1), 70.0));
}
#[test]
fn test_generic_bounds_unknown_constraint_id_skipped() {
let id_map: HashMap<i32, usize> = [(0, 0)].into_iter().collect();
let rows = vec![(99i32, 0i32, None::<i32>, 1000.0f64)];
let t = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
assert!(!t.is_active(0, 0), "unknown constraint_id must be skipped");
assert!(t.bounds_for_stage(0, 0).is_empty());
}
#[test]
fn test_generic_bounds_no_rows() {
let id_map: HashMap<i32, usize> = [(0, 0), (1, 1)].into_iter().collect();
let t = ResolvedGenericConstraintBounds::new(&id_map, std::iter::empty());
assert!(!t.is_active(0, 0));
assert!(!t.is_active(1, 0));
assert!(t.bounds_for_stage(0, 0).is_empty());
}
#[test]
fn test_generic_bounds_two_stages_one_constraint() {
let id_map: HashMap<i32, usize> = [(0, 0), (1, 1)].into_iter().collect();
let rows = vec![
(0i32, 0i32, None::<i32>, 100.0f64),
(0i32, 1i32, None::<i32>, 200.0f64),
];
let t = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
assert!(t.is_active(0, 0));
assert!(t.is_active(0, 1));
assert!(!t.is_active(1, 0));
assert!(!t.is_active(1, 1));
let s0 = t.bounds_for_stage(0, 0);
assert_eq!(s0.len(), 1);
assert!((s0[0].1 - 100.0).abs() < f64::EPSILON);
let s1 = t.bounds_for_stage(0, 1);
assert_eq!(s1.len(), 1);
assert!((s1[0].1 - 200.0).abs() < f64::EPSILON);
}
#[test]
#[cfg(feature = "serde")]
fn test_generic_bounds_serde_roundtrip() {
let id_map: HashMap<i32, usize> = [(0, 0), (1, 1)].into_iter().collect();
let rows = vec![
(0i32, 0i32, None::<i32>, 100.0f64),
(0i32, 0i32, Some(1i32), 150.0f64),
(1i32, 2i32, None::<i32>, 300.0f64),
];
let original = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
let json = serde_json::to_string(&original).expect("serialize");
let restored: ResolvedGenericConstraintBounds =
serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, restored);
}
#[test]
fn test_load_factors_empty_returns_one() {
let t = ResolvedLoadFactors::empty();
assert!((t.factor(0, 0, 0) - 1.0).abs() < f64::EPSILON);
assert!((t.factor(5, 3, 2) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_load_factors_new_default_is_one() {
let t = ResolvedLoadFactors::new(2, 1, 3);
for bus in 0..2 {
for blk in 0..3 {
assert!(
(t.factor(bus, 0, blk) - 1.0).abs() < f64::EPSILON,
"expected 1.0 at ({bus}, 0, {blk})"
);
}
}
}
#[test]
fn test_load_factors_set_and_get() {
let mut t = ResolvedLoadFactors::new(2, 1, 3);
t.set(0, 0, 0, 0.85);
t.set(0, 0, 1, 1.15);
assert!((t.factor(0, 0, 0) - 0.85).abs() < 1e-10);
assert!((t.factor(0, 0, 1) - 1.15).abs() < 1e-10);
assert!((t.factor(0, 0, 2) - 1.0).abs() < f64::EPSILON);
assert!((t.factor(1, 0, 0) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_load_factors_out_of_bounds_returns_one() {
let t = ResolvedLoadFactors::new(1, 1, 2);
assert!((t.factor(5, 0, 0) - 1.0).abs() < f64::EPSILON);
assert!((t.factor(0, 0, 99) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_exchange_factors_empty_returns_one_one() {
let t = ResolvedExchangeFactors::empty();
assert_eq!(t.factors(0, 0, 0), (1.0, 1.0));
assert_eq!(t.factors(5, 3, 2), (1.0, 1.0));
}
#[test]
fn test_exchange_factors_new_default_is_one_one() {
let t = ResolvedExchangeFactors::new(1, 1, 2);
assert_eq!(t.factors(0, 0, 0), (1.0, 1.0));
assert_eq!(t.factors(0, 0, 1), (1.0, 1.0));
}
#[test]
fn test_exchange_factors_set_and_get() {
let mut t = ResolvedExchangeFactors::new(1, 1, 2);
t.set(0, 0, 0, 0.9, 0.85);
assert_eq!(t.factors(0, 0, 0), (0.9, 0.85));
assert_eq!(t.factors(0, 0, 1), (1.0, 1.0));
}
#[test]
fn test_exchange_factors_out_of_bounds_returns_default() {
let t = ResolvedExchangeFactors::new(1, 1, 1);
assert_eq!(t.factors(5, 0, 0), (1.0, 1.0));
}
#[test]
fn test_ncs_bounds_empty_is_empty() {
let t = ResolvedNcsBounds::empty();
assert!(t.is_empty());
assert!((t.available_generation(0, 0) - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_ncs_bounds_new_uses_defaults() {
let t = ResolvedNcsBounds::new(2, 3, &[100.0, 200.0]);
assert!(!t.is_empty());
assert!((t.available_generation(0, 0) - 100.0).abs() < f64::EPSILON);
assert!((t.available_generation(0, 2) - 100.0).abs() < f64::EPSILON);
assert!((t.available_generation(1, 0) - 200.0).abs() < f64::EPSILON);
assert!((t.available_generation(1, 2) - 200.0).abs() < f64::EPSILON);
}
#[test]
fn test_ncs_bounds_set_and_get() {
let mut t = ResolvedNcsBounds::new(2, 3, &[100.0, 200.0]);
t.set(0, 1, 50.0);
assert!((t.available_generation(0, 1) - 50.0).abs() < f64::EPSILON);
assert!((t.available_generation(0, 0) - 100.0).abs() < f64::EPSILON);
assert!((t.available_generation(1, 0) - 200.0).abs() < f64::EPSILON);
}
#[test]
fn test_ncs_bounds_out_of_bounds_returns_zero() {
let t = ResolvedNcsBounds::new(1, 1, &[100.0]);
assert!((t.available_generation(5, 0) - 0.0).abs() < f64::EPSILON);
assert!((t.available_generation(0, 99) - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_ncs_factors_empty_returns_one() {
let t = ResolvedNcsFactors::empty();
assert!((t.factor(0, 0, 0) - 1.0).abs() < f64::EPSILON);
assert!((t.factor(5, 3, 2) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_ncs_factors_new_default_is_one() {
let t = ResolvedNcsFactors::new(2, 1, 3);
for ncs in 0..2 {
for blk in 0..3 {
assert!(
(t.factor(ncs, 0, blk) - 1.0).abs() < f64::EPSILON,
"factor({ncs}, 0, {blk}) should be 1.0"
);
}
}
}
#[test]
fn test_ncs_factors_set_and_get() {
let mut t = ResolvedNcsFactors::new(2, 1, 3);
t.set(0, 0, 1, 0.8);
assert!((t.factor(0, 0, 1) - 0.8).abs() < 1e-10);
assert!((t.factor(0, 0, 0) - 1.0).abs() < f64::EPSILON);
assert!((t.factor(1, 0, 0) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_ncs_factors_out_of_bounds_returns_one() {
let t = ResolvedNcsFactors::new(1, 1, 2);
assert!((t.factor(5, 0, 0) - 1.0).abs() < f64::EPSILON);
assert!((t.factor(0, 0, 99) - 1.0).abs() < f64::EPSILON);
}
}