Skip to main content

powerio_prob/solution/
scuc.rs

1//! The AC security constrained unit commitment solution, preserving the DOE
2//! GO Challenge 3 output fields.
3
4use 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/// Per time point network outputs: `values[t][row]` over the stated
13/// element table order.
14#[derive(Clone, Debug, Default, PartialEq)]
15#[non_exhaustive]
16pub struct ScucNetworkOutputs {
17    /// Bus voltage magnitude, per unit.
18    pub bus_vm: Vec<Vec<f64>>,
19    /// Bus voltage angle, radians.
20    pub bus_va: Vec<Vec<f64>>,
21    /// Shunt step counts.
22    pub shunt_step: Vec<Vec<i64>>,
23    /// AC line on status.
24    pub ac_line_on_status: Vec<Vec<bool>>,
25    /// Two winding transformer winding ratio.
26    pub transformer_tm: Vec<Vec<f64>>,
27    /// Two winding transformer phase shift, radians.
28    pub transformer_ta: Vec<Vec<f64>>,
29    /// Two winding transformer on status.
30    pub transformer_on_status: Vec<Vec<bool>>,
31    /// DC line from-side active flow, per unit power.
32    pub dc_line_pdc_fr: Vec<Vec<f64>>,
33    /// DC line from-side reactive flow, per unit power.
34    pub dc_line_qdc_fr: Vec<Vec<f64>>,
35    /// DC line to-side reactive flow, per unit power.
36    pub dc_line_qdc_to: Vec<Vec<f64>>,
37}
38
39/// Per time point simple dispatchable device outputs: `values[t][device]`
40/// over the stated device order.
41#[derive(Clone, Debug, Default, PartialEq)]
42#[non_exhaustive]
43pub struct ScucDeviceOutputs {
44    /// Commitment.
45    pub on_status: Vec<Vec<bool>>,
46    /// Startup status.
47    pub startup_status: Vec<Vec<bool>>,
48    /// Shutdown status.
49    pub shutdown_status: Vec<Vec<bool>>,
50    /// Dispatched active power while on, per unit power.
51    pub p_on: Vec<Vec<f64>>,
52    /// Dispatched reactive power, per unit power.
53    pub q: Vec<Vec<f64>>,
54    /// Regulation up reserve, per unit power.
55    pub p_reg_res_up: Vec<Vec<f64>>,
56    /// Regulation down reserve, per unit power.
57    pub p_reg_res_down: Vec<Vec<f64>>,
58    /// Synchronized reserve, per unit power.
59    pub p_syn_res: Vec<Vec<f64>>,
60    /// Non-synchronized reserve, per unit power.
61    pub p_nsyn_res: Vec<Vec<f64>>,
62    /// Ramp up reserve when online, per unit power.
63    pub p_ramp_res_up_online: Vec<Vec<f64>>,
64    /// Ramp up reserve when offline, per unit power.
65    pub p_ramp_res_up_offline: Vec<Vec<f64>>,
66    /// Ramp down reserve when online, per unit power.
67    pub p_ramp_res_down_online: Vec<Vec<f64>>,
68    /// Ramp down reserve when offline, per unit power.
69    pub p_ramp_res_down_offline: Vec<Vec<f64>>,
70    /// Reactive reserve up, per unit power.
71    pub q_res_up: Vec<Vec<f64>>,
72    /// Reactive reserve down, per unit power.
73    pub q_res_down: Vec<Vec<f64>>,
74}
75
76/// Every stored series of [`ScucNetworkOutputs`], one name per field. The
77/// serialized fields and this list stay in agreement through the exhaustive
78/// destructure test below: adding a field breaks the build until the name
79/// lands here and in the stored document.
80pub 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
93/// Every stored series of [`ScucDeviceOutputs`], as
94/// [`SCUC_NETWORK_OUTPUT_SERIES`].
95pub 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/// The AC security constrained unit commitment solution over the shared
114/// instance.
115#[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    /// Assemble the solution. Every stated output series must carry one row
128    /// per time point of the instance's time axis and one value per component
129    /// in the corresponding instance table. Empty series remain permitted for
130    /// producers that do not supply a category; GO Challenge 3 output requires
131    /// every category and its writer checks that stronger requirement.
132    ///
133    /// # Errors
134    /// An output series whose time axis disagrees with the instance.
135    #[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    /// The immutable instance this solution solves. Borrowed; never a copy.
298    #[must_use]
299    pub fn instance(&self) -> &AcScucInstance {
300        &self.instance
301    }
302
303    /// The shared instance owner, for another solution of the same problem.
304    #[must_use]
305    pub fn shared_instance(&self) -> Arc<AcScucInstance> {
306        Arc::clone(&self.instance)
307    }
308
309    /// How the producing calculation ended.
310    #[must_use]
311    pub const fn termination(&self) -> &Termination {
312        &self.termination
313    }
314
315    /// The reported numerical residuals.
316    #[must_use]
317    pub const fn residuals(&self) -> &Residuals {
318        &self.residuals
319    }
320
321    /// The producer or solver identity, when recorded.
322    #[must_use]
323    pub fn producer(&self) -> Option<&str> {
324        self.producer.as_deref()
325    }
326
327    /// Record the producer identity.
328    #[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    /// Record the numerical residuals.
335    #[must_use]
336    pub fn with_residuals(mut self, residuals: Residuals) -> Self {
337        self.residuals = residuals;
338        self
339    }
340
341    /// The per time point network outputs.
342    #[must_use]
343    pub const fn network_outputs(&self) -> &ScucNetworkOutputs {
344        &self.network_outputs
345    }
346
347    /// The per time point device outputs.
348    #[must_use]
349    pub const fn device_outputs(&self) -> &ScucDeviceOutputs {
350        &self.device_outputs
351    }
352
353    /// The reported objective value, when the producer states one.
354    #[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    /// Exhaustive destructures with no rest binding: a field added to either
416    /// struct fails this build until its name joins the series constant.
417    #[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}