use std::sync::Arc;
use powerio_core::Error;
use powerio_tx::BalancedNetwork;
use crate::diagnostics::codes;
use crate::instance::{AcOpfInstance, AcOtsInstance, ObjectiveTerm};
use crate::solution::{AcOpfSolution, Residuals, Termination};
#[derive(Clone, Debug)]
pub struct AcOtsSolution {
instance: Arc<AcOtsInstance>,
branch_closed: Vec<bool>,
ac_solution: AcOpfSolution,
switching_cost: f64,
}
impl AcOtsSolution {
pub fn new(
instance: Arc<AcOtsInstance>,
branch_closed: Vec<bool>,
ac_solution: AcOpfSolution,
) -> Result<Self, Error> {
let switching_cost = instance.validate_topology(&branch_closed)?;
let (fixed, _) = instance.fix_topology(&branch_closed)?;
require_same_problem(&fixed, ac_solution.instance())?;
require_open_branches_idle(&instance, &branch_closed, &ac_solution)?;
Ok(Self {
instance,
branch_closed,
ac_solution,
switching_cost,
})
}
#[must_use]
pub fn instance(&self) -> &AcOtsInstance {
&self.instance
}
#[must_use]
pub fn shared_instance(&self) -> Arc<AcOtsInstance> {
Arc::clone(&self.instance)
}
#[must_use]
pub fn network(&self) -> &BalancedNetwork {
self.instance.network()
}
#[must_use]
pub fn branch_closed(&self) -> &[bool] {
&self.branch_closed
}
#[must_use]
pub const fn ac_solution(&self) -> &AcOpfSolution {
&self.ac_solution
}
#[must_use]
pub const fn switching_cost(&self) -> f64 {
self.switching_cost
}
#[must_use]
pub fn branches_open(&self) -> usize {
self.branch_closed.iter().filter(|closed| !**closed).count()
}
#[must_use]
pub fn branches_switched(&self) -> usize {
self.instance
.branches()
.iter()
.zip(&self.branch_closed)
.filter(|(branch, closed)| branch.initial_closed != **closed)
.count()
}
#[must_use]
pub fn objective(&self) -> f64 {
let switching = if self
.instance
.objective()
.contains(&ObjectiveTerm::BranchSwitchingCost)
{
self.switching_cost
} else {
0.0
};
self.ac_solution.objective() + switching
}
#[must_use]
pub fn termination(&self) -> &Termination {
self.ac_solution.termination()
}
#[must_use]
pub fn residuals(&self) -> &Residuals {
self.ac_solution.residuals()
}
#[must_use]
pub fn producer(&self) -> Option<&str> {
self.ac_solution.producer()
}
}
fn mismatch(message: String) -> Error {
Error::new(&codes::BUILD_SOLUTION_TOPOLOGY_MISMATCH, message)
}
fn require_same_problem(fixed: &AcOpfInstance, solved: &AcOpfInstance) -> Result<(), Error> {
if fixed.objective() != solved.objective() {
return Err(mismatch(
"the AC solution's objective differs from the fixed-topology problem's".to_owned(),
));
}
if fixed.constraints() != solved.constraints() {
return Err(mismatch(
"the AC solution's constraint selections differ from the fixed-topology problem's"
.to_owned(),
));
}
let stored = |network: &BalancedNetwork| {
serde_json::to_value(network)
.map_err(|error| mismatch(format!("a network could not be compared: {error}")))
};
if stored(fixed.network())? != stored(solved.network())? {
let detail = match fixed
.network()
.branches()
.iter()
.zip(solved.network().branches())
.position(|(a, b)| a.in_service != b.in_service)
{
Some(row) => format!(", first at branch row {row}'s status"),
None => String::new(),
};
return Err(mismatch(format!(
"the AC solution's network differs from the fixed-topology network{detail}"
)));
}
Ok(())
}
#[allow(clippy::float_cmp)] fn require_open_branches_idle(
instance: &AcOtsInstance,
branch_closed: &[bool],
solution: &AcOpfSolution,
) -> Result<(), Error> {
let flows = [
solution.branch_from_active_flows(),
solution.branch_from_reactive_flows(),
solution.branch_to_active_flows(),
solution.branch_to_reactive_flows(),
];
for (row, (branch, &closed)) in instance.branches().iter().zip(branch_closed).enumerate() {
if !closed
&& flows
.iter()
.any(|column| !column[row].is_nan() && column[row] != 0.0)
{
return Err(Error::new(
&codes::BUILD_SOLUTION_VALUE_INVALID,
format!(
"branch `{}` is open in the final topology but carries flow",
branch.id
),
));
}
}
Ok(())
}