1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5use crate::network::{BalancedNetwork, BusId, GenCost};
6use crate::{Error, Result};
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
10#[serde(tag = "mode", rename_all = "snake_case")]
11pub enum MissingGenCostPolicy {
12 #[default]
14 Preserve,
15 Require,
17 Fill {
19 c2: f64,
20 c1: f64,
21 c0: f64,
22 startup: f64,
23 shutdown: f64,
24 },
25}
26
27impl MissingGenCostPolicy {
28 #[must_use]
29 pub fn zero() -> Self {
30 Self::Fill {
31 c2: 0.0,
32 c1: 0.0,
33 c0: 0.0,
34 startup: 0.0,
35 shutdown: 0.0,
36 }
37 }
38
39 #[must_use]
40 pub fn quadratic(c2: f64, c1: f64, c0: f64) -> Self {
41 Self::Fill {
42 c2,
43 c1,
44 c0,
45 startup: 0.0,
46 shutdown: 0.0,
47 }
48 }
49
50 #[must_use]
51 pub fn is_preserve(self) -> bool {
52 matches!(self, Self::Preserve)
53 }
54
55 #[must_use]
56 pub fn label(self) -> &'static str {
57 match self {
58 Self::Preserve => "preserve",
59 Self::Require => "require",
60 Self::Fill { .. } => "fill",
61 }
62 }
63
64 fn fill_cost(c2: f64, c1: f64, c0: f64, startup: f64, shutdown: f64) -> Result<GenCost> {
65 for (field, value) in [
66 ("c2", c2),
67 ("c1", c1),
68 ("c0", c0),
69 ("startup", startup),
70 ("shutdown", shutdown),
71 ] {
72 if !value.is_finite() {
73 return Err(Error::NonFiniteGenCost { field, value });
74 }
75 }
76 Ok(GenCost {
77 model: 2,
78 startup,
79 shutdown,
80 ncost: 3,
81 coeffs: vec![c2, c1, c0],
82 })
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct GenCostPatch {
89 pub gen_index: usize,
91 pub bus: BusId,
93 pub cost: GenCost,
94}
95
96#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
98pub struct GenCostPolicyReport {
99 pub missing_before: usize,
100 pub missing_in_service_before: usize,
101 pub patched: usize,
102 pub synthesized: usize,
103}
104
105impl BalancedNetwork {
106 pub fn apply_gen_cost_policy(
111 &mut self,
112 patches: &[GenCostPatch],
113 policy: MissingGenCostPolicy,
114 ) -> Result<GenCostPolicyReport> {
115 let patched = self.apply_gen_cost_patches(patches)?;
116 let missing_before = self
117 .generators()
118 .iter()
119 .filter(|g| g.cost.is_none())
120 .count();
121 let missing_in_service_before = self
122 .generators()
123 .iter()
124 .filter(|g| g.in_service && g.cost.is_none())
125 .count();
126
127 let mut synthesized = 0usize;
128 match policy {
129 MissingGenCostPolicy::Preserve => {}
130 MissingGenCostPolicy::Require => {
131 if let Some((idx, _)) = self
132 .generators()
133 .iter()
134 .enumerate()
135 .find(|(_, g)| g.in_service && g.cost.is_none())
136 {
137 return Err(Error::MissingGenCost { gen_index: idx });
138 }
139 }
140 MissingGenCostPolicy::Fill {
141 c2,
142 c1,
143 c0,
144 startup,
145 shutdown,
146 } => {
147 let cost = MissingGenCostPolicy::fill_cost(c2, c1, c0, startup, shutdown)?;
148 for generator in self.generators_mut() {
149 if generator.cost.is_none() {
150 generator.cost = Some(cost.clone());
151 synthesized += 1;
152 }
153 }
154 }
155 }
156
157 Ok(GenCostPolicyReport {
158 missing_before,
159 missing_in_service_before,
160 patched,
161 synthesized,
162 })
163 }
164
165 fn apply_gen_cost_patches(&mut self, patches: &[GenCostPatch]) -> Result<usize> {
166 let mut seen = BTreeSet::new();
167 for (row, patch) in patches.iter().enumerate() {
168 let row = row + 1;
169 if !seen.insert(patch.gen_index) {
170 return Err(Error::InvalidGenCostPatch {
171 row,
172 reason: format!("duplicate gen_index {}", patch.gen_index),
173 });
174 }
175 let Some(generator) = self.generators_mut().get_mut(patch.gen_index) else {
176 return Err(Error::InvalidGenCostPatch {
177 row,
178 reason: format!(
179 "gen_index {} out of range for {} generator(s)",
180 patch.gen_index,
181 self.generators().len()
182 ),
183 });
184 };
185 if generator.bus != patch.bus {
186 return Err(Error::InvalidGenCostPatch {
187 row,
188 reason: format!(
189 "bus mismatch for gen_index {}: table has {}, network has {}",
190 patch.gen_index, patch.bus, generator.bus
191 ),
192 });
193 }
194 validate_cost(&patch.cost, row)?;
195 generator.cost = Some(patch.cost.clone());
196 }
197 Ok(patches.len())
198 }
199}
200
201pub fn parse_gen_cost_csv(content: &str) -> Result<Vec<GenCostPatch>> {
207 let mut lines = content
208 .lines()
209 .enumerate()
210 .filter(|(_, line)| !line.trim().is_empty());
211 let Some((_, header)) = lines.next() else {
212 return Err(Error::InvalidGenCostPatch {
213 row: 0,
214 reason: "empty generator cost CSV".into(),
215 });
216 };
217 let header = split_csv_line(header);
218 let col = |name: &'static str| {
219 header
220 .iter()
221 .position(|h| h == name)
222 .ok_or_else(|| Error::InvalidGenCostPatch {
223 row: 0,
224 reason: format!("missing required column `{name}`"),
225 })
226 };
227 let gen_index_col = col("gen_index")?;
228 let bus_col = col("bus")?;
229 let c2_col = col("c2")?;
230 let c1_col = col("c1")?;
231 let c0_col = col("c0")?;
232 let startup_col = header.iter().position(|h| h == "startup");
233 let shutdown_col = header.iter().position(|h| h == "shutdown");
234
235 let mut out = Vec::new();
236 for (line_no, line) in lines {
237 let row = line_no + 1;
238 let fields = split_csv_line(line);
239 let get = |idx: usize, name: &'static str| {
240 fields
241 .get(idx)
242 .filter(|s| !s.is_empty())
243 .ok_or_else(|| Error::InvalidGenCostPatch {
244 row,
245 reason: format!("missing value for `{name}`"),
246 })
247 };
248 let gen_index = parse_usize(get(gen_index_col, "gen_index")?, row, "gen_index")?;
249 let bus = BusId(parse_usize(get(bus_col, "bus")?, row, "bus")?);
250 let c2 = parse_f64(get(c2_col, "c2")?, row, "c2")?;
251 let c1 = parse_f64(get(c1_col, "c1")?, row, "c1")?;
252 let c0 = parse_f64(get(c0_col, "c0")?, row, "c0")?;
253 let startup = match startup_col {
254 Some(idx) => fields
255 .get(idx)
256 .filter(|s| !s.is_empty())
257 .map_or(Ok(0.0), |s| parse_f64(s, row, "startup"))?,
258 None => 0.0,
259 };
260 let shutdown = match shutdown_col {
261 Some(idx) => fields
262 .get(idx)
263 .filter(|s| !s.is_empty())
264 .map_or(Ok(0.0), |s| parse_f64(s, row, "shutdown"))?,
265 None => 0.0,
266 };
267 out.push(GenCostPatch {
268 gen_index,
269 bus,
270 cost: GenCost {
271 model: 2,
272 startup,
273 shutdown,
274 ncost: 3,
275 coeffs: vec![c2, c1, c0],
276 },
277 });
278 }
279 Ok(out)
280}
281
282fn split_csv_line(line: &str) -> Vec<String> {
283 line.split(',')
284 .map(|s| s.trim().trim_matches('"').to_string())
285 .collect()
286}
287
288fn parse_usize(value: &str, row: usize, field: &'static str) -> Result<usize> {
289 value
290 .parse::<usize>()
291 .map_err(|_| Error::InvalidGenCostPatch {
292 row,
293 reason: format!("`{field}` is not a non-negative integer: {value}"),
294 })
295}
296
297fn parse_f64(value: &str, row: usize, field: &'static str) -> Result<f64> {
298 let parsed = value
299 .parse::<f64>()
300 .map_err(|_| Error::InvalidGenCostPatch {
301 row,
302 reason: format!("`{field}` is not a number: {value}"),
303 })?;
304 if parsed.is_finite() {
305 Ok(parsed)
306 } else {
307 Err(Error::InvalidGenCostPatch {
308 row,
309 reason: format!("`{field}` is not finite: {parsed}"),
310 })
311 }
312}
313
314fn validate_cost(cost: &GenCost, row: usize) -> Result<()> {
315 for (field, value) in [("startup", cost.startup), ("shutdown", cost.shutdown)] {
316 if !value.is_finite() {
317 return Err(Error::InvalidGenCostPatch {
318 row,
319 reason: format!("`{field}` is not finite: {value}"),
320 });
321 }
322 }
323 for (idx, value) in cost.coeffs.iter().enumerate() {
324 if !value.is_finite() {
325 return Err(Error::InvalidGenCostPatch {
326 row,
327 reason: format!("cost coefficient {idx} is not finite: {value}"),
328 });
329 }
330 }
331 Ok(())
332}