use crate::error::Result;
use crate::network::topology::PowerNetwork;
use crate::testcases::ieee::{ieee14, ieee30, ieee57};
#[cfg(feature = "powerflow")]
use crate::powerflow::{PowerFlowConfig, PowerFlowMethod};
#[derive(Debug, Clone)]
pub struct ExpectedPowerFlowResult {
pub converged: bool,
pub n_iterations: usize,
pub max_voltage_pu: f64,
pub min_voltage_pu: f64,
pub total_losses_mw: f64,
pub slack_generation_mw: f64,
pub tolerance: f64,
}
pub struct BenchmarkScenario {
pub name: String,
pub network: PowerNetwork,
pub expected_result: ExpectedPowerFlowResult,
}
#[derive(Debug, Clone)]
pub struct BenchmarkReport {
pub scenario_name: String,
pub passed: bool,
pub actual_converged: bool,
pub actual_iterations: usize,
pub voltage_error_pu: f64,
pub losses_error_mw: f64,
pub notes: Vec<String>,
}
impl BenchmarkReport {
pub fn is_pass(&self) -> bool {
self.passed
}
}
pub fn power_flow_benchmarks() -> Vec<BenchmarkScenario> {
let mut benchmarks = Vec::new();
if let Ok(net) = ieee14() {
benchmarks.push(BenchmarkScenario {
name: "IEEE 14-Bus".to_string(),
network: net,
expected_result: ExpectedPowerFlowResult {
converged: true,
n_iterations: 4,
max_voltage_pu: 1.060,
min_voltage_pu: 1.020,
total_losses_mw: 13.4,
slack_generation_mw: 232.4,
tolerance: 0.02,
},
});
}
if let Ok(net) = ieee30() {
benchmarks.push(BenchmarkScenario {
name: "IEEE 30-Bus".to_string(),
network: net,
expected_result: ExpectedPowerFlowResult {
converged: true,
n_iterations: 5,
max_voltage_pu: 1.060,
min_voltage_pu: 0.995,
total_losses_mw: 17.6,
slack_generation_mw: 260.2,
tolerance: 0.02,
},
});
}
if let Ok(net) = ieee57() {
benchmarks.push(BenchmarkScenario {
name: "IEEE 57-Bus".to_string(),
network: net,
expected_result: ExpectedPowerFlowResult {
converged: true,
n_iterations: 5,
max_voltage_pu: 1.040,
min_voltage_pu: 0.930,
total_losses_mw: 27.9,
slack_generation_mw: 478.9,
tolerance: 0.03,
},
});
}
benchmarks
}
#[cfg(feature = "stability")]
pub fn ieee9_stability() -> Result<(PowerNetwork, crate::stability::transient::TransientConfig)> {
use crate::network::branch::Branch;
use crate::network::bus::{Bus, BusType};
use crate::network::topology::Generator;
use crate::stability::transient::{TransientConfig, TransientEvent};
use crate::units::{Power, ReactivePower, Voltage};
let mut net = PowerNetwork::new(100.0);
let bus_info: &[(usize, BusType, f64, f64, f64)] = &[
(1, BusType::Slack, 0.0, 0.0, 1.040),
(2, BusType::PV, 0.0, 0.0, 1.025),
(3, BusType::PV, 0.0, 0.0, 1.025),
(4, BusType::PQ, 0.0, 0.0, 1.026),
(5, BusType::PQ, 125.0, 50.0, 0.996),
(6, BusType::PQ, 90.0, 30.0, 1.013),
(7, BusType::PQ, 0.0, 0.0, 1.026),
(8, BusType::PQ, 100.0, 35.0, 1.016),
(9, BusType::PQ, 0.0, 0.0, 1.032),
];
for &(id, bus_type, pd, qd, vm) in bus_info {
net.buses.push(Bus {
id,
name: format!("Bus {id}"),
bus_type,
base_kv: Voltage(230.0),
vm,
va: 0.0,
pd: Power(pd),
qd: ReactivePower(qd),
gs: 0.0,
bs: 0.0,
zone: None,
});
}
let branch_data: &[(usize, usize, f64, f64, f64)] = &[
(1, 4, 0.0, 0.0576, 0.0),
(4, 5, 0.0100, 0.0850, 0.1760),
(5, 6, 0.0170, 0.0920, 0.1580),
(3, 6, 0.0, 0.0586, 0.0),
(6, 7, 0.0390, 0.1700, 0.3580),
(7, 8, 0.0085, 0.0720, 0.1490),
(8, 2, 0.0, 0.0625, 0.0),
(8, 9, 0.0320, 0.1610, 0.3060),
(9, 4, 0.0100, 0.0850, 0.1760),
];
for &(from, to, r, x, b) in branch_data {
net.branches.push(Branch {
from_bus: from,
to_bus: to,
r,
x,
b,
rate_a: 250.0,
rate_b: 250.0,
rate_c: 250.0,
tap: 0.0,
shift: 0.0,
status: true,
});
}
let gen_data: &[(usize, f64, f64, f64, f64)] = &[
(1, 71.6, 27.0, 1.040, 247.5),
(2, 163.0, 6.7, 1.025, 192.0),
(3, 85.0, -10.9, 1.025, 128.0),
];
for &(bus_id, pg, qg, vg, pmax) in gen_data {
net.generators.push(Generator {
bus_id,
pg,
qg,
qmax: pmax * 0.5,
qmin: -pmax * 0.3,
vg,
mbase: 100.0,
status: true,
pmax,
pmin: 0.0,
});
}
let cfg = TransientConfig {
t_end: 3.0,
events: vec![
TransientEvent::FaultOn {
time: 0.1,
bus: 6,
fault_impedance: 0.0,
},
TransientEvent::FaultOff { time: 0.25, bus: 6 },
],
..TransientConfig::default()
};
Ok((net, cfg))
}
#[cfg(feature = "powerflow")]
pub fn run_benchmark(
scenario: &BenchmarkScenario,
solver: &crate::powerflow::newton_raphson::NewtonRaphsonSolver,
) -> Result<BenchmarkReport> {
use crate::powerflow::PowerFlowSolver;
let cfg = PowerFlowConfig {
method: PowerFlowMethod::NewtonRaphson,
max_iter: 50,
tolerance: 1e-6,
enforce_q_limits: false,
};
let mut notes = Vec::new();
let result = solver.solve(&scenario.network, &cfg);
match result {
Ok(pf) => {
let max_v = pf
.voltage_magnitude
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let min_v = pf
.voltage_magnitude
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
let losses = pf.total_p_loss();
let exp = &scenario.expected_result;
let tol = exp.tolerance;
let v_err = (max_v - exp.max_voltage_pu)
.abs()
.max((min_v - exp.min_voltage_pu).abs());
let l_err = (losses - exp.total_losses_mw).abs();
let passed = pf.converged == exp.converged
&& v_err <= tol
&& l_err <= exp.total_losses_mw * 0.15 + 1.0;
if !pf.converged && exp.converged {
notes.push("Solver did not converge (expected convergence)".to_string());
}
if v_err > tol {
notes.push(format!(
"Voltage error {v_err:.4} p.u. exceeds tolerance {tol:.4}"
));
}
Ok(BenchmarkReport {
scenario_name: scenario.name.clone(),
passed,
actual_converged: pf.converged,
actual_iterations: pf.iterations,
voltage_error_pu: v_err,
losses_error_mw: l_err,
notes,
})
}
Err(e) => {
notes.push(format!("Solver returned error: {e}"));
Ok(BenchmarkReport {
scenario_name: scenario.name.clone(),
passed: false,
actual_converged: false,
actual_iterations: 0,
voltage_error_pu: f64::NAN,
losses_error_mw: f64::NAN,
notes,
})
}
}
}
#[cfg(feature = "powerflow")]
pub fn validate_all_benchmarks() -> Vec<BenchmarkReport> {
use crate::powerflow::newton_raphson::NewtonRaphsonSolver;
let scenarios = power_flow_benchmarks();
let solver = NewtonRaphsonSolver;
let mut reports = Vec::new();
for scenario in &scenarios {
match run_benchmark(scenario, &solver) {
Ok(report) => reports.push(report),
Err(e) => reports.push(BenchmarkReport {
scenario_name: scenario.name.clone(),
passed: false,
actual_converged: false,
actual_iterations: 0,
voltage_error_pu: f64::NAN,
losses_error_mw: f64::NAN,
notes: vec![format!("Error: {e}")],
}),
}
}
reports
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_benchmark_suite_length() {
let suite = power_flow_benchmarks();
assert_eq!(suite.len(), 3, "Expected 3 benchmark scenarios");
}
#[test]
fn test_benchmark_scenario_names() {
let suite = power_flow_benchmarks();
let names: Vec<&str> = suite.iter().map(|s| s.name.as_str()).collect();
assert!(
names.iter().any(|n| n.contains("14")),
"Expected IEEE 14-Bus scenario"
);
assert!(
names.iter().any(|n| n.contains("30")),
"Expected IEEE 30-Bus scenario"
);
assert!(
names.iter().any(|n| n.contains("57")),
"Expected IEEE 57-Bus scenario"
);
}
#[test]
fn test_expected_results_voltage_ordering() {
let suite = power_flow_benchmarks();
for scenario in &suite {
let exp = &scenario.expected_result;
assert!(
exp.max_voltage_pu > exp.min_voltage_pu,
"Scenario '{}': max_voltage_pu ({}) must exceed min_voltage_pu ({})",
scenario.name,
exp.max_voltage_pu,
exp.min_voltage_pu
);
}
}
#[test]
fn test_expected_results_positive_tolerance() {
let suite = power_flow_benchmarks();
for scenario in &suite {
assert!(
scenario.expected_result.tolerance > 0.0,
"Scenario '{}': tolerance must be positive",
scenario.name
);
}
}
#[test]
fn test_expected_results_converged_flag() {
let suite = power_flow_benchmarks();
for scenario in &suite {
assert!(
scenario.expected_result.converged,
"Scenario '{}': expected_result.converged should be true",
scenario.name
);
}
}
#[test]
fn test_report_is_pass_true() {
let report = BenchmarkReport {
scenario_name: "test".to_string(),
passed: true,
actual_converged: true,
actual_iterations: 4,
voltage_error_pu: 0.001,
losses_error_mw: 0.5,
notes: vec![],
};
assert!(report.is_pass());
}
#[test]
fn test_report_is_pass_false() {
let report = BenchmarkReport {
scenario_name: "test".to_string(),
passed: false,
actual_converged: false,
actual_iterations: 0,
voltage_error_pu: f64::NAN,
losses_error_mw: f64::NAN,
notes: vec!["Solver error".to_string()],
};
assert!(!report.is_pass());
}
#[cfg(feature = "powerflow")]
#[test]
fn test_run_benchmark_ieee14_returns_ok() {
use crate::powerflow::newton_raphson::NewtonRaphsonSolver;
let suite = power_flow_benchmarks();
let ieee14_scenario = suite.iter().find(|s| s.name.contains("14"));
assert!(ieee14_scenario.is_some(), "IEEE 14-Bus scenario must exist");
let scenario = ieee14_scenario.expect("already checked is_some");
let solver = NewtonRaphsonSolver;
let result = run_benchmark(scenario, &solver);
assert!(
result.is_ok(),
"run_benchmark should return Ok for IEEE 14-Bus"
);
let report = result.expect("already checked is_ok");
assert_eq!(report.scenario_name, "IEEE 14-Bus");
}
#[cfg(feature = "powerflow")]
#[test]
fn test_run_benchmark_records_iterations() {
use crate::powerflow::newton_raphson::NewtonRaphsonSolver;
let suite = power_flow_benchmarks();
let scenario = suite.first().expect("at least one scenario");
let solver = NewtonRaphsonSolver;
let report = run_benchmark(scenario, &solver).expect("run_benchmark must return Ok");
if report.actual_converged {
assert!(
report.actual_iterations > 0,
"Converged report must have actual_iterations > 0"
);
}
}
#[cfg(feature = "powerflow")]
#[test]
fn test_validate_all_benchmarks_count() {
let expected_count = power_flow_benchmarks().len();
let reports = validate_all_benchmarks();
assert_eq!(
reports.len(),
expected_count,
"validate_all_benchmarks() count must match power_flow_benchmarks()"
);
}
#[cfg(feature = "stability")]
#[test]
fn test_ieee9_stability_topology() {
let result = ieee9_stability();
assert!(result.is_ok(), "ieee9_stability() must return Ok");
let (net, _cfg) = result.expect("already checked is_ok");
assert_eq!(net.buses.len(), 9, "IEEE 9-Bus must have 9 buses");
assert_eq!(net.branches.len(), 9, "IEEE 9-Bus must have 9 branches");
assert_eq!(net.generators.len(), 3, "IEEE 9-Bus must have 3 generators");
}
#[cfg(feature = "stability")]
#[test]
fn test_ieee9_stability_event_times() {
use crate::stability::transient::TransientEvent;
let result = ieee9_stability();
assert!(result.is_ok(), "ieee9_stability() must return Ok");
let (_net, cfg) = result.expect("already checked is_ok");
assert_eq!(cfg.events.len(), 2, "Must have exactly 2 transient events");
match &cfg.events[0] {
TransientEvent::FaultOn { time, .. } => {
assert_relative_eq!(*time, 0.1, epsilon = 1e-9);
}
_ => panic!("First event must be FaultOn"),
}
match &cfg.events[1] {
TransientEvent::FaultOff { time, .. } => {
assert_relative_eq!(*time, 0.25, epsilon = 1e-9);
}
_ => panic!("Second event must be FaultOff"),
}
}
#[cfg(feature = "powerflow")]
#[test]
fn test_run_benchmark_voltage_error_not_nan_on_convergence() {
use crate::powerflow::newton_raphson::NewtonRaphsonSolver;
let suite = power_flow_benchmarks();
let solver = NewtonRaphsonSolver;
for scenario in &suite {
let report = run_benchmark(scenario, &solver).expect("run_benchmark must return Ok");
if report.actual_converged {
assert!(
!report.voltage_error_pu.is_nan(),
"Scenario '{}': voltage_error_pu must not be NaN on convergence",
report.scenario_name
);
assert!(
!report.losses_error_mw.is_nan(),
"Scenario '{}': losses_error_mw must not be NaN on convergence",
report.scenario_name
);
}
}
}
#[test]
fn test_reference_n_iterations_positive() {
let suite = power_flow_benchmarks();
for scenario in &suite {
assert!(
scenario.expected_result.n_iterations > 0,
"Scenario '{}': n_iterations must be > 0",
scenario.name
);
}
}
#[test]
fn test_reference_slack_generation_positive() {
let suite = power_flow_benchmarks();
for scenario in &suite {
assert!(
scenario.expected_result.slack_generation_mw > 0.0,
"Scenario '{}': slack_generation_mw ({}) must be positive",
scenario.name,
scenario.expected_result.slack_generation_mw
);
}
}
#[test]
fn test_reference_total_losses_positive() {
let suite = power_flow_benchmarks();
for scenario in &suite {
assert!(
scenario.expected_result.total_losses_mw > 0.0,
"Scenario '{}': total_losses_mw ({}) must be positive",
scenario.name,
scenario.expected_result.total_losses_mw
);
}
}
#[test]
fn test_benchmark_report_clone_and_debug() {
let report = BenchmarkReport {
scenario_name: "clone-test".to_string(),
passed: true,
actual_converged: true,
actual_iterations: 3,
voltage_error_pu: 0.005,
losses_error_mw: 1.2,
notes: vec!["note1".to_string()],
};
let cloned = report.clone();
assert_eq!(cloned.scenario_name, report.scenario_name);
assert_eq!(cloned.passed, report.passed);
let _ = format!("{:?}", report);
}
#[test]
fn test_expected_result_clone_and_debug() {
let exp = ExpectedPowerFlowResult {
converged: true,
n_iterations: 5,
max_voltage_pu: 1.05,
min_voltage_pu: 0.95,
total_losses_mw: 20.0,
slack_generation_mw: 250.0,
tolerance: 0.02,
};
let cloned = exp.clone();
assert_relative_eq!(cloned.max_voltage_pu, exp.max_voltage_pu, epsilon = 1e-12);
assert_relative_eq!(cloned.tolerance, exp.tolerance, epsilon = 1e-12);
let _ = format!("{:?}", exp);
}
#[cfg(feature = "powerflow")]
#[test]
fn test_validate_all_benchmarks_nonempty_names() {
let reports = validate_all_benchmarks();
for report in &reports {
assert!(
!report.scenario_name.is_empty(),
"validate_all_benchmarks: scenario_name must not be empty"
);
}
}
#[cfg(feature = "powerflow")]
#[test]
fn test_validate_all_benchmarks_names_match_scenarios() {
let scenario_names: Vec<String> = power_flow_benchmarks()
.into_iter()
.map(|s| s.name)
.collect();
let reports = validate_all_benchmarks();
let report_names: Vec<&str> = reports.iter().map(|r| r.scenario_name.as_str()).collect();
for name in &scenario_names {
assert!(
report_names.iter().any(|rn| rn == name),
"Expected report for scenario '{}' but not found",
name
);
}
}
#[cfg(feature = "powerflow")]
#[test]
fn test_run_benchmark_ieee30_returns_ok() {
use crate::powerflow::newton_raphson::NewtonRaphsonSolver;
let suite = power_flow_benchmarks();
let scenario = suite
.iter()
.find(|s| s.name.contains("30"))
.expect("IEEE 30-Bus scenario must exist");
let solver = NewtonRaphsonSolver;
let result = run_benchmark(scenario, &solver);
assert!(
result.is_ok(),
"run_benchmark must return Ok for IEEE 30-Bus"
);
let report = result.expect("already checked is_ok");
assert_eq!(report.scenario_name, "IEEE 30-Bus");
}
#[cfg(feature = "stability")]
#[test]
fn test_ieee9_stability_generator_bus_ids() {
let result = ieee9_stability();
assert!(result.is_ok(), "ieee9_stability() must return Ok");
let (net, _cfg) = result.expect("already checked is_ok");
let bus_ids: Vec<usize> = net.generators.iter().map(|g| g.bus_id).collect();
assert!(bus_ids.contains(&1), "Generator at bus 1 must exist");
assert!(bus_ids.contains(&2), "Generator at bus 2 must exist");
assert!(bus_ids.contains(&3), "Generator at bus 3 must exist");
}
#[cfg(feature = "stability")]
#[test]
fn test_ieee9_stability_t_end_boundary() {
let result = ieee9_stability();
assert!(result.is_ok(), "ieee9_stability() must return Ok");
let (_net, cfg) = result.expect("already checked is_ok");
assert_relative_eq!(cfg.t_end, 3.0, epsilon = 1e-12);
}
#[test]
fn test_benchmark_report_notes_preserved_via_clone() {
let notes = vec![
"Voltage error exceeded".to_string(),
"Solver did not converge".to_string(),
];
let report = BenchmarkReport {
scenario_name: "notes-test".to_string(),
passed: false,
actual_converged: false,
actual_iterations: 0,
voltage_error_pu: f64::NAN,
losses_error_mw: f64::NAN,
notes: notes.clone(),
};
let cloned = report.clone();
assert_eq!(cloned.notes.len(), 2);
assert_eq!(cloned.notes[0], "Voltage error exceeded");
assert_eq!(cloned.notes[1], "Solver did not converge");
}
#[test]
fn test_benchmark_scenario_bus_counts() {
let suite = power_flow_benchmarks();
for scenario in &suite {
let bus_count = scenario.network.buses.len();
assert!(
bus_count > 0,
"Scenario '{}': network must have at least 1 bus",
scenario.name
);
if scenario.name.contains("14") {
assert_eq!(
bus_count, 14,
"IEEE 14-Bus scenario must have exactly 14 buses"
);
} else if scenario.name.contains("30") {
assert_eq!(
bus_count, 30,
"IEEE 30-Bus scenario must have exactly 30 buses"
);
} else if scenario.name.contains("57") {
assert_eq!(
bus_count, 57,
"IEEE 57-Bus scenario must have exactly 57 buses"
);
}
}
}
#[test]
fn test_expected_voltage_range_is_physical() {
let suite = power_flow_benchmarks();
for scenario in &suite {
let exp = &scenario.expected_result;
assert!(
exp.max_voltage_pu > 0.5,
"Scenario '{}': max_voltage_pu ({}) must be > 0.5",
scenario.name,
exp.max_voltage_pu
);
assert!(
exp.max_voltage_pu < 1.5,
"Scenario '{}': max_voltage_pu ({}) must be < 1.5",
scenario.name,
exp.max_voltage_pu
);
assert!(
exp.min_voltage_pu > 0.5,
"Scenario '{}': min_voltage_pu ({}) must be > 0.5",
scenario.name,
exp.min_voltage_pu
);
assert!(
exp.min_voltage_pu < 1.5,
"Scenario '{}': min_voltage_pu ({}) must be < 1.5",
scenario.name,
exp.min_voltage_pu
);
}
}
#[test]
fn test_report_notes_vec_is_empty_by_default() {
let report = BenchmarkReport {
scenario_name: "empty-notes-test".to_string(),
passed: true,
actual_converged: true,
actual_iterations: 5,
voltage_error_pu: 0.001,
losses_error_mw: 0.5,
notes: vec![],
};
assert!(
report.notes.is_empty(),
"BenchmarkReport notes should be empty when constructed with vec![]"
);
assert_eq!(report.notes.len(), 0);
}
#[test]
fn test_expected_result_n_iterations_in_reasonable_range() {
let suite = power_flow_benchmarks();
for scenario in &suite {
let n = scenario.expected_result.n_iterations;
assert!(
(1..=20).contains(&n),
"Scenario '{}': n_iterations ({}) must be in range 1..=20",
scenario.name,
n
);
}
}
#[test]
fn test_benchmark_scenario_network_has_branches() {
let suite = power_flow_benchmarks();
for scenario in &suite {
assert!(
!scenario.network.branches.is_empty(),
"Scenario '{}': network must have at least 1 branch",
scenario.name
);
}
}
#[test]
fn test_benchmark_report_with_zero_voltage_error_passes_sanity() {
let report = BenchmarkReport {
scenario_name: "perfect-solver".to_string(),
passed: true,
actual_converged: true,
actual_iterations: 4,
voltage_error_pu: 0.0,
losses_error_mw: 0.0,
notes: vec![],
};
assert!(
report.is_pass(),
"Report with passed=true must return is_pass()=true"
);
assert!(
report.voltage_error_pu == 0.0,
"voltage_error_pu should be exactly 0.0"
);
assert!(
report.losses_error_mw == 0.0,
"losses_error_mw should be exactly 0.0"
);
}
}