powerio_prob/solution/
ots.rs1use std::sync::Arc;
13
14use powerio_core::Error;
15use powerio_tx::BalancedNetwork;
16
17use crate::diagnostics::codes;
18use crate::instance::{AcOpfInstance, AcOtsInstance, ObjectiveTerm};
19use crate::solution::{AcOpfSolution, Residuals, Termination};
20
21#[derive(Clone, Debug)]
25pub struct AcOtsSolution {
26 instance: Arc<AcOtsInstance>,
27 branch_closed: Vec<bool>,
28 ac_solution: AcOpfSolution,
29 switching_cost: f64,
30}
31
32impl AcOtsSolution {
33 pub fn new(
44 instance: Arc<AcOtsInstance>,
45 branch_closed: Vec<bool>,
46 ac_solution: AcOpfSolution,
47 ) -> Result<Self, Error> {
48 let switching_cost = instance.validate_topology(&branch_closed)?;
49 let (fixed, _) = instance.fix_topology(&branch_closed)?;
50 require_same_problem(&fixed, ac_solution.instance())?;
51 require_open_branches_idle(&instance, &branch_closed, &ac_solution)?;
52 Ok(Self {
53 instance,
54 branch_closed,
55 ac_solution,
56 switching_cost,
57 })
58 }
59
60 #[must_use]
62 pub fn instance(&self) -> &AcOtsInstance {
63 &self.instance
64 }
65
66 #[must_use]
68 pub fn shared_instance(&self) -> Arc<AcOtsInstance> {
69 Arc::clone(&self.instance)
70 }
71
72 #[must_use]
76 pub fn network(&self) -> &BalancedNetwork {
77 self.instance.network()
78 }
79
80 #[must_use]
82 pub fn branch_closed(&self) -> &[bool] {
83 &self.branch_closed
84 }
85
86 #[must_use]
88 pub const fn ac_solution(&self) -> &AcOpfSolution {
89 &self.ac_solution
90 }
91
92 #[must_use]
94 pub const fn switching_cost(&self) -> f64 {
95 self.switching_cost
96 }
97
98 #[must_use]
100 pub fn branches_open(&self) -> usize {
101 self.branch_closed.iter().filter(|closed| !**closed).count()
102 }
103
104 #[must_use]
107 pub fn branches_switched(&self) -> usize {
108 self.instance
109 .branches()
110 .iter()
111 .zip(&self.branch_closed)
112 .filter(|(branch, closed)| branch.initial_closed != **closed)
113 .count()
114 }
115
116 #[must_use]
119 pub fn objective(&self) -> f64 {
120 let switching = if self
121 .instance
122 .objective()
123 .contains(&ObjectiveTerm::BranchSwitchingCost)
124 {
125 self.switching_cost
126 } else {
127 0.0
128 };
129 self.ac_solution.objective() + switching
130 }
131
132 #[must_use]
134 pub fn termination(&self) -> &Termination {
135 self.ac_solution.termination()
136 }
137
138 #[must_use]
140 pub fn residuals(&self) -> &Residuals {
141 self.ac_solution.residuals()
142 }
143
144 #[must_use]
146 pub fn producer(&self) -> Option<&str> {
147 self.ac_solution.producer()
148 }
149}
150
151fn mismatch(message: String) -> Error {
152 Error::new(&codes::BUILD_SOLUTION_TOPOLOGY_MISMATCH, message)
153}
154
155fn require_same_problem(fixed: &AcOpfInstance, solved: &AcOpfInstance) -> Result<(), Error> {
160 if fixed.objective() != solved.objective() {
161 return Err(mismatch(
162 "the AC solution's objective differs from the fixed-topology problem's".to_owned(),
163 ));
164 }
165 if fixed.constraints() != solved.constraints() {
166 return Err(mismatch(
167 "the AC solution's constraint selections differ from the fixed-topology problem's"
168 .to_owned(),
169 ));
170 }
171 let stored = |network: &BalancedNetwork| {
172 serde_json::to_value(network)
173 .map_err(|error| mismatch(format!("a network could not be compared: {error}")))
174 };
175 if stored(fixed.network())? != stored(solved.network())? {
176 let detail = match fixed
177 .network()
178 .branches()
179 .iter()
180 .zip(solved.network().branches())
181 .position(|(a, b)| a.in_service != b.in_service)
182 {
183 Some(row) => format!(", first at branch row {row}'s status"),
184 None => String::new(),
185 };
186 return Err(mismatch(format!(
187 "the AC solution's network differs from the fixed-topology network{detail}"
188 )));
189 }
190 Ok(())
191}
192
193#[allow(clippy::float_cmp)] fn require_open_branches_idle(
195 instance: &AcOtsInstance,
196 branch_closed: &[bool],
197 solution: &AcOpfSolution,
198) -> Result<(), Error> {
199 let flows = [
200 solution.branch_from_active_flows(),
201 solution.branch_from_reactive_flows(),
202 solution.branch_to_active_flows(),
203 solution.branch_to_reactive_flows(),
204 ];
205 for (row, (branch, &closed)) in instance.branches().iter().zip(branch_closed).enumerate() {
206 if !closed
208 && flows
209 .iter()
210 .any(|column| !column[row].is_nan() && column[row] != 0.0)
211 {
212 return Err(Error::new(
213 &codes::BUILD_SOLUTION_VALUE_INVALID,
214 format!(
215 "branch `{}` is open in the final topology but carries flow",
216 branch.id
217 ),
218 ));
219 }
220 }
221 Ok(())
222}