powerio-prob 0.11.3

Problem instance builders for power system analysis and optimization.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! The AC security constrained unit commitment solution, preserving the DOE
//! GO Challenge 3 output fields.

use std::sync::Arc;

use powerio_core::Error;

use crate::diagnostics::codes;
use crate::instance::AcScucInstance;
use crate::solution::{Producer, Residuals, Termination};

/// Per time point network outputs: `values[t][row]` over the stated
/// element table order.
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct ScucNetworkOutputs {
    /// Bus voltage magnitude, per unit.
    pub bus_vm: Vec<Vec<f64>>,
    /// Bus voltage angle, radians.
    pub bus_va: Vec<Vec<f64>>,
    /// Shunt step counts.
    pub shunt_step: Vec<Vec<i64>>,
    /// AC line on status.
    pub ac_line_on_status: Vec<Vec<bool>>,
    /// Two winding transformer winding ratio.
    pub transformer_tm: Vec<Vec<f64>>,
    /// Two winding transformer phase shift, radians.
    pub transformer_ta: Vec<Vec<f64>>,
    /// Two winding transformer on status.
    pub transformer_on_status: Vec<Vec<bool>>,
    /// DC line from-side active flow, per unit power.
    pub dc_line_pdc_fr: Vec<Vec<f64>>,
    /// DC line from-side reactive flow, per unit power.
    pub dc_line_qdc_fr: Vec<Vec<f64>>,
    /// DC line to-side reactive flow, per unit power.
    pub dc_line_qdc_to: Vec<Vec<f64>>,
}

/// Per time point simple dispatchable device outputs: `values[t][device]`
/// over the stated device order.
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct ScucDeviceOutputs {
    /// Commitment.
    pub on_status: Vec<Vec<bool>>,
    /// Startup status.
    pub startup_status: Vec<Vec<bool>>,
    /// Shutdown status.
    pub shutdown_status: Vec<Vec<bool>>,
    /// Dispatched active power while on, per unit power.
    pub p_on: Vec<Vec<f64>>,
    /// Dispatched reactive power, per unit power.
    pub q: Vec<Vec<f64>>,
    /// Regulation up reserve, per unit power.
    pub p_reg_res_up: Vec<Vec<f64>>,
    /// Regulation down reserve, per unit power.
    pub p_reg_res_down: Vec<Vec<f64>>,
    /// Synchronized reserve, per unit power.
    pub p_syn_res: Vec<Vec<f64>>,
    /// Non-synchronized reserve, per unit power.
    pub p_nsyn_res: Vec<Vec<f64>>,
    /// Ramp up reserve when online, per unit power.
    pub p_ramp_res_up_online: Vec<Vec<f64>>,
    /// Ramp up reserve when offline, per unit power.
    pub p_ramp_res_up_offline: Vec<Vec<f64>>,
    /// Ramp down reserve when online, per unit power.
    pub p_ramp_res_down_online: Vec<Vec<f64>>,
    /// Ramp down reserve when offline, per unit power.
    pub p_ramp_res_down_offline: Vec<Vec<f64>>,
    /// Reactive reserve up, per unit power.
    pub q_res_up: Vec<Vec<f64>>,
    /// Reactive reserve down, per unit power.
    pub q_res_down: Vec<Vec<f64>>,
}

/// Every stored series of [`ScucNetworkOutputs`], one name per field. The
/// serialized fields and this list stay in agreement through the exhaustive
/// destructure test below: adding a field breaks the build until the name
/// lands here and in the stored document.
pub const SCUC_NETWORK_OUTPUT_SERIES: [&str; 10] = [
    "bus_vm",
    "bus_va",
    "shunt_step",
    "ac_line_on_status",
    "transformer_tm",
    "transformer_ta",
    "transformer_on_status",
    "dc_line_pdc_fr",
    "dc_line_qdc_fr",
    "dc_line_qdc_to",
];

/// Every stored series of [`ScucDeviceOutputs`], as
/// [`SCUC_NETWORK_OUTPUT_SERIES`].
pub const SCUC_DEVICE_OUTPUT_SERIES: [&str; 15] = [
    "on_status",
    "startup_status",
    "shutdown_status",
    "p_on",
    "q",
    "p_reg_res_up",
    "p_reg_res_down",
    "p_syn_res",
    "p_nsyn_res",
    "p_ramp_res_up_online",
    "p_ramp_res_up_offline",
    "p_ramp_res_down_online",
    "p_ramp_res_down_offline",
    "q_res_up",
    "q_res_down",
];

/// The AC security constrained unit commitment solution over the shared
/// instance.
#[derive(Clone, Debug)]
pub struct AcScucSolution {
    instance: Arc<AcScucInstance>,
    termination: Termination,
    residuals: Residuals,
    producer: Producer,
    network_outputs: ScucNetworkOutputs,
    device_outputs: ScucDeviceOutputs,
    objective: Option<f64>,
}

impl AcScucSolution {
    /// Assemble the solution. Every stated output series must carry one row
    /// per time point of the instance's time axis and one value per component
    /// in the corresponding instance table. Empty series remain permitted for
    /// producers that do not supply a category; GO Challenge 3 output requires
    /// every category and its writer checks that stronger requirement.
    ///
    /// # Errors
    /// An output series whose time axis disagrees with the instance.
    #[allow(clippy::too_many_lines)]
    pub fn new(
        instance: Arc<AcScucInstance>,
        termination: Termination,
        network_outputs: ScucNetworkOutputs,
        device_outputs: ScucDeviceOutputs,
        objective: Option<f64>,
    ) -> Result<Self, Error> {
        let periods = instance.inputs().interval_durations.len();
        let inputs = instance.inputs();
        let network = instance.network();
        let buses = network.buses().len();
        let shunts = inputs.shunts.len();
        let ac_lines = inputs
            .branch_switching_costs
            .iter()
            .filter(|row| row.id.component_type() == "branch")
            .count();
        let transformers = inputs
            .branch_switching_costs
            .iter()
            .filter(|row| row.id.component_type() == "transformer")
            .count();
        let dc_lines = network.hvdc().len();
        let devices = inputs.devices.len();

        check_finite_grid("bus vm", &network_outputs.bus_vm, periods, buses)?;
        check_finite_grid("bus va", &network_outputs.bus_va, periods, buses)?;
        check_grid("shunt step", &network_outputs.shunt_step, periods, shunts)?;
        check_grid(
            "ac line on status",
            &network_outputs.ac_line_on_status,
            periods,
            ac_lines,
        )?;
        check_finite_grid(
            "transformer tm",
            &network_outputs.transformer_tm,
            periods,
            transformers,
        )?;
        check_finite_grid(
            "transformer ta",
            &network_outputs.transformer_ta,
            periods,
            transformers,
        )?;
        check_grid(
            "transformer on status",
            &network_outputs.transformer_on_status,
            periods,
            transformers,
        )?;
        check_finite_grid(
            "dc line pdc_fr",
            &network_outputs.dc_line_pdc_fr,
            periods,
            dc_lines,
        )?;
        check_finite_grid(
            "dc line qdc_fr",
            &network_outputs.dc_line_qdc_fr,
            periods,
            dc_lines,
        )?;
        check_finite_grid(
            "dc line qdc_to",
            &network_outputs.dc_line_qdc_to,
            periods,
            dc_lines,
        )?;
        check_grid(
            "device on status",
            &device_outputs.on_status,
            periods,
            devices,
        )?;
        check_grid(
            "device startup status",
            &device_outputs.startup_status,
            periods,
            devices,
        )?;
        check_grid(
            "device shutdown status",
            &device_outputs.shutdown_status,
            periods,
            devices,
        )?;
        check_finite_grid("device p_on", &device_outputs.p_on, periods, devices)?;
        check_finite_grid("device q", &device_outputs.q, periods, devices)?;
        check_finite_grid(
            "device p_reg_res_up",
            &device_outputs.p_reg_res_up,
            periods,
            devices,
        )?;
        check_finite_grid(
            "device p_reg_res_down",
            &device_outputs.p_reg_res_down,
            periods,
            devices,
        )?;
        check_finite_grid(
            "device p_syn_res",
            &device_outputs.p_syn_res,
            periods,
            devices,
        )?;
        check_finite_grid(
            "device p_nsyn_res",
            &device_outputs.p_nsyn_res,
            periods,
            devices,
        )?;
        check_finite_grid(
            "device p_ramp_res_up_online",
            &device_outputs.p_ramp_res_up_online,
            periods,
            devices,
        )?;
        check_finite_grid(
            "device p_ramp_res_up_offline",
            &device_outputs.p_ramp_res_up_offline,
            periods,
            devices,
        )?;
        check_finite_grid(
            "device p_ramp_res_down_online",
            &device_outputs.p_ramp_res_down_online,
            periods,
            devices,
        )?;
        check_finite_grid(
            "device p_ramp_res_down_offline",
            &device_outputs.p_ramp_res_down_offline,
            periods,
            devices,
        )?;
        check_finite_grid(
            "device q_res_up",
            &device_outputs.q_res_up,
            periods,
            devices,
        )?;
        check_finite_grid(
            "device q_res_down",
            &device_outputs.q_res_down,
            periods,
            devices,
        )?;
        Ok(Self {
            instance,
            termination,
            residuals: Residuals::default(),
            producer: None,
            network_outputs,
            device_outputs,
            objective,
        })
    }

    /// The immutable instance this solution solves. Borrowed; never a copy.
    #[must_use]
    pub fn instance(&self) -> &AcScucInstance {
        &self.instance
    }

    /// The shared instance owner, for another solution of the same problem.
    #[must_use]
    pub fn shared_instance(&self) -> Arc<AcScucInstance> {
        Arc::clone(&self.instance)
    }

    /// How the producing calculation ended.
    #[must_use]
    pub const fn termination(&self) -> &Termination {
        &self.termination
    }

    /// The reported numerical residuals.
    #[must_use]
    pub const fn residuals(&self) -> &Residuals {
        &self.residuals
    }

    /// The producer or solver identity, when recorded.
    #[must_use]
    pub fn producer(&self) -> Option<&str> {
        self.producer.as_deref()
    }

    /// Record the producer identity.
    #[must_use]
    pub fn with_producer(mut self, producer: impl Into<String>) -> Self {
        self.producer = Some(producer.into());
        self
    }

    /// Record the numerical residuals.
    #[must_use]
    pub fn with_residuals(mut self, residuals: Residuals) -> Self {
        self.residuals = residuals;
        self
    }

    /// The per time point network outputs.
    #[must_use]
    pub const fn network_outputs(&self) -> &ScucNetworkOutputs {
        &self.network_outputs
    }

    /// The per time point device outputs.
    #[must_use]
    pub const fn device_outputs(&self) -> &ScucDeviceOutputs {
        &self.device_outputs
    }

    /// The reported objective value, when the producer states one.
    #[must_use]
    pub const fn objective(&self) -> Option<f64> {
        self.objective
    }
}

fn check_grid<T>(what: &str, series: &[Vec<T>], periods: usize, width: usize) -> Result<(), Error> {
    if series.is_empty() {
        return Ok(());
    }
    if series.len() != periods {
        return Err(Error::new(
            &codes::BUILD_SOLUTION_SHAPE_MISMATCH,
            format!(
                "{what} carries {} time rows; the instance states {periods} intervals",
                series.len()
            ),
        ));
    }
    if let Some((time, row)) = series
        .iter()
        .enumerate()
        .find(|(_, row)| row.len() != width)
    {
        return Err(Error::new(
            &codes::BUILD_SOLUTION_SHAPE_MISMATCH,
            format!(
                "{what} time row {time} carries {} values; the instance states {width} components",
                row.len()
            ),
        ));
    }
    Ok(())
}

fn check_finite_grid(
    what: &str,
    series: &[Vec<f64>],
    periods: usize,
    width: usize,
) -> Result<(), Error> {
    check_grid(what, series, periods, width)?;
    if let Some((time, column, value)) = series.iter().enumerate().find_map(|(time, row)| {
        row.iter()
            .copied()
            .enumerate()
            .find(|(_, value)| !value.is_finite())
            .map(|(column, value)| (time, column, value))
    }) {
        return Err(Error::new(
            &codes::BUILD_SOLUTION_SHAPE_MISMATCH,
            format!("{what}[{time}][{column}] is not finite: {value}"),
        ));
    }
    Ok(())
}

#[cfg(test)]
mod series_vocabulary_tests {
    use super::*;

    /// Exhaustive destructures with no rest binding: a field added to either
    /// struct fails this build until its name joins the series constant.
    #[test]
    fn every_output_field_is_named_in_the_series_constants() {
        let ScucNetworkOutputs {
            bus_vm: _,
            bus_va: _,
            shunt_step: _,
            ac_line_on_status: _,
            transformer_tm: _,
            transformer_ta: _,
            transformer_on_status: _,
            dc_line_pdc_fr: _,
            dc_line_qdc_fr: _,
            dc_line_qdc_to: _,
        } = ScucNetworkOutputs::default();
        assert_eq!(SCUC_NETWORK_OUTPUT_SERIES.len(), 10);

        let ScucDeviceOutputs {
            on_status: _,
            startup_status: _,
            shutdown_status: _,
            p_on: _,
            q: _,
            p_reg_res_up: _,
            p_reg_res_down: _,
            p_syn_res: _,
            p_nsyn_res: _,
            p_ramp_res_up_online: _,
            p_ramp_res_up_offline: _,
            p_ramp_res_down_online: _,
            p_ramp_res_down_offline: _,
            q_res_up: _,
            q_res_down: _,
        } = ScucDeviceOutputs::default();
        assert_eq!(SCUC_DEVICE_OUTPUT_SERIES.len(), 15);
    }
}