Skip to main content

powerio_prob/instance/
scuc.rs

1//! The AC security constrained unit commitment instance, following the DOE
2//! GO Challenge 3 mathematical definition and data format.
3//!
4//! `AcScucInstance` contains the balanced electrical network plus the
5//! inputs used by the Challenge 3 formulation: interval durations, initial
6//! commitment durations, time varying bounds, cost blocks, reserve zones and memberships,
7//! energy requirements,
8//! contingencies, and violation costs — carried by [`ScucInputs`]. The GO
9//! Challenge 3 grid exchange format also defines optional data outside that
10//! formulation; such fields do not change the meaning of this type. Asking only for the
11//! network discards scheduling, reserve, and contingency data; the
12//! transformations below report those omissions.
13
14use std::collections::HashSet;
15
16use powerio_core::{ComponentId, Error};
17use powerio_tx::BalancedNetwork;
18
19use crate::diagnostics::codes;
20use crate::instance::balanced::{
21    AcOpfInstance, AcPfInstance, DcOpfInstance, DcPfInstance, transform_discarded,
22};
23use crate::instance::scuc_inputs::ScucInputs;
24
25/// The AC security constrained unit commitment instance: the shared balanced
26/// network plus the Challenge 3 formulation inputs.
27#[derive(Clone, Debug)]
28pub struct AcScucInstance {
29    network: BalancedNetwork,
30    inputs: ScucInputs,
31}
32
33impl AcScucInstance {
34    /// Assemble the instance from a network and typed AC SCUC inputs. The
35    /// GO Challenge 3 reader that builds both halves from one
36    /// document owns identity reconciliation; this constructor checks the
37    /// identities and time dimensions that must agree for the pair to describe
38    /// one calculation.
39    ///
40    /// # Errors
41    /// A referenced component is absent, an identity is repeated, or a time
42    /// varying record disagrees with the horizon.
43    pub fn new(network: BalancedNetwork, inputs: ScucInputs) -> Result<Self, Error> {
44        validate_inputs(&network, &inputs)?;
45        Ok(Self { network, inputs })
46    }
47
48    /// The balanced network this instance schedules. Borrowed; never a copy.
49    #[must_use]
50    pub fn network(&self) -> &BalancedNetwork {
51        &self.network
52    }
53
54    /// The Challenge 3 formulation inputs.
55    #[must_use]
56    pub const fn inputs(&self) -> &ScucInputs {
57        &self.inputs
58    }
59
60    /// The single period AC OPF instance this problem's network implies. The
61    /// scheduling categories are discarded and the discard is recorded.
62    ///
63    /// # Errors
64    /// As [`AcOpfInstance::from_network`].
65    pub fn to_ac_opf(&self) -> Result<(AcOpfInstance, Vec<powerio_core::Diagnostic>), Error> {
66        let instance = AcOpfInstance::from_network(self.network.clone())?;
67        Ok((instance, vec![scheduling_discarded()]))
68    }
69
70    /// The single period DC OPF instance this problem's network implies.
71    ///
72    /// # Errors
73    /// As [`DcOpfInstance::from_network`].
74    pub fn to_dc_opf(&self) -> Result<(DcOpfInstance, Vec<powerio_core::Diagnostic>), Error> {
75        let instance = DcOpfInstance::from_network(self.network.clone())?;
76        Ok((instance, vec![scheduling_discarded()]))
77    }
78
79    /// The AC power flow instance for this problem's network at its stated
80    /// injections and setpoints.
81    ///
82    /// # Errors
83    /// As [`AcPfInstance::from_network`].
84    pub fn to_ac_pf(&self) -> Result<(AcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
85        let instance = AcPfInstance::from_network(self.network.clone())?;
86        Ok((instance, vec![scheduling_discarded()]))
87    }
88
89    /// The DC power flow instance for this problem's network at its stated
90    /// injections.
91    ///
92    /// # Errors
93    /// As [`DcPfInstance::from_network`].
94    pub fn to_dc_pf(&self) -> Result<(DcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
95        let instance = DcPfInstance::from_network(self.network.clone())?;
96        Ok((instance, vec![scheduling_discarded()]))
97    }
98}
99
100#[allow(clippy::too_many_lines)]
101fn validate_inputs(network: &BalancedNetwork, inputs: &ScucInputs) -> Result<(), Error> {
102    let periods = inputs.interval_durations.len();
103    if periods == 0
104        || inputs
105            .interval_durations
106            .iter()
107            .any(|duration| !duration.is_finite() || *duration <= 0.0)
108    {
109        return Err(Error::new(
110            &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
111            "the AC SCUC horizon must contain positive finite interval durations",
112        ));
113    }
114
115    let mut ids = HashSet::new();
116    for device in &inputs.devices {
117        require_unique(&mut ids, &device.id)?;
118        if device.periods.len() != periods {
119            return Err(shape_error(format!(
120                "device {} has {} intervals; expected {periods}",
121                device.id,
122                device.periods.len()
123            )));
124        }
125        let found = match device.kind {
126            super::scuc_inputs::ScucDeviceKind::Producer => {
127                device.id.component_type() == "generator"
128                    && network
129                        .generators()
130                        .iter()
131                        .any(|component| component.uid.as_deref() == Some(device.id.local_id()))
132            }
133            super::scuc_inputs::ScucDeviceKind::Consumer => {
134                device.id.component_type() == "load"
135                    && network
136                        .loads()
137                        .iter()
138                        .any(|component| component.uid.as_deref() == Some(device.id.local_id()))
139            }
140        };
141        if !found {
142            return Err(shape_error(format!(
143                "device {} has no matching component in the balanced network",
144                device.id
145            )));
146        }
147    }
148    for shunt in &inputs.shunts {
149        require_unique(&mut ids, &shunt.id)?;
150        if shunt.id.component_type() != "shunt"
151            || !network
152                .shunts()
153                .iter()
154                .any(|component| component.uid.as_deref() == Some(shunt.id.local_id()))
155        {
156            return Err(shape_error(format!(
157                "shunt {} has no matching component in the balanced network",
158                shunt.id
159            )));
160        }
161    }
162    for switching in &inputs.branch_switching_costs {
163        require_branch(network, &switching.id)?;
164    }
165    for control in &inputs.transformer_controls {
166        require_branch(network, &control.id)?;
167    }
168    for zone in &inputs.active_reserve_zones {
169        require_unique(&mut ids, &zone.id)?;
170        require_periods(
171            &zone.id,
172            "ramping up requirement",
173            zone.ramping_up_requirement.len(),
174            periods,
175        )?;
176        require_periods(
177            &zone.id,
178            "ramping down requirement",
179            zone.ramping_down_requirement.len(),
180            periods,
181        )?;
182        require_buses(network, &zone.buses)?;
183    }
184    for zone in &inputs.reactive_reserve_zones {
185        require_unique(&mut ids, &zone.id)?;
186        require_periods(
187            &zone.id,
188            "reactive up requirement",
189            zone.reactive_up_requirement.len(),
190            periods,
191        )?;
192        require_periods(
193            &zone.id,
194            "reactive down requirement",
195            zone.reactive_down_requirement.len(),
196            periods,
197        )?;
198        require_buses(network, &zone.buses)?;
199    }
200    for contingency in &inputs.contingencies {
201        require_unique(&mut ids, &contingency.id)?;
202        if contingency.components.is_empty() {
203            return Err(shape_error(format!(
204                "contingency {} contains no component",
205                contingency.id
206            )));
207        }
208        for component in &contingency.components {
209            require_branch(network, component)?;
210        }
211    }
212    Ok(())
213}
214
215fn scheduling_discarded() -> powerio_core::Diagnostic {
216    transform_discarded(
217        "the scheduling, reserve, contingency, energy window, and violation cost categories",
218    )
219}
220
221fn shape_error(message: impl Into<String>) -> Error {
222    Error::new(&codes::BUILD_INSTANCE_SHAPE_MISMATCH, message)
223}
224
225fn require_unique(ids: &mut HashSet<ComponentId>, id: &ComponentId) -> Result<(), Error> {
226    if ids.insert(id.clone()) {
227        Ok(())
228    } else {
229        Err(shape_error(format!("component identity {id} is repeated")))
230    }
231}
232
233fn require_periods(
234    id: &ComponentId,
235    field: &str,
236    actual: usize,
237    expected: usize,
238) -> Result<(), Error> {
239    if actual == expected {
240        Ok(())
241    } else {
242        Err(shape_error(format!(
243            "{id} {field} has {actual} intervals; expected {expected}"
244        )))
245    }
246}
247
248fn require_buses(network: &BalancedNetwork, buses: &[ComponentId]) -> Result<(), Error> {
249    for id in buses {
250        if id.component_type() != "bus"
251            || !network
252                .buses()
253                .iter()
254                .any(|component| component.uid.as_deref() == Some(id.local_id()))
255        {
256            return Err(shape_error(format!(
257                "reserve zone bus {id} has no matching bus in the balanced network"
258            )));
259        }
260    }
261    Ok(())
262}
263
264fn require_branch(network: &BalancedNetwork, id: &ComponentId) -> Result<(), Error> {
265    let component_type = id.component_type();
266    let found = match component_type {
267        "branch" | "transformer" => network
268            .branches()
269            .iter()
270            .any(|component| component.uid.as_deref() == Some(id.local_id())),
271        "hvdc" => network
272            .hvdc()
273            .iter()
274            .any(|component| component.uid.as_deref() == Some(id.local_id())),
275        _ => false,
276    };
277    if found {
278        Ok(())
279    } else {
280        Err(shape_error(format!(
281            "{id} has no matching branch in the balanced network"
282        )))
283    }
284}