1use std::sync::Arc;
5
6use powerio_core::Error;
7
8use crate::diagnostics::codes;
9use crate::instance::AcScucInstance;
10use crate::solution::{Producer, Residuals, Termination};
11
12#[derive(Clone, Debug, Default, PartialEq)]
15#[non_exhaustive]
16pub struct ScucNetworkOutputs {
17 pub bus_vm: Vec<Vec<f64>>,
19 pub bus_va: Vec<Vec<f64>>,
21 pub shunt_step: Vec<Vec<i64>>,
23 pub ac_line_on_status: Vec<Vec<bool>>,
25 pub transformer_tm: Vec<Vec<f64>>,
27 pub transformer_ta: Vec<Vec<f64>>,
29 pub transformer_on_status: Vec<Vec<bool>>,
31 pub dc_line_pdc_fr: Vec<Vec<f64>>,
33 pub dc_line_qdc_fr: Vec<Vec<f64>>,
35 pub dc_line_qdc_to: Vec<Vec<f64>>,
37}
38
39#[derive(Clone, Debug, Default, PartialEq)]
42#[non_exhaustive]
43pub struct ScucDeviceOutputs {
44 pub on_status: Vec<Vec<bool>>,
46 pub startup_status: Vec<Vec<bool>>,
48 pub shutdown_status: Vec<Vec<bool>>,
50 pub p_on: Vec<Vec<f64>>,
52 pub q: Vec<Vec<f64>>,
54 pub p_reg_res_up: Vec<Vec<f64>>,
56 pub p_reg_res_down: Vec<Vec<f64>>,
58 pub p_syn_res: Vec<Vec<f64>>,
60 pub p_nsyn_res: Vec<Vec<f64>>,
62 pub p_ramp_res_up_online: Vec<Vec<f64>>,
64 pub p_ramp_res_up_offline: Vec<Vec<f64>>,
66 pub p_ramp_res_down_online: Vec<Vec<f64>>,
68 pub p_ramp_res_down_offline: Vec<Vec<f64>>,
70 pub q_res_up: Vec<Vec<f64>>,
72 pub q_res_down: Vec<Vec<f64>>,
74}
75
76pub const SCUC_NETWORK_OUTPUT_SERIES: [&str; 10] = [
81 "bus_vm",
82 "bus_va",
83 "shunt_step",
84 "ac_line_on_status",
85 "transformer_tm",
86 "transformer_ta",
87 "transformer_on_status",
88 "dc_line_pdc_fr",
89 "dc_line_qdc_fr",
90 "dc_line_qdc_to",
91];
92
93pub const SCUC_DEVICE_OUTPUT_SERIES: [&str; 15] = [
96 "on_status",
97 "startup_status",
98 "shutdown_status",
99 "p_on",
100 "q",
101 "p_reg_res_up",
102 "p_reg_res_down",
103 "p_syn_res",
104 "p_nsyn_res",
105 "p_ramp_res_up_online",
106 "p_ramp_res_up_offline",
107 "p_ramp_res_down_online",
108 "p_ramp_res_down_offline",
109 "q_res_up",
110 "q_res_down",
111];
112
113#[derive(Clone, Debug)]
116pub struct AcScucSolution {
117 instance: Arc<AcScucInstance>,
118 termination: Termination,
119 residuals: Residuals,
120 producer: Producer,
121 network_outputs: ScucNetworkOutputs,
122 device_outputs: ScucDeviceOutputs,
123 objective: Option<f64>,
124}
125
126impl AcScucSolution {
127 #[allow(clippy::too_many_lines)]
136 pub fn new(
137 instance: Arc<AcScucInstance>,
138 termination: Termination,
139 network_outputs: ScucNetworkOutputs,
140 device_outputs: ScucDeviceOutputs,
141 objective: Option<f64>,
142 ) -> Result<Self, Error> {
143 let periods = instance.inputs().interval_durations.len();
144 let inputs = instance.inputs();
145 let network = instance.network();
146 let buses = network.buses().len();
147 let shunts = inputs.shunts.len();
148 let ac_lines = inputs
149 .branch_switching_costs
150 .iter()
151 .filter(|row| row.id.component_type() == "branch")
152 .count();
153 let transformers = inputs
154 .branch_switching_costs
155 .iter()
156 .filter(|row| row.id.component_type() == "transformer")
157 .count();
158 let dc_lines = network.hvdc().len();
159 let devices = inputs.devices.len();
160
161 check_finite_grid("bus vm", &network_outputs.bus_vm, periods, buses)?;
162 check_finite_grid("bus va", &network_outputs.bus_va, periods, buses)?;
163 check_grid("shunt step", &network_outputs.shunt_step, periods, shunts)?;
164 check_grid(
165 "ac line on status",
166 &network_outputs.ac_line_on_status,
167 periods,
168 ac_lines,
169 )?;
170 check_finite_grid(
171 "transformer tm",
172 &network_outputs.transformer_tm,
173 periods,
174 transformers,
175 )?;
176 check_finite_grid(
177 "transformer ta",
178 &network_outputs.transformer_ta,
179 periods,
180 transformers,
181 )?;
182 check_grid(
183 "transformer on status",
184 &network_outputs.transformer_on_status,
185 periods,
186 transformers,
187 )?;
188 check_finite_grid(
189 "dc line pdc_fr",
190 &network_outputs.dc_line_pdc_fr,
191 periods,
192 dc_lines,
193 )?;
194 check_finite_grid(
195 "dc line qdc_fr",
196 &network_outputs.dc_line_qdc_fr,
197 periods,
198 dc_lines,
199 )?;
200 check_finite_grid(
201 "dc line qdc_to",
202 &network_outputs.dc_line_qdc_to,
203 periods,
204 dc_lines,
205 )?;
206 check_grid(
207 "device on status",
208 &device_outputs.on_status,
209 periods,
210 devices,
211 )?;
212 check_grid(
213 "device startup status",
214 &device_outputs.startup_status,
215 periods,
216 devices,
217 )?;
218 check_grid(
219 "device shutdown status",
220 &device_outputs.shutdown_status,
221 periods,
222 devices,
223 )?;
224 check_finite_grid("device p_on", &device_outputs.p_on, periods, devices)?;
225 check_finite_grid("device q", &device_outputs.q, periods, devices)?;
226 check_finite_grid(
227 "device p_reg_res_up",
228 &device_outputs.p_reg_res_up,
229 periods,
230 devices,
231 )?;
232 check_finite_grid(
233 "device p_reg_res_down",
234 &device_outputs.p_reg_res_down,
235 periods,
236 devices,
237 )?;
238 check_finite_grid(
239 "device p_syn_res",
240 &device_outputs.p_syn_res,
241 periods,
242 devices,
243 )?;
244 check_finite_grid(
245 "device p_nsyn_res",
246 &device_outputs.p_nsyn_res,
247 periods,
248 devices,
249 )?;
250 check_finite_grid(
251 "device p_ramp_res_up_online",
252 &device_outputs.p_ramp_res_up_online,
253 periods,
254 devices,
255 )?;
256 check_finite_grid(
257 "device p_ramp_res_up_offline",
258 &device_outputs.p_ramp_res_up_offline,
259 periods,
260 devices,
261 )?;
262 check_finite_grid(
263 "device p_ramp_res_down_online",
264 &device_outputs.p_ramp_res_down_online,
265 periods,
266 devices,
267 )?;
268 check_finite_grid(
269 "device p_ramp_res_down_offline",
270 &device_outputs.p_ramp_res_down_offline,
271 periods,
272 devices,
273 )?;
274 check_finite_grid(
275 "device q_res_up",
276 &device_outputs.q_res_up,
277 periods,
278 devices,
279 )?;
280 check_finite_grid(
281 "device q_res_down",
282 &device_outputs.q_res_down,
283 periods,
284 devices,
285 )?;
286 Ok(Self {
287 instance,
288 termination,
289 residuals: Residuals::default(),
290 producer: None,
291 network_outputs,
292 device_outputs,
293 objective,
294 })
295 }
296
297 #[must_use]
299 pub fn instance(&self) -> &AcScucInstance {
300 &self.instance
301 }
302
303 #[must_use]
305 pub fn shared_instance(&self) -> Arc<AcScucInstance> {
306 Arc::clone(&self.instance)
307 }
308
309 #[must_use]
311 pub const fn termination(&self) -> &Termination {
312 &self.termination
313 }
314
315 #[must_use]
317 pub const fn residuals(&self) -> &Residuals {
318 &self.residuals
319 }
320
321 #[must_use]
323 pub fn producer(&self) -> Option<&str> {
324 self.producer.as_deref()
325 }
326
327 #[must_use]
329 pub fn with_producer(mut self, producer: impl Into<String>) -> Self {
330 self.producer = Some(producer.into());
331 self
332 }
333
334 #[must_use]
336 pub fn with_residuals(mut self, residuals: Residuals) -> Self {
337 self.residuals = residuals;
338 self
339 }
340
341 #[must_use]
343 pub const fn network_outputs(&self) -> &ScucNetworkOutputs {
344 &self.network_outputs
345 }
346
347 #[must_use]
349 pub const fn device_outputs(&self) -> &ScucDeviceOutputs {
350 &self.device_outputs
351 }
352
353 #[must_use]
355 pub const fn objective(&self) -> Option<f64> {
356 self.objective
357 }
358}
359
360fn check_grid<T>(what: &str, series: &[Vec<T>], periods: usize, width: usize) -> Result<(), Error> {
361 if series.is_empty() {
362 return Ok(());
363 }
364 if series.len() != periods {
365 return Err(Error::new(
366 &codes::BUILD_SOLUTION_SHAPE_MISMATCH,
367 format!(
368 "{what} carries {} time rows; the instance states {periods} intervals",
369 series.len()
370 ),
371 ));
372 }
373 if let Some((time, row)) = series
374 .iter()
375 .enumerate()
376 .find(|(_, row)| row.len() != width)
377 {
378 return Err(Error::new(
379 &codes::BUILD_SOLUTION_SHAPE_MISMATCH,
380 format!(
381 "{what} time row {time} carries {} values; the instance states {width} components",
382 row.len()
383 ),
384 ));
385 }
386 Ok(())
387}
388
389fn check_finite_grid(
390 what: &str,
391 series: &[Vec<f64>],
392 periods: usize,
393 width: usize,
394) -> Result<(), Error> {
395 check_grid(what, series, periods, width)?;
396 if let Some((time, column, value)) = series.iter().enumerate().find_map(|(time, row)| {
397 row.iter()
398 .copied()
399 .enumerate()
400 .find(|(_, value)| !value.is_finite())
401 .map(|(column, value)| (time, column, value))
402 }) {
403 return Err(Error::new(
404 &codes::BUILD_SOLUTION_SHAPE_MISMATCH,
405 format!("{what}[{time}][{column}] is not finite: {value}"),
406 ));
407 }
408 Ok(())
409}
410
411#[cfg(test)]
412mod series_vocabulary_tests {
413 use super::*;
414
415 #[test]
418 fn every_output_field_is_named_in_the_series_constants() {
419 let ScucNetworkOutputs {
420 bus_vm: _,
421 bus_va: _,
422 shunt_step: _,
423 ac_line_on_status: _,
424 transformer_tm: _,
425 transformer_ta: _,
426 transformer_on_status: _,
427 dc_line_pdc_fr: _,
428 dc_line_qdc_fr: _,
429 dc_line_qdc_to: _,
430 } = ScucNetworkOutputs::default();
431 assert_eq!(SCUC_NETWORK_OUTPUT_SERIES.len(), 10);
432
433 let ScucDeviceOutputs {
434 on_status: _,
435 startup_status: _,
436 shutdown_status: _,
437 p_on: _,
438 q: _,
439 p_reg_res_up: _,
440 p_reg_res_down: _,
441 p_syn_res: _,
442 p_nsyn_res: _,
443 p_ramp_res_up_online: _,
444 p_ramp_res_up_offline: _,
445 p_ramp_res_down_online: _,
446 p_ramp_res_down_offline: _,
447 q_res_up: _,
448 q_res_down: _,
449 } = ScucDeviceOutputs::default();
450 assert_eq!(SCUC_DEVICE_OUTPUT_SERIES.len(), 15);
451 }
452}