use std::collections::HashMap;
use std::fmt;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::measurement::{Measurement, MeasurementKind, Target};
use crate::types::Bus;
use super::SeNetwork;
use super::nr::{PersistentEstimator, SeOptions, SeReport, linear_start};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct MeasurementOverride {
pub measurement: usize,
pub value: Option<f64>,
pub sigma: Option<f64>,
}
impl MeasurementOverride {
pub fn new(measurement: usize) -> Self {
Self {
measurement,
value: None,
sigma: None,
}
}
pub fn value(mut self, value: f64) -> Self {
self.value = Some(value);
self
}
pub fn sigma(mut self, sigma: f64) -> Self {
self.sigma = Some(sigma);
self
}
pub fn absent(mut self) -> Self {
self.sigma = Some(f64::INFINITY);
self
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SeScenario {
pub overrides: Vec<MeasurementOverride>,
}
impl SeScenario {
pub fn new(overrides: Vec<MeasurementOverride>) -> Self {
Self { overrides }
}
}
#[derive(Clone, Debug)]
pub struct SeBatchResult {
pub buses: Vec<Bus>,
pub report: SeReport,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SeBatchError {
MeasurementOutOfRange {
scenario: usize,
measurement: usize,
n: usize,
},
AngleReferenceChanged { scenario: usize },
IncompatibleScenarios,
ThreadPool(String),
}
impl fmt::Display for SeBatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SeBatchError::MeasurementOutOfRange {
scenario,
measurement,
n,
} => write!(
f,
"scenario {scenario} overrides measurement {measurement}, but the template has \
only {n} measurement(s)"
),
SeBatchError::AngleReferenceChanged { scenario } => write!(
f,
"scenario {scenario} changes whether any voltage angle is measured, which changes \
the number of unknowns — run it as its own batch"
),
SeBatchError::IncompatibleScenarios => {
write!(f, "no single measurement template covers every scenario")
}
SeBatchError::ThreadPool(e) => write!(f, "building the rayon thread pool failed: {e}"),
}
}
}
impl std::error::Error for SeBatchError {}
fn angle_is_measured(measurements: &[Measurement]) -> bool {
measurements
.iter()
.any(|m| m.kind == MeasurementKind::VoltageAngle && m.weight() > 0.0)
}
fn apply(template: &[Measurement], scenario: &SeScenario) -> Vec<Measurement> {
let mut out = template.to_vec();
for ov in &scenario.overrides {
let m = &mut out[ov.measurement];
if let Some(v) = ov.value {
m.value = v;
}
if let Some(s) = ov.sigma {
m.sigma = s;
}
}
out
}
pub fn align_scenarios(
scenarios: &[Vec<Measurement>],
) -> Result<(Vec<Measurement>, Vec<SeScenario>), SeBatchError> {
if scenarios.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let mut index: HashMap<(MeasurementKind, Target), usize> = HashMap::new();
let mut template: Vec<Measurement> = Vec::new();
for scenario in scenarios {
for m in scenario {
index.entry((m.kind, m.target)).or_insert_with(|| {
template.push(*m);
template.len() - 1
});
}
}
let mut out = Vec::with_capacity(scenarios.len());
for scenario in scenarios {
let mut present = vec![false; template.len()];
let mut overrides = Vec::with_capacity(template.len());
for m in scenario {
let i = index[&(m.kind, m.target)];
if present[i] {
return Err(SeBatchError::IncompatibleScenarios);
}
present[i] = true;
overrides.push(MeasurementOverride::new(i).value(m.value).sigma(m.sigma));
}
for (i, &seen) in present.iter().enumerate() {
if !seen {
overrides.push(MeasurementOverride::new(i).absent());
}
}
overrides.sort_unstable_by_key(|o| o.measurement);
out.push(SeScenario::new(overrides));
}
Ok((template, out))
}
pub struct SeBatchSolver {
options: SeOptions,
pool: Option<rayon::ThreadPool>,
}
impl SeBatchSolver {
pub fn new(options: SeOptions) -> Self {
Self {
options,
pool: None,
}
}
pub fn with_threads(options: SeOptions, threads: usize) -> Result<Self, SeBatchError> {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.map_err(|e| SeBatchError::ThreadPool(e.to_string()))?;
Ok(Self {
options,
pool: Some(pool),
})
}
pub fn threads(&self) -> usize {
match &self.pool {
Some(p) => p.current_num_threads(),
None => rayon::current_num_threads(),
}
}
pub fn estimate(
&self,
buses_template: &[Bus],
net: &SeNetwork,
measurements_template: &[Measurement],
scenarios: &[SeScenario],
) -> Result<Vec<SeBatchResult>, SeBatchError> {
let n = measurements_template.len();
let template_has_angle = angle_is_measured(measurements_template);
for (i, sc) in scenarios.iter().enumerate() {
for ov in &sc.overrides {
if ov.measurement >= n {
return Err(SeBatchError::MeasurementOutOfRange {
scenario: i,
measurement: ov.measurement,
n,
});
}
}
if angle_is_measured(&apply(measurements_template, sc)) != template_has_angle {
return Err(SeBatchError::AngleReferenceChanged { scenario: i });
}
}
if scenarios.is_empty() {
return Ok(Vec::new());
}
let next = AtomicUsize::new(0);
let collected: Mutex<Vec<(usize, SeBatchResult)>> =
Mutex::new(Vec::with_capacity(scenarios.len()));
let n_workers = self.threads().max(1).min(scenarios.len());
let options = self.options;
let run = || {
rayon::scope(|s| {
for _ in 0..n_workers {
s.spawn(|_| {
let mut estimator = PersistentEstimator::new(options);
loop {
let i = next.fetch_add(1, Ordering::Relaxed);
if i >= scenarios.len() {
break;
}
let measurements = apply(measurements_template, &scenarios[i]);
let mut buses = buses_template.to_vec();
linear_start(&mut buses, net, &measurements);
let report = estimator.estimate(&measurements, &mut buses, net);
collected
.lock()
.expect("batch result mutex poisoned by a panicking worker")
.push((i, SeBatchResult { buses, report }));
}
});
}
});
};
match &self.pool {
Some(pool) => pool.install(run),
None => run(),
}
let mut out = collected.into_inner().expect("batch result mutex poisoned");
out.sort_by_key(|(i, _)| *i);
Ok(out.into_iter().map(|(_, r)| r).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::measurement::Target;
fn m(kind: MeasurementKind, target: Target, value: f64, sigma: f64) -> Measurement {
Measurement {
kind,
target,
value,
sigma,
}
}
#[test]
fn align_unions_scenarios_and_marks_absent_rows() {
let a = vec![
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 1.0, 0.1),
m(MeasurementKind::ActivePower, Target::Bus(1), 0.5, 0.2),
];
let b = vec![
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 1.1, 0.1),
m(MeasurementKind::ReactivePower, Target::Bus(1), 0.3, 0.2),
];
let (template, scenarios) = align_scenarios(&[a, b]).unwrap();
assert_eq!(template.len(), 3, "union of two rows and one shared");
assert_eq!(template[0].kind, MeasurementKind::VoltageMagnitude);
assert_eq!(template[1].kind, MeasurementKind::ActivePower);
assert_eq!(template[2].kind, MeasurementKind::ReactivePower);
let first = apply(&template, &scenarios[0]);
assert_eq!(first[0].value, 1.0);
assert_eq!(first[1].value, 0.5);
assert!(
first[2].sigma.is_infinite(),
"scenario 0 has no reactive row"
);
assert_eq!(first[2].weight(), 0.0);
let second = apply(&template, &scenarios[1]);
assert_eq!(second[0].value, 1.1);
assert!(
second[1].sigma.is_infinite(),
"scenario 1 has no active row"
);
assert_eq!(second[2].value, 0.3);
}
#[test]
fn a_single_scenario_round_trips_exactly() {
let one = vec![
m(MeasurementKind::ActivePower, Target::Bus(2), 0.7, 0.3),
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 1.0, 0.1),
];
let (template, scenarios) = align_scenarios(std::slice::from_ref(&one)).unwrap();
assert_eq!(apply(&template, &scenarios[0]), one);
}
#[test]
fn a_duplicated_quantity_in_one_scenario_is_rejected() {
let dup = vec![
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 1.0, 0.1),
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 1.2, 0.1),
];
assert_eq!(
align_scenarios(&[dup]),
Err(SeBatchError::IncompatibleScenarios)
);
}
}