use std::collections::BTreeMap;
use crate::EntityId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SamplingScheme {
InSample,
OutOfSample,
External,
Historical,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScenarioSource {
pub inflow_scheme: SamplingScheme,
pub load_scheme: SamplingScheme,
pub ncs_scheme: SamplingScheme,
pub seed: Option<i64>,
pub historical_years: Option<HistoricalYears>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum HistoricalYears {
List(Vec<i32>),
Range {
from: i32,
to: i32,
},
}
impl HistoricalYears {
#[must_use]
pub fn to_years(&self) -> Vec<i32> {
match self {
HistoricalYears::List(years) => years.clone(),
HistoricalYears::Range { from, to } => (*from..=*to).collect(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnnualComponent {
pub coefficient: f64,
pub mean_m3s: f64,
pub std_m3s: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InflowModel {
pub hydro_id: EntityId,
pub stage_id: i32,
pub mean_m3s: f64,
pub std_m3s: f64,
pub ar_coefficients: Vec<f64>,
pub residual_std_ratio: f64,
pub annual: Option<AnnualComponent>,
}
impl InflowModel {
#[must_use]
pub fn ar_order(&self) -> usize {
self.ar_coefficients.len()
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LoadModel {
pub bus_id: EntityId,
pub stage_id: i32,
pub mean_mw: f64,
pub std_mw: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NcsModel {
pub ncs_id: EntityId,
pub stage_id: i32,
pub mean: f64,
pub std: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InflowHistoryRow {
pub hydro_id: EntityId,
pub date: chrono::NaiveDate,
pub value_m3s: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExternalScenarioRow {
pub stage_id: i32,
pub scenario_id: i32,
pub hydro_id: EntityId,
pub value_m3s: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExternalLoadRow {
pub stage_id: i32,
pub scenario_id: i32,
pub bus_id: EntityId,
pub value_mw: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExternalNcsRow {
pub stage_id: i32,
pub scenario_id: i32,
pub ncs_id: EntityId,
pub value: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CorrelationEntity {
pub entity_type: String,
pub id: EntityId,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CorrelationGroup {
pub name: String,
pub entities: Vec<CorrelationEntity>,
pub matrix: Vec<Vec<f64>>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CorrelationProfile {
pub groups: Vec<CorrelationGroup>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CorrelationScheduleEntry {
pub stage_id: i32,
pub profile_name: String,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CorrelationModel {
pub method: String,
pub profiles: BTreeMap<String, CorrelationProfile>,
pub schedule: Vec<CorrelationScheduleEntry>,
}
impl Default for ScenarioSource {
fn default() -> Self {
Self {
inflow_scheme: SamplingScheme::InSample,
load_scheme: SamplingScheme::InSample,
ncs_scheme: SamplingScheme::InSample,
seed: None,
historical_years: None,
}
}
}
impl Default for CorrelationModel {
fn default() -> Self {
Self {
method: "spectral".to_string(),
profiles: BTreeMap::new(),
schedule: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
#[cfg(feature = "serde")]
use super::ScenarioSource;
use super::{
AnnualComponent, CorrelationEntity, CorrelationGroup, CorrelationModel, CorrelationProfile,
CorrelationScheduleEntry, InflowModel, NcsModel, SamplingScheme,
};
use crate::EntityId;
#[test]
fn test_inflow_model_construction() {
let model = InflowModel {
hydro_id: EntityId(7),
stage_id: 11,
mean_m3s: 250.0,
std_m3s: 55.0,
ar_coefficients: vec![0.5, 0.2, 0.1],
residual_std_ratio: 0.85,
annual: None,
};
assert_eq!(model.hydro_id, EntityId(7));
assert_eq!(model.stage_id, 11);
assert_eq!(model.mean_m3s, 250.0);
assert_eq!(model.std_m3s, 55.0);
assert_eq!(model.ar_order(), 3);
assert_eq!(model.ar_coefficients, vec![0.5, 0.2, 0.1]);
assert_eq!(model.ar_coefficients.len(), model.ar_order());
assert!((model.residual_std_ratio - 0.85).abs() < f64::EPSILON);
}
#[test]
fn test_inflow_model_ar_order_method() {
let white_noise = InflowModel {
hydro_id: EntityId(1),
stage_id: 0,
mean_m3s: 100.0,
std_m3s: 10.0,
ar_coefficients: vec![],
residual_std_ratio: 1.0,
annual: None,
};
assert_eq!(white_noise.ar_order(), 0);
let par2 = InflowModel {
hydro_id: EntityId(2),
stage_id: 1,
mean_m3s: 200.0,
std_m3s: 20.0,
ar_coefficients: vec![0.45, 0.22],
residual_std_ratio: 0.85,
annual: None,
};
assert_eq!(par2.ar_order(), 2);
}
#[test]
fn test_correlation_model_construction() {
let make_profile = |entity_ids: &[i32]| {
let entities: Vec<CorrelationEntity> = entity_ids
.iter()
.map(|&id| CorrelationEntity {
entity_type: "inflow".to_string(),
id: EntityId(id),
})
.collect();
let n = entities.len();
let matrix: Vec<Vec<f64>> = (0..n)
.map(|i| (0..n).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
.collect();
CorrelationProfile {
groups: vec![CorrelationGroup {
name: "group_a".to_string(),
entities,
matrix,
}],
}
};
let mut profiles = BTreeMap::new();
profiles.insert("wet".to_string(), make_profile(&[1, 2, 3]));
profiles.insert("dry".to_string(), make_profile(&[1, 2]));
let model = CorrelationModel {
method: "spectral".to_string(),
profiles,
schedule: vec![
CorrelationScheduleEntry {
stage_id: 0,
profile_name: "wet".to_string(),
},
CorrelationScheduleEntry {
stage_id: 6,
profile_name: "dry".to_string(),
},
],
};
assert_eq!(model.profiles.len(), 2);
let mut profile_iter = model.profiles.keys();
assert_eq!(profile_iter.next().unwrap(), "dry");
assert_eq!(profile_iter.next().unwrap(), "wet");
assert!(model.profiles.contains_key("wet"));
assert!(model.profiles.contains_key("dry"));
let wet = &model.profiles["wet"];
assert_eq!(wet.groups[0].matrix.len(), 3);
let dry = &model.profiles["dry"];
assert_eq!(dry.groups[0].matrix.len(), 2);
assert_eq!(model.schedule.len(), 2);
assert_eq!(model.schedule[0].profile_name, "wet");
assert_eq!(model.schedule[1].profile_name, "dry");
}
#[test]
fn test_sampling_scheme_copy() {
let original = SamplingScheme::InSample;
let copied = original;
assert_eq!(original, copied);
let original_oos = SamplingScheme::OutOfSample;
let copied_oos = original_oos;
assert_eq!(original_oos, copied_oos);
let original_ext = SamplingScheme::External;
let copied_ext = original_ext;
assert_eq!(original_ext, copied_ext);
let original_hist = SamplingScheme::Historical;
let copied_hist = original_hist;
assert_eq!(original_hist, copied_hist);
}
#[cfg(feature = "serde")]
#[test]
fn test_scenario_source_serde_roundtrip() {
use super::HistoricalYears;
let source = ScenarioSource {
inflow_scheme: SamplingScheme::InSample,
load_scheme: SamplingScheme::OutOfSample,
ncs_scheme: SamplingScheme::External,
seed: Some(12345),
historical_years: None,
};
let json = serde_json::to_string(&source).unwrap();
let deserialized: ScenarioSource = serde_json::from_str(&json).unwrap();
assert_eq!(source, deserialized);
let source_hist = ScenarioSource {
inflow_scheme: SamplingScheme::Historical,
load_scheme: SamplingScheme::InSample,
ncs_scheme: SamplingScheme::InSample,
seed: Some(7),
historical_years: Some(HistoricalYears::List(vec![1990, 2000, 2010])),
};
let json_hist = serde_json::to_string(&source_hist).unwrap();
let deserialized_hist: ScenarioSource = serde_json::from_str(&json_hist).unwrap();
assert_eq!(source_hist, deserialized_hist);
let source_range = ScenarioSource {
inflow_scheme: SamplingScheme::Historical,
load_scheme: SamplingScheme::InSample,
ncs_scheme: SamplingScheme::InSample,
seed: None,
historical_years: Some(HistoricalYears::Range {
from: 1940,
to: 2010,
}),
};
let json_range = serde_json::to_string(&source_range).unwrap();
let deserialized_range: ScenarioSource = serde_json::from_str(&json_range).unwrap();
assert_eq!(source_range, deserialized_range);
let source_default = ScenarioSource {
inflow_scheme: SamplingScheme::InSample,
load_scheme: SamplingScheme::InSample,
ncs_scheme: SamplingScheme::InSample,
seed: None,
historical_years: None,
};
let json_default = serde_json::to_string(&source_default).unwrap();
let deserialized_default: ScenarioSource = serde_json::from_str(&json_default).unwrap();
assert_eq!(source_default, deserialized_default);
}
#[test]
fn test_scenario_source_default() {
let source = ScenarioSource::default();
assert_eq!(source.inflow_scheme, SamplingScheme::InSample);
assert_eq!(source.load_scheme, SamplingScheme::InSample);
assert_eq!(source.ncs_scheme, SamplingScheme::InSample);
assert!(source.seed.is_none());
assert!(source.historical_years.is_none());
}
#[test]
fn test_historical_years_list_construction() {
use super::HistoricalYears;
let years = HistoricalYears::List(vec![1940, 1953, 1971]);
match &years {
HistoricalYears::List(v) => {
assert_eq!(v.len(), 3);
assert_eq!(v[0], 1940);
assert_eq!(v[1], 1953);
assert_eq!(v[2], 1971);
}
HistoricalYears::Range { .. } => panic!("expected List variant"),
}
}
#[test]
fn test_historical_years_range_construction() {
use super::HistoricalYears;
let years = HistoricalYears::Range {
from: 1940,
to: 2010,
};
match years {
HistoricalYears::Range { from, to } => {
assert_eq!(from, 1940);
assert_eq!(to, 2010);
}
HistoricalYears::List(_) => panic!("expected Range variant"),
}
}
#[cfg(feature = "serde")]
#[test]
fn test_historical_years_list_serde_roundtrip() {
use super::HistoricalYears;
let years = HistoricalYears::List(vec![1940, 1953, 1971]);
let json = serde_json::to_string(&years).unwrap();
let deserialized: HistoricalYears = serde_json::from_str(&json).unwrap();
assert_eq!(years, deserialized);
}
#[cfg(feature = "serde")]
#[test]
fn test_historical_years_range_serde_roundtrip() {
use super::HistoricalYears;
let years = HistoricalYears::Range {
from: 1940,
to: 2010,
};
let json = serde_json::to_string(&years).unwrap();
let deserialized: HistoricalYears = serde_json::from_str(&json).unwrap();
assert_eq!(years, deserialized);
}
#[cfg(feature = "serde")]
#[test]
fn test_inflow_model_serde_roundtrip() {
let model = InflowModel {
hydro_id: EntityId(3),
stage_id: 0,
mean_m3s: 150.0,
std_m3s: 30.0,
ar_coefficients: vec![0.45, 0.22],
residual_std_ratio: 0.85,
annual: None,
};
let json = serde_json::to_string(&model).unwrap();
let deserialized: InflowModel = serde_json::from_str(&json).unwrap();
assert_eq!(model, deserialized);
assert!((deserialized.residual_std_ratio - 0.85).abs() < f64::EPSILON);
}
#[test]
fn test_ncs_model_construction() {
let model = NcsModel {
ncs_id: EntityId(3),
stage_id: 0,
mean: 0.5,
std: 0.1,
};
assert_eq!(model.ncs_id, EntityId(3));
assert_eq!(model.stage_id, 0);
assert_eq!(model.mean, 0.5);
assert_eq!(model.std, 0.1);
}
#[cfg(feature = "serde")]
#[test]
fn test_ncs_model_serde_roundtrip() {
let model = NcsModel {
ncs_id: EntityId(5),
stage_id: 2,
mean: 0.75,
std: 0.15,
};
let json = serde_json::to_string(&model).unwrap();
let deserialized: NcsModel = serde_json::from_str(&json).unwrap();
assert_eq!(model, deserialized);
}
#[test]
fn test_correlation_model_identity_matrix_access() {
let identity = vec![
vec![1.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0],
vec![0.0, 0.0, 1.0],
];
let mut profiles = BTreeMap::new();
profiles.insert(
"default".to_string(),
CorrelationProfile {
groups: vec![CorrelationGroup {
name: "all_hydros".to_string(),
entities: vec![
CorrelationEntity {
entity_type: "inflow".to_string(),
id: EntityId(1),
},
CorrelationEntity {
entity_type: "inflow".to_string(),
id: EntityId(2),
},
CorrelationEntity {
entity_type: "inflow".to_string(),
id: EntityId(3),
},
],
matrix: identity,
}],
},
);
let model = CorrelationModel {
method: "spectral".to_string(),
profiles,
schedule: vec![],
};
assert_eq!(model.profiles["default"].groups[0].matrix.len(), 3);
}
#[test]
fn inflow_model_annual_default_none() {
let m = InflowModel {
hydro_id: EntityId(1),
stage_id: 0,
mean_m3s: 100.0,
std_m3s: 10.0,
ar_coefficients: vec![0.5],
residual_std_ratio: 0.85,
annual: None,
};
assert!(m.annual.is_none());
}
#[test]
fn inflow_model_annual_some_round_trip() {
let ann = AnnualComponent {
coefficient: 0.15,
mean_m3s: 90.0,
std_m3s: 12.0,
};
let m = InflowModel {
hydro_id: EntityId(1),
stage_id: 0,
mean_m3s: 100.0,
std_m3s: 10.0,
ar_coefficients: vec![0.5],
residual_std_ratio: 0.85,
annual: Some(ann.clone()),
};
assert_eq!(m.annual.as_ref().expect("annual present"), &ann);
assert_eq!(m.ar_order(), 1);
}
#[test]
fn annual_component_partial_eq_clone() {
let a = AnnualComponent {
coefficient: 0.15,
mean_m3s: 90.0,
std_m3s: 12.0,
};
let b = a.clone();
assert_eq!(a, b);
}
}