use crate::EntityId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum VariableRef {
HydroStorage {
hydro_id: EntityId,
},
HydroTurbined {
hydro_id: EntityId,
block_id: Option<usize>,
},
HydroSpillage {
hydro_id: EntityId,
block_id: Option<usize>,
},
HydroDiversion {
hydro_id: EntityId,
block_id: Option<usize>,
},
HydroOutflow {
hydro_id: EntityId,
block_id: Option<usize>,
},
HydroGeneration {
hydro_id: EntityId,
block_id: Option<usize>,
},
HydroEvaporation {
hydro_id: EntityId,
},
HydroWithdrawal {
hydro_id: EntityId,
},
ThermalGeneration {
thermal_id: EntityId,
block_id: Option<usize>,
},
LineDirect {
line_id: EntityId,
block_id: Option<usize>,
},
LineReverse {
line_id: EntityId,
block_id: Option<usize>,
},
LineExchange {
line_id: EntityId,
block_id: Option<usize>,
},
BusDeficit {
bus_id: EntityId,
block_id: Option<usize>,
},
BusExcess {
bus_id: EntityId,
block_id: Option<usize>,
},
PumpingFlow {
station_id: EntityId,
block_id: Option<usize>,
},
PumpingPower {
station_id: EntityId,
block_id: Option<usize>,
},
ContractImport {
contract_id: EntityId,
block_id: Option<usize>,
},
ContractExport {
contract_id: EntityId,
block_id: Option<usize>,
},
NonControllableGeneration {
source_id: EntityId,
block_id: Option<usize>,
},
NonControllableCurtailment {
source_id: EntityId,
block_id: Option<usize>,
},
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LinearTerm {
pub coefficient: f64,
pub variable: VariableRef,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConstraintExpression {
pub terms: Vec<LinearTerm>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ConstraintSense {
GreaterEqual,
LessEqual,
Equal,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SlackConfig {
pub enabled: bool,
pub penalty: Option<f64>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GenericConstraint {
pub id: EntityId,
pub name: String,
pub description: Option<String>,
pub expression: ConstraintExpression,
pub sense: ConstraintSense,
pub slack: SlackConfig,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_variable_ref_variants() {
let variants: &[(&str, VariableRef)] = &[
(
"HydroStorage",
VariableRef::HydroStorage {
hydro_id: EntityId(0),
},
),
(
"HydroTurbined",
VariableRef::HydroTurbined {
hydro_id: EntityId(0),
block_id: None,
},
),
(
"HydroSpillage",
VariableRef::HydroSpillage {
hydro_id: EntityId(0),
block_id: Some(1),
},
),
(
"HydroDiversion",
VariableRef::HydroDiversion {
hydro_id: EntityId(0),
block_id: None,
},
),
(
"HydroOutflow",
VariableRef::HydroOutflow {
hydro_id: EntityId(0),
block_id: None,
},
),
(
"HydroGeneration",
VariableRef::HydroGeneration {
hydro_id: EntityId(0),
block_id: Some(0),
},
),
(
"HydroEvaporation",
VariableRef::HydroEvaporation {
hydro_id: EntityId(0),
},
),
(
"HydroWithdrawal",
VariableRef::HydroWithdrawal {
hydro_id: EntityId(0),
},
),
(
"ThermalGeneration",
VariableRef::ThermalGeneration {
thermal_id: EntityId(0),
block_id: None,
},
),
(
"LineDirect",
VariableRef::LineDirect {
line_id: EntityId(0),
block_id: None,
},
),
(
"LineReverse",
VariableRef::LineReverse {
line_id: EntityId(0),
block_id: None,
},
),
(
"LineExchange",
VariableRef::LineExchange {
line_id: EntityId(0),
block_id: None,
},
),
(
"BusDeficit",
VariableRef::BusDeficit {
bus_id: EntityId(0),
block_id: None,
},
),
(
"BusExcess",
VariableRef::BusExcess {
bus_id: EntityId(0),
block_id: None,
},
),
(
"PumpingFlow",
VariableRef::PumpingFlow {
station_id: EntityId(0),
block_id: None,
},
),
(
"PumpingPower",
VariableRef::PumpingPower {
station_id: EntityId(0),
block_id: None,
},
),
(
"ContractImport",
VariableRef::ContractImport {
contract_id: EntityId(0),
block_id: None,
},
),
(
"ContractExport",
VariableRef::ContractExport {
contract_id: EntityId(0),
block_id: None,
},
),
(
"NonControllableGeneration",
VariableRef::NonControllableGeneration {
source_id: EntityId(0),
block_id: None,
},
),
(
"NonControllableCurtailment",
VariableRef::NonControllableCurtailment {
source_id: EntityId(0),
block_id: None,
},
),
];
assert_eq!(
variants.len(),
20,
"VariableRef must have exactly 20 variants"
);
for (name, variant) in variants {
let debug_str = format!("{variant:?}");
assert!(
debug_str.contains(name),
"Debug output for {name} does not contain the variant name: {debug_str}"
);
}
}
#[test]
fn test_generic_constraint_construction() {
let expr = ConstraintExpression {
terms: vec![
LinearTerm {
coefficient: 1.0,
variable: VariableRef::HydroGeneration {
hydro_id: EntityId(10),
block_id: None,
},
},
LinearTerm {
coefficient: 1.0,
variable: VariableRef::HydroGeneration {
hydro_id: EntityId(11),
block_id: None,
},
},
],
};
let gc = GenericConstraint {
id: EntityId(0),
name: "min_southeast_hydro".to_string(),
description: Some("Minimum hydro generation in Southeast region".to_string()),
expression: expr,
sense: ConstraintSense::GreaterEqual,
slack: SlackConfig {
enabled: true,
penalty: Some(5_000.0),
},
};
assert_eq!(gc.expression.terms.len(), 2);
assert_eq!(gc.id, EntityId(0));
assert_eq!(gc.name, "min_southeast_hydro");
assert!(gc.description.is_some());
assert_eq!(gc.sense, ConstraintSense::GreaterEqual);
assert!(gc.slack.enabled);
assert_eq!(gc.slack.penalty, Some(5_000.0));
}
#[test]
fn test_slack_config_disabled_has_no_penalty() {
let slack = SlackConfig {
enabled: false,
penalty: None,
};
assert!(!slack.enabled);
assert!(slack.penalty.is_none());
}
#[test]
fn test_constraint_sense_variants() {
assert_ne!(ConstraintSense::GreaterEqual, ConstraintSense::LessEqual);
assert_ne!(ConstraintSense::GreaterEqual, ConstraintSense::Equal);
assert_ne!(ConstraintSense::LessEqual, ConstraintSense::Equal);
}
#[test]
fn test_linear_term_with_coefficient() {
let term = LinearTerm {
coefficient: 2.5,
variable: VariableRef::ThermalGeneration {
thermal_id: EntityId(5),
block_id: None,
},
};
assert_eq!(term.coefficient, 2.5);
let debug = format!("{:?}", term.variable);
assert!(debug.contains("ThermalGeneration"));
}
#[test]
fn test_variable_ref_block_none_vs_some() {
let all_blocks = VariableRef::HydroTurbined {
hydro_id: EntityId(3),
block_id: None,
};
let specific_block = VariableRef::HydroTurbined {
hydro_id: EntityId(3),
block_id: Some(0),
};
assert_ne!(all_blocks, specific_block);
}
#[cfg(feature = "serde")]
#[test]
fn test_generic_constraint_serde_roundtrip() {
let gc = GenericConstraint {
id: EntityId(0),
name: "test".to_string(),
description: None,
expression: ConstraintExpression {
terms: vec![
LinearTerm {
coefficient: 1.0,
variable: VariableRef::HydroGeneration {
hydro_id: EntityId(10),
block_id: None,
},
},
LinearTerm {
coefficient: 1.0,
variable: VariableRef::HydroGeneration {
hydro_id: EntityId(11),
block_id: None,
},
},
],
},
sense: ConstraintSense::GreaterEqual,
slack: SlackConfig {
enabled: true,
penalty: Some(5_000.0),
},
};
let json = serde_json::to_string(&gc).unwrap();
let deserialized: GenericConstraint = serde_json::from_str(&json).unwrap();
assert_eq!(gc, deserialized);
assert_eq!(deserialized.expression.terms.len(), 2);
}
}