1use std::collections::HashSet;
4
5use powerio_prob::{ConstraintSelection, Objective, ObjectiveTerm, ReferenceBuses};
6use powerio_tx::{BalancedNetwork, BusId, BusType, IndexedNetwork};
7
8use crate::{Error, Result};
9
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12#[serde(rename_all = "snake_case")]
13#[non_exhaustive]
14pub enum PreparedObjective {
15 Feasibility,
17 #[default]
19 NetworkGeneratorCost,
20}
21
22#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
30#[non_exhaustive]
31pub struct PiecewiseLinearCost {
32 pub power: Vec<f64>,
34 pub value: Vec<f64>,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
46#[serde(rename_all = "snake_case", tag = "kind")]
47#[non_exhaustive]
48pub enum AnalysisBranchSource {
49 Branch {
50 row: usize,
51 },
52 ThreeWindingTransformerWinding {
53 transformer_row: usize,
54 winding: usize,
56 },
57}
58
59pub(crate) fn analysis_branch_sources(source: &BalancedNetwork) -> Vec<AnalysisBranchSource> {
60 let mut sources = (0..source.branches().len())
61 .map(|row| AnalysisBranchSource::Branch { row })
62 .collect::<Vec<_>>();
63 for (transformer_row, transformer) in source.transformers_3w().iter().enumerate() {
64 if !transformer.in_service {
65 continue;
66 }
67 sources.extend((0..3).map(|winding| {
68 AnalysisBranchSource::ThreeWindingTransformerWinding {
69 transformer_row,
70 winding,
71 }
72 }));
73 }
74 sources
75}
76
77pub(crate) fn compile_objective(objective: &Objective) -> Result<PreparedObjective> {
78 match objective.terms() {
79 [] => Ok(PreparedObjective::Feasibility),
80 [ObjectiveTerm::NetworkGeneratorCost] => Ok(PreparedObjective::NetworkGeneratorCost),
81 [ObjectiveTerm::ActivePowerDispatchCost] => Err(Error::UnsupportedOpfObjective {
82 reason: "`active_power_dispatch_cost` belongs to multiconductor OPF".to_owned(),
83 }),
84 _ => Err(Error::UnsupportedOpfObjective {
85 reason: "balanced OPF preparation supports either an empty objective or exactly one `network_generator_cost` term".to_owned(),
86 }),
87 }
88}
89
90pub(crate) fn row_identity(uid: Option<&str>, table: &str, row: usize) -> String {
91 uid.map_or_else(|| format!("{table}:{row}"), str::to_owned)
92}
93
94pub(crate) struct ActiveBusIndex {
98 pub analysis_rows: Vec<usize>,
99 pub dense_by_analysis: Vec<Option<usize>>,
100 pub bus_ids: Vec<BusId>,
101 pub reference_buses: ReferenceBuses,
102}
103
104fn find(parent: &mut [usize], mut node: usize) -> usize {
105 while parent[node] != node {
106 parent[node] = parent[parent[node]];
107 node = parent[node];
108 }
109 node
110}
111
112pub(crate) fn active_bus_index(case: &IndexedNetwork<'_>) -> Result<ActiveBusIndex> {
116 let mut analysis_rows = Vec::new();
117 let mut dense_by_analysis = vec![None; case.n()];
118 let mut bus_ids = Vec::new();
119 let mut reference_rows = Vec::new();
120 for (analysis_row, bus) in case.network().buses().iter().enumerate() {
121 if bus.kind == BusType::Isolated {
122 continue;
123 }
124 let dense = analysis_rows.len();
125 analysis_rows.push(analysis_row);
126 dense_by_analysis[analysis_row] = Some(dense);
127 bus_ids.push(bus.id);
128 if bus.kind == BusType::Ref {
129 reference_rows.push(dense);
130 }
131 }
132
133 let mut parent: Vec<usize> = (0..analysis_rows.len()).collect();
134 for (_, branch) in case.in_service_branches() {
135 let Some(from_analysis) = case.bus_index(branch.from) else {
136 continue;
137 };
138 let Some(to_analysis) = case.bus_index(branch.to) else {
139 continue;
140 };
141 let (Some(from), Some(to)) = (
142 dense_by_analysis[from_analysis],
143 dense_by_analysis[to_analysis],
144 ) else {
145 continue;
146 };
147 let from_root = find(&mut parent, from);
148 let to_root = find(&mut parent, to);
149 if from_root != to_root {
150 parent[to_root] = from_root;
151 }
152 }
153
154 let mut grounded = vec![false; parent.len()];
155 for &reference in &reference_rows {
156 let root = find(&mut parent, reference);
157 grounded[root] = true;
158 }
159 let mut roots = std::collections::HashSet::with_capacity(parent.len());
160 for bus in 0..parent.len() {
161 roots.insert(find(&mut parent, bus));
162 }
163 let ungrounded = roots.iter().filter(|&&root| !grounded[root]).count();
164 if ungrounded > 0 {
165 return Err(powerio_tx::Error::UngroundedComponent {
166 components: ungrounded,
167 }
168 .into());
169 }
170
171 Ok(ActiveBusIndex {
172 analysis_rows,
173 dense_by_analysis,
174 bus_ids,
175 reference_buses: ReferenceBuses::new(reference_rows),
176 })
177}
178
179pub(crate) fn constraint_mask(
182 family: &'static str,
183 selection: &ConstraintSelection,
184 all_identities: &[String],
185 active_identities: &[String],
186) -> Result<Vec<bool>> {
187 let mut declared = HashSet::with_capacity(all_identities.len());
188 for identity in all_identities {
189 if !declared.insert(identity.as_str()) {
190 return Err(Error::DuplicateElementIdentity {
191 family,
192 identity: identity.clone(),
193 });
194 }
195 }
196 if let ConstraintSelection::Only(selected) = selection {
197 for identity in selected {
198 if !declared.contains(identity.as_str()) {
199 return Err(Error::UnknownConstraintIdentity {
200 family,
201 identity: identity.clone(),
202 });
203 }
204 }
205 }
206 Ok(active_identities
207 .iter()
208 .map(|identity| selection.selects(identity))
209 .collect())
210}