1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5use crate::network::{BusId, GenCost, Network};
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 Network {
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.generators.iter().filter(|g| g.cost.is_none()).count();
117 let missing_in_service_before = self
118 .generators
119 .iter()
120 .filter(|g| g.in_service && g.cost.is_none())
121 .count();
122
123 let mut synthesized = 0usize;
124 match policy {
125 MissingGenCostPolicy::Preserve => {}
126 MissingGenCostPolicy::Require => {
127 if let Some((idx, _)) = self
128 .generators
129 .iter()
130 .enumerate()
131 .find(|(_, g)| g.in_service && g.cost.is_none())
132 {
133 return Err(Error::MissingGenCost { gen_index: idx });
134 }
135 }
136 MissingGenCostPolicy::Fill {
137 c2,
138 c1,
139 c0,
140 startup,
141 shutdown,
142 } => {
143 let cost = MissingGenCostPolicy::fill_cost(c2, c1, c0, startup, shutdown)?;
144 for generator in &mut self.generators {
145 if generator.cost.is_none() {
146 generator.cost = Some(cost.clone());
147 synthesized += 1;
148 }
149 }
150 }
151 }
152
153 Ok(GenCostPolicyReport {
154 missing_before,
155 missing_in_service_before,
156 patched,
157 synthesized,
158 })
159 }
160
161 fn apply_gen_cost_patches(&mut self, patches: &[GenCostPatch]) -> Result<usize> {
162 let mut seen = BTreeSet::new();
163 for (row, patch) in patches.iter().enumerate() {
164 let row = row + 1;
165 if !seen.insert(patch.gen_index) {
166 return Err(Error::InvalidGenCostPatch {
167 row,
168 reason: format!("duplicate gen_index {}", patch.gen_index),
169 });
170 }
171 let Some(generator) = self.generators.get_mut(patch.gen_index) else {
172 return Err(Error::InvalidGenCostPatch {
173 row,
174 reason: format!(
175 "gen_index {} out of range for {} generator(s)",
176 patch.gen_index,
177 self.generators.len()
178 ),
179 });
180 };
181 if generator.bus != patch.bus {
182 return Err(Error::InvalidGenCostPatch {
183 row,
184 reason: format!(
185 "bus mismatch for gen_index {}: table has {}, network has {}",
186 patch.gen_index, patch.bus, generator.bus
187 ),
188 });
189 }
190 validate_cost(&patch.cost, row)?;
191 generator.cost = Some(patch.cost.clone());
192 }
193 Ok(patches.len())
194 }
195}
196
197pub fn parse_gen_cost_csv(content: &str) -> Result<Vec<GenCostPatch>> {
203 let mut lines = content
204 .lines()
205 .enumerate()
206 .filter(|(_, line)| !line.trim().is_empty());
207 let Some((_, header)) = lines.next() else {
208 return Err(Error::InvalidGenCostPatch {
209 row: 0,
210 reason: "empty generator cost CSV".into(),
211 });
212 };
213 let header = split_csv_line(header);
214 let col = |name: &'static str| {
215 header
216 .iter()
217 .position(|h| h == name)
218 .ok_or_else(|| Error::InvalidGenCostPatch {
219 row: 0,
220 reason: format!("missing required column `{name}`"),
221 })
222 };
223 let gen_index_col = col("gen_index")?;
224 let bus_col = col("bus")?;
225 let c2_col = col("c2")?;
226 let c1_col = col("c1")?;
227 let c0_col = col("c0")?;
228 let startup_col = header.iter().position(|h| h == "startup");
229 let shutdown_col = header.iter().position(|h| h == "shutdown");
230
231 let mut out = Vec::new();
232 for (line_no, line) in lines {
233 let row = line_no + 1;
234 let fields = split_csv_line(line);
235 let get = |idx: usize, name: &'static str| {
236 fields
237 .get(idx)
238 .filter(|s| !s.is_empty())
239 .ok_or_else(|| Error::InvalidGenCostPatch {
240 row,
241 reason: format!("missing value for `{name}`"),
242 })
243 };
244 let gen_index = parse_usize(get(gen_index_col, "gen_index")?, row, "gen_index")?;
245 let bus = BusId(parse_usize(get(bus_col, "bus")?, row, "bus")?);
246 let c2 = parse_f64(get(c2_col, "c2")?, row, "c2")?;
247 let c1 = parse_f64(get(c1_col, "c1")?, row, "c1")?;
248 let c0 = parse_f64(get(c0_col, "c0")?, row, "c0")?;
249 let startup = match startup_col {
250 Some(idx) => fields
251 .get(idx)
252 .filter(|s| !s.is_empty())
253 .map_or(Ok(0.0), |s| parse_f64(s, row, "startup"))?,
254 None => 0.0,
255 };
256 let shutdown = match shutdown_col {
257 Some(idx) => fields
258 .get(idx)
259 .filter(|s| !s.is_empty())
260 .map_or(Ok(0.0), |s| parse_f64(s, row, "shutdown"))?,
261 None => 0.0,
262 };
263 out.push(GenCostPatch {
264 gen_index,
265 bus,
266 cost: GenCost {
267 model: 2,
268 startup,
269 shutdown,
270 ncost: 3,
271 coeffs: vec![c2, c1, c0],
272 },
273 });
274 }
275 Ok(out)
276}
277
278fn split_csv_line(line: &str) -> Vec<String> {
279 line.split(',')
280 .map(|s| s.trim().trim_matches('"').to_string())
281 .collect()
282}
283
284fn parse_usize(value: &str, row: usize, field: &'static str) -> Result<usize> {
285 value
286 .parse::<usize>()
287 .map_err(|_| Error::InvalidGenCostPatch {
288 row,
289 reason: format!("`{field}` is not a non-negative integer: {value}"),
290 })
291}
292
293fn parse_f64(value: &str, row: usize, field: &'static str) -> Result<f64> {
294 let parsed = value
295 .parse::<f64>()
296 .map_err(|_| Error::InvalidGenCostPatch {
297 row,
298 reason: format!("`{field}` is not a number: {value}"),
299 })?;
300 if parsed.is_finite() {
301 Ok(parsed)
302 } else {
303 Err(Error::InvalidGenCostPatch {
304 row,
305 reason: format!("`{field}` is not finite: {parsed}"),
306 })
307 }
308}
309
310fn validate_cost(cost: &GenCost, row: usize) -> Result<()> {
311 for (field, value) in [("startup", cost.startup), ("shutdown", cost.shutdown)] {
312 if !value.is_finite() {
313 return Err(Error::InvalidGenCostPatch {
314 row,
315 reason: format!("`{field}` is not finite: {value}"),
316 });
317 }
318 }
319 for (idx, value) in cost.coeffs.iter().enumerate() {
320 if !value.is_finite() {
321 return Err(Error::InvalidGenCostPatch {
322 row,
323 reason: format!("cost coefficient {idx} is not finite: {value}"),
324 });
325 }
326 }
327 Ok(())
328}