powerio_prob/instance/
scuc.rs1use 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#[derive(Clone, Debug)]
28pub struct AcScucInstance {
29 network: BalancedNetwork,
30 inputs: ScucInputs,
31}
32
33impl AcScucInstance {
34 pub fn new(network: BalancedNetwork, inputs: ScucInputs) -> Result<Self, Error> {
44 validate_inputs(&network, &inputs)?;
45 Ok(Self { network, inputs })
46 }
47
48 #[must_use]
50 pub fn network(&self) -> &BalancedNetwork {
51 &self.network
52 }
53
54 #[must_use]
56 pub const fn inputs(&self) -> &ScucInputs {
57 &self.inputs
58 }
59
60 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 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 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 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}