1use std::collections::BTreeSet;
13
14use super::sub::SubsystemSet;
15use super::{
16 AutomaticOrder, AutomaticSpec, AutomaticTarget, ContingencyAction, ContingencyCase,
17 ContingencySet, PsseEquipmentIndex, SkipRule,
18};
19use crate::diagnostics::{Diagnostic, codes};
20use crate::network::{BalancedNetwork, BusId, Transformer3W};
21
22#[derive(Debug, Clone)]
31#[non_exhaustive]
32pub struct Expanded {
33 pub set: ContingencySet,
34 pub diagnostics: Vec<Diagnostic>,
35}
36
37impl ContingencySet {
38 #[must_use]
55 pub fn expand(&self, net: &BalancedNetwork, subsystems: &SubsystemSet) -> Expanded {
56 self.expand_with(&PsseEquipmentIndex::new(net), subsystems)
57 }
58
59 #[must_use]
65 pub fn expand_with(
66 &self,
67 index: &PsseEquipmentIndex<'_>,
68 subsystems: &SubsystemSet,
69 ) -> Expanded {
70 let net = index.network();
71 let mut diagnostics = Vec::new();
72 let mut kept = Vec::new();
73 let mut generated = Vec::new();
74 for spec in &self.automatic {
75 let Some(subsystem) = subsystems.get(&spec.subsystem) else {
76 diagnostics.push(Diagnostic::of(
77 &codes::BUILD_CON_SUBSYSTEM_UNKNOWN,
78 format!(
79 "{}: the subsystem set states no subsystem '{}'",
80 describe(spec),
81 spec.subsystem
82 ),
83 ));
84 kept.push(spec.clone());
85 continue;
86 };
87 let buses = subsystem.select_buses(net);
88 let singles = single_cases(net, index, spec, &buses, &self.skips);
89 let needed = match spec.order {
90 AutomaticOrder::Single => 1,
91 AutomaticOrder::Double => 2,
92 };
93 let eligible = singles.len();
94 if eligible < needed {
95 let plural = if eligible == 1 { "element" } else { "elements" };
96 diagnostics.push(Diagnostic::of(
97 &codes::BUILD_CON_SPECIFICATION_EMPTY,
98 format!(
99 "{}: subsystem '{}' holds {eligible} in service {plural} this specification names, and its order needs {needed}",
100 describe(spec),
101 spec.subsystem
102 ),
103 ));
104 }
105 match spec.order {
106 AutomaticOrder::Single => generated.extend(singles),
107 AutomaticOrder::Double => generated.extend(double_cases(&singles)),
108 }
109 }
110 let mut cases = self.cases.clone();
111 cases.extend(generated);
112 let skips = if kept.is_empty() && !self.automatic.is_empty() {
116 Vec::new()
117 } else {
118 self.skips.clone()
119 };
120 Expanded {
121 set: ContingencySet {
122 header: self.header.clone(),
123 cases,
124 automatic: kept,
125 skips,
126 retained: self.retained.clone(),
127 },
128 diagnostics,
129 }
130 }
131}
132
133fn describe(spec: &AutomaticSpec) -> String {
135 let order = match spec.order {
136 AutomaticOrder::Single => "SINGLE",
137 AutomaticOrder::Double => "DOUBLE",
138 };
139 let target = match spec.target {
140 AutomaticTarget::Branch => "BRANCH",
141 AutomaticTarget::Unit => "UNIT",
142 AutomaticTarget::Tie => "TIE",
143 };
144 format!("{order} {target}")
145}
146
147fn single_cases(
149 net: &BalancedNetwork,
150 index: &PsseEquipmentIndex,
151 spec: &AutomaticSpec,
152 buses: &BTreeSet<BusId>,
153 skips: &[SkipRule],
154) -> Vec<ContingencyCase> {
155 let mut cases = Vec::new();
156 match spec.target {
157 AutomaticTarget::Branch | AutomaticTarget::Tie => {
158 let tie = spec.target == AutomaticTarget::Tie;
159 for (row, branch) in net.branches().iter().enumerate() {
160 let inside = usize::from(buses.contains(&branch.from))
161 + usize::from(buses.contains(&branch.to));
162 let wanted = if tie { inside == 1 } else { inside == 2 };
163 if !branch.in_service || !wanted {
164 continue;
165 }
166 let circuit = index.circuit_ids()[row].trim();
167 if skipped(skips, branch.from, branch.to, circuit) {
168 continue;
169 }
170 cases.push(ContingencyCase {
171 name: format!("L_{}_{}_{circuit}", branch.from.0, branch.to.0),
172 actions: vec![ContingencyAction::OpenBranch {
173 from: branch.from,
174 to: branch.to,
175 circuit: circuit.to_owned(),
176 }],
177 });
178 }
179 if spec.low_voltage_3w && !tie {
180 cases.extend(three_winding_cases(net, index, buses));
181 }
182 }
183 AutomaticTarget::Unit => {
184 for (row, generator) in net.generators().iter().enumerate() {
185 if !generator.in_service || !buses.contains(&generator.bus) {
186 continue;
187 }
188 let id = index.machine_ids()[row].trim();
189 cases.push(ContingencyCase {
190 name: format!("G_{}_{id}", generator.bus.0),
191 actions: vec![ContingencyAction::RemoveMachine {
192 bus: generator.bus,
193 id: id.to_owned(),
194 }],
195 });
196 }
197 }
198 }
199 cases
200}
201
202fn three_winding_cases(
205 net: &BalancedNetwork,
206 index: &PsseEquipmentIndex,
207 buses: &BTreeSet<BusId>,
208) -> Vec<ContingencyCase> {
209 let mut cases = Vec::new();
210 for (row, transformer) in net.transformers_3w().iter().enumerate() {
211 if !transformer.in_service || !buses.contains(&low_voltage_bus(net, index, transformer)) {
212 continue;
213 }
214 let circuit = index.transformer_3w_ids()[row].trim();
215 let terminals = [
216 transformer.windings[0].bus,
217 transformer.windings[1].bus,
218 transformer.windings[2].bus,
219 ];
220 cases.push(ContingencyCase {
221 name: format!(
222 "T_{}_{}_{}_{circuit}",
223 terminals[0].0, terminals[1].0, terminals[2].0
224 ),
225 actions: vec![ContingencyAction::OpenThreeWinding {
226 buses: terminals,
227 circuit: circuit.to_owned(),
228 }],
229 });
230 }
231 cases
232}
233
234fn double_cases(singles: &[ContingencyCase]) -> Vec<ContingencyCase> {
237 let mut cases = Vec::new();
238 for (position, first) in singles.iter().enumerate() {
239 for second in &singles[position + 1..] {
240 let mut actions = first.actions.clone();
241 actions.extend(second.actions.iter().cloned());
242 cases.push(ContingencyCase {
243 name: format!("{}+{}", first.name, second.name),
244 actions,
245 });
246 }
247 }
248 cases
249}
250
251fn skipped(skips: &[SkipRule], from: BusId, to: BusId, circuit: &str) -> bool {
254 skips.iter().any(|rule| {
255 rule.circuit.trim() == circuit
256 && ((rule.from == from && rule.to == to) || (rule.from == to && rule.to == from))
257 })
258}
259
260pub(super) fn low_voltage_bus(
264 net: &BalancedNetwork,
265 index: &PsseEquipmentIndex,
266 transformer: &Transformer3W,
267) -> BusId {
268 let kv_of = |winding: &crate::network::Winding| {
269 if winding.nominal_kv > 0.0 {
270 return winding.nominal_kv;
271 }
272 index
273 .bus_row(winding.bus)
274 .map_or(0.0, |row| net.buses()[row].base_kv)
275 };
276 let mut lowest = &transformer.windings[0];
277 for winding in &transformer.windings[1..] {
278 if kv_of(winding) < kv_of(lowest) {
279 lowest = winding;
280 }
281 }
282 lowest.bus
283}