use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::network::{BusId, GenCost, Network};
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum MissingGenCostPolicy {
#[default]
Preserve,
Require,
Fill {
c2: f64,
c1: f64,
c0: f64,
startup: f64,
shutdown: f64,
},
}
impl MissingGenCostPolicy {
#[must_use]
pub fn zero() -> Self {
Self::Fill {
c2: 0.0,
c1: 0.0,
c0: 0.0,
startup: 0.0,
shutdown: 0.0,
}
}
#[must_use]
pub fn quadratic(c2: f64, c1: f64, c0: f64) -> Self {
Self::Fill {
c2,
c1,
c0,
startup: 0.0,
shutdown: 0.0,
}
}
#[must_use]
pub fn is_preserve(self) -> bool {
matches!(self, Self::Preserve)
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Preserve => "preserve",
Self::Require => "require",
Self::Fill { .. } => "fill",
}
}
fn fill_cost(c2: f64, c1: f64, c0: f64, startup: f64, shutdown: f64) -> Result<GenCost> {
for (field, value) in [
("c2", c2),
("c1", c1),
("c0", c0),
("startup", startup),
("shutdown", shutdown),
] {
if !value.is_finite() {
return Err(Error::NonFiniteGenCost { field, value });
}
}
Ok(GenCost {
model: 2,
startup,
shutdown,
ncost: 3,
coeffs: vec![c2, c1, c0],
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenCostPatch {
pub gen_index: usize,
pub bus: BusId,
pub cost: GenCost,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GenCostPolicyReport {
pub missing_before: usize,
pub missing_in_service_before: usize,
pub patched: usize,
pub synthesized: usize,
}
impl Network {
pub fn apply_gen_cost_policy(
&mut self,
patches: &[GenCostPatch],
policy: MissingGenCostPolicy,
) -> Result<GenCostPolicyReport> {
let patched = self.apply_gen_cost_patches(patches)?;
let missing_before = self.generators.iter().filter(|g| g.cost.is_none()).count();
let missing_in_service_before = self
.generators
.iter()
.filter(|g| g.in_service && g.cost.is_none())
.count();
let mut synthesized = 0usize;
match policy {
MissingGenCostPolicy::Preserve => {}
MissingGenCostPolicy::Require => {
if let Some((idx, _)) = self
.generators
.iter()
.enumerate()
.find(|(_, g)| g.in_service && g.cost.is_none())
{
return Err(Error::MissingGenCost { gen_index: idx });
}
}
MissingGenCostPolicy::Fill {
c2,
c1,
c0,
startup,
shutdown,
} => {
let cost = MissingGenCostPolicy::fill_cost(c2, c1, c0, startup, shutdown)?;
for generator in &mut self.generators {
if generator.cost.is_none() {
generator.cost = Some(cost.clone());
synthesized += 1;
}
}
}
}
Ok(GenCostPolicyReport {
missing_before,
missing_in_service_before,
patched,
synthesized,
})
}
fn apply_gen_cost_patches(&mut self, patches: &[GenCostPatch]) -> Result<usize> {
let mut seen = BTreeSet::new();
for (row, patch) in patches.iter().enumerate() {
let row = row + 1;
if !seen.insert(patch.gen_index) {
return Err(Error::InvalidGenCostPatch {
row,
reason: format!("duplicate gen_index {}", patch.gen_index),
});
}
let Some(generator) = self.generators.get_mut(patch.gen_index) else {
return Err(Error::InvalidGenCostPatch {
row,
reason: format!(
"gen_index {} out of range for {} generator(s)",
patch.gen_index,
self.generators.len()
),
});
};
if generator.bus != patch.bus {
return Err(Error::InvalidGenCostPatch {
row,
reason: format!(
"bus mismatch for gen_index {}: table has {}, network has {}",
patch.gen_index, patch.bus, generator.bus
),
});
}
validate_cost(&patch.cost, row)?;
generator.cost = Some(patch.cost.clone());
}
Ok(patches.len())
}
}
pub fn parse_gen_cost_csv(content: &str) -> Result<Vec<GenCostPatch>> {
let mut lines = content
.lines()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty());
let Some((_, header)) = lines.next() else {
return Err(Error::InvalidGenCostPatch {
row: 0,
reason: "empty generator cost CSV".into(),
});
};
let header = split_csv_line(header);
let col = |name: &'static str| {
header
.iter()
.position(|h| h == name)
.ok_or_else(|| Error::InvalidGenCostPatch {
row: 0,
reason: format!("missing required column `{name}`"),
})
};
let gen_index_col = col("gen_index")?;
let bus_col = col("bus")?;
let c2_col = col("c2")?;
let c1_col = col("c1")?;
let c0_col = col("c0")?;
let startup_col = header.iter().position(|h| h == "startup");
let shutdown_col = header.iter().position(|h| h == "shutdown");
let mut out = Vec::new();
for (line_no, line) in lines {
let row = line_no + 1;
let fields = split_csv_line(line);
let get = |idx: usize, name: &'static str| {
fields
.get(idx)
.filter(|s| !s.is_empty())
.ok_or_else(|| Error::InvalidGenCostPatch {
row,
reason: format!("missing value for `{name}`"),
})
};
let gen_index = parse_usize(get(gen_index_col, "gen_index")?, row, "gen_index")?;
let bus = BusId(parse_usize(get(bus_col, "bus")?, row, "bus")?);
let c2 = parse_f64(get(c2_col, "c2")?, row, "c2")?;
let c1 = parse_f64(get(c1_col, "c1")?, row, "c1")?;
let c0 = parse_f64(get(c0_col, "c0")?, row, "c0")?;
let startup = match startup_col {
Some(idx) => fields
.get(idx)
.filter(|s| !s.is_empty())
.map_or(Ok(0.0), |s| parse_f64(s, row, "startup"))?,
None => 0.0,
};
let shutdown = match shutdown_col {
Some(idx) => fields
.get(idx)
.filter(|s| !s.is_empty())
.map_or(Ok(0.0), |s| parse_f64(s, row, "shutdown"))?,
None => 0.0,
};
out.push(GenCostPatch {
gen_index,
bus,
cost: GenCost {
model: 2,
startup,
shutdown,
ncost: 3,
coeffs: vec![c2, c1, c0],
},
});
}
Ok(out)
}
fn split_csv_line(line: &str) -> Vec<String> {
line.split(',')
.map(|s| s.trim().trim_matches('"').to_string())
.collect()
}
fn parse_usize(value: &str, row: usize, field: &'static str) -> Result<usize> {
value
.parse::<usize>()
.map_err(|_| Error::InvalidGenCostPatch {
row,
reason: format!("`{field}` is not a non-negative integer: {value}"),
})
}
fn parse_f64(value: &str, row: usize, field: &'static str) -> Result<f64> {
let parsed = value
.parse::<f64>()
.map_err(|_| Error::InvalidGenCostPatch {
row,
reason: format!("`{field}` is not a number: {value}"),
})?;
if parsed.is_finite() {
Ok(parsed)
} else {
Err(Error::InvalidGenCostPatch {
row,
reason: format!("`{field}` is not finite: {parsed}"),
})
}
}
fn validate_cost(cost: &GenCost, row: usize) -> Result<()> {
for (field, value) in [("startup", cost.startup), ("shutdown", cost.shutdown)] {
if !value.is_finite() {
return Err(Error::InvalidGenCostPatch {
row,
reason: format!("`{field}` is not finite: {value}"),
});
}
}
for (idx, value) in cost.coeffs.iter().enumerate() {
if !value.is_finite() {
return Err(Error::InvalidGenCostPatch {
row,
reason: format!("cost coefficient {idx} is not finite: {value}"),
});
}
}
Ok(())
}