use std::path::PathBuf;
use gridoxide::rao::crac::*;
use gridoxide::rao::{Network, Resolution, crac_json, evaluate};
use gridoxide::ucte;
mod openrao;
fn ucte_fixture(name: &str) -> PathBuf {
openrao::fixture(name)
}
fn rao_fixture(name: &str) -> PathBuf {
openrao::fixture(name)
}
fn reference_file(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/ucte")
.join(name)
}
struct Case {
net: ucte::UcteImport,
crac: Crac,
}
fn case() -> Case {
let net = ucte::read(ucte_fixture("TestCase12Nodes.uct")).expect("network");
let (crac, _) = crac_json::read(rao_fixture("crac-for-12nodes.json")).expect("crac");
Case { net, crac }
}
impl Case {
fn network(&self) -> Network<'_> {
Network {
generation: &self.net.generation,
buses: &self.net.buses,
lines: &self.net.lines,
transformers: &self.net.transformers,
branch_ids: &self.net.branch_ids,
bus_ids: &[],
initially_open: &[],
bus_countries: &[],
shunts: &[],
tap_changers: &[],
base_mva: self.net.base_mva,
}
}
}
#[test]
fn a_crac_written_for_this_network_resolves_completely() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
assert!(
resolution.is_complete(),
"unresolved elements: {:?}",
resolution.unresolved
);
assert!(resolution.branch_of.len() >= 10);
}
#[test]
fn ids_match_with_or_without_ucte_column_padding() {
let c = case();
let padded = c.net.branch_ids[0].clone();
assert!(
padded.contains(" "),
"fixture should have padded ids: {padded:?}"
);
let crac = Crac {
flow_cnecs: vec![FlowCnec {
id: "x".into(),
network_element: padded.trim().to_string(),
state: State::preventive(0),
thresholds: vec![],
reliability_margin: 0.0,
optimized: true,
monitored: false,
operator: None,
i_max: None,
nominal_v: None,
}],
instants: vec![Instant {
id: "preventive".into(),
kind: InstantKind::Preventive,
}],
..Default::default()
};
let resolution = Resolution::new(&crac, &c.net.branch_ids);
assert!(
resolution.is_complete(),
"trimmed id did not match: {:?}",
resolution.unresolved
);
}
#[test]
fn an_unresolvable_element_is_reported_and_its_cnec_skipped() {
let c = case();
let mut crac = c.crac.clone();
crac.flow_cnecs[0].network_element = "NO SUCH BRANCH".into();
let resolution = Resolution::new(&crac, &c.net.branch_ids);
assert!(!resolution.is_complete());
assert!(resolution.unresolved.iter().any(|e| e == "NO SUCH BRANCH"));
let result = evaluate(&crac, &c.network(), &resolution);
assert_eq!(result.skipped, vec![0]);
}
#[test]
fn an_ampere_threshold_converts_through_root_three() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let result = evaluate(&c.crac, &c.network(), &resolution);
let preventive = &result.perimeters[0];
let limit = preventive
.cnecs
.iter()
.map(|r| r.limit_mw)
.find(|l| (*l - 1500.0).abs() < 1.0)
.expect("a 2165 A threshold at 400 kV should convert to about 1500 MW");
assert!((limit - 1500.0).abs() < 1.0, "got {limit}");
}
#[test]
fn percent_imax_is_a_fraction_not_a_percentage() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let result = evaluate(&c.crac, &c.network(), &resolution);
for perimeter in &result.perimeters {
for r in &perimeter.cnecs {
assert!(
r.limit_mw > 100.0,
"cnec `{}` has a {} MW limit on a 380 kV branch, which is a factor-of-100 error",
c.crac.flow_cnecs[r.cnec].id,
r.limit_mw
);
}
}
}
#[test]
fn a_reliability_margin_tightens_the_limit() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let base = evaluate(&c.crac, &c.network(), &resolution);
let mut tightened = c.crac.clone();
for cnec in &mut tightened.flow_cnecs {
cnec.reliability_margin = 100.0;
}
let after = evaluate(&tightened, &c.network(), &resolution);
let before_limit = base.perimeters[0].cnecs[0].limit_mw;
let after_limit = after.perimeters[0].cnecs[0].limit_mw;
assert!(
(before_limit - after_limit - 100.0).abs() < 1e-9,
"{before_limit} -> {after_limit}"
);
}
#[test]
fn base_case_flows_are_the_dc_solution() {
use gridoxide::linear::btheta::dc_power_flow;
let c = case();
let mut buses = c.net.buses.clone();
let expected = dc_power_flow(
&mut buses,
&c.net.lines,
&c.net.transformers,
Default::default(),
)
.branch_p;
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let result = evaluate(&c.crac, &c.network(), &resolution);
let preventive = result
.perimeters
.iter()
.find(|p| p.state.is_preventive())
.expect("a preventive perimeter");
for r in &preventive.cnecs {
let want = expected[r.branch] * c.net.base_mva;
assert!(
(r.flow_mw - want).abs() < 1e-9,
"branch {}: {} vs {want}",
r.branch,
r.flow_mw
);
}
}
#[test]
fn screened_outage_flows_match_a_full_resolve() {
use gridoxide::linear::btheta::{dc_branches, dc_power_flow};
use gridoxide::linear::sensitivity::DcSensitivity;
let c = case();
let options = Default::default();
let dc = dc_branches(&c.net.lines, &c.net.transformers, options);
let mut buses = c.net.buses.clone();
let base = dc_power_flow(&mut buses, &c.net.lines, &c.net.transformers, options).branch_p;
let sensitivity =
DcSensitivity::new(&c.net.buses, &dc, c.net.n_branches()).expect("sensitivity");
let mut compared = 0;
for outage in 0..c.net.lines.len() {
if sensitivity.is_breaking_set(&[outage]) {
continue;
}
let Some(screened) = sensitivity.multi_outage_flows(&base, &[outage]) else {
continue;
};
let mut lines = c.net.lines.clone();
lines[outage].r = 1e9;
lines[outage].x = 1e9;
lines[outage].b_shunt = 0.0;
lines[outage].g_shunt = 0.0;
let mut buses = c.net.buses.clone();
let resolved = dc_power_flow(&mut buses, &lines, &c.net.transformers, options).branch_p;
for b in 0..screened.len() {
if b == outage {
continue;
}
let diff = (screened[b] - resolved[b]).abs() * c.net.base_mva;
assert!(diff < 1e-6, "outage {outage}, branch {b}: {diff} MW apart");
}
compared += 1;
}
assert!(compared >= 10, "only {compared} outages were screenable");
}
#[test]
fn a_contingency_actually_changes_the_flows() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let result = evaluate(&c.crac, &c.network(), &resolution);
let preventive = result
.perimeters
.iter()
.find(|p| p.state.is_preventive())
.unwrap();
let outage = result
.perimeters
.iter()
.find(|p| p.state.contingency.is_some() && !p.severed)
.expect("a post-contingency perimeter");
let shared: Vec<(f64, f64)> = outage
.cnecs
.iter()
.filter_map(|a| {
preventive
.cnecs
.iter()
.find(|b| b.branch == a.branch)
.map(|b| (a.flow_mw, b.flow_mw))
})
.collect();
assert!(
!shared.is_empty(),
"no branch is monitored in both perimeters"
);
assert!(
shared.iter().any(|(a, b)| (a - b).abs() > 1.0),
"the contingency moved nothing: {shared:?}"
);
}
#[test]
fn every_state_the_crac_defines_gets_a_perimeter() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let result = evaluate(&c.crac, &c.network(), &resolution);
for state in c.crac.states() {
assert!(
result.perimeters.iter().any(|p| p.state == state),
"no perimeter for {state:?}"
);
}
assert!(
result
.perimeters
.windows(2)
.all(|w| w[0].state.instant <= w[1].state.instant)
);
}
#[test]
fn a_margin_is_the_distance_to_the_nearer_bound() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let result = evaluate(&c.crac, &c.network(), &resolution);
let mut one_sided = 0;
for perimeter in &result.perimeters {
for r in &perimeter.cnecs {
let expected = f64::min(r.upper_mw - r.flow_mw, r.flow_mw - r.lower_mw);
assert!(
(r.margin_mw - expected).abs() < 1e-9,
"margin does not agree with its bounds: {r:?}"
);
assert_eq!(r.is_violated(), r.margin_mw < 0.0);
if !r.upper_mw.is_finite() || !r.lower_mw.is_finite() {
one_sided += 1;
} else {
assert!(
(r.margin_mw - (r.limit_mw - r.flow_mw.abs())).abs() < 1e-9,
"a two-sided CNEC should still be limit - |flow|: {r:?}"
);
}
}
}
assert!(
one_sided > 0,
"this fixture should exercise a one-sided threshold"
);
}
#[test]
fn direction_does_not_change_whether_a_branch_is_overloaded() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let result = evaluate(&c.crac, &c.network(), &resolution);
let negative_and_violated = result.violations().any(|(_, r)| r.flow_mw < 0.0);
assert!(
negative_and_violated,
"this fixture has a violated branch flowing backwards; none was found"
);
}
#[test]
fn the_worst_margin_is_reported_across_every_perimeter() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let result = evaluate(&c.crac, &c.network(), &resolution);
let worst = result.min_margin().expect("a margin");
let by_hand = result
.perimeters
.iter()
.flat_map(|p| p.cnecs.iter().map(|c| c.margin_mw))
.fold(f64::INFINITY, f64::min);
assert!((worst - by_hand).abs() < 1e-12);
assert_eq!(result.is_secure(), worst >= 0.0);
assert!(
!result.is_secure(),
"the fixture should have violations to find"
);
}
#[test]
fn a_severing_contingency_is_flagged_rather_than_silently_wrong() {
let c = case();
let mut crac = c.crac.clone();
crac.contingencies[0].elements = vec!["NOT A BRANCH".into()];
let resolution = Resolution::new(&crac, &c.net.branch_ids);
let result = evaluate(&crac, &c.network(), &resolution);
assert!(
result
.perimeters
.iter()
.filter(|p| p.state.contingency.is_some())
.all(|p| p.severed),
"an unsimulatable contingency must be flagged"
);
}
fn reference_flows(name: &str) -> std::collections::HashMap<String, [f64; 4]> {
let text = std::fs::read_to_string(reference_file(name)).expect("reference");
let doc: serde_json::Value = serde_json::from_str(&text).expect("json");
doc["branches"]
.as_object()
.expect("branches")
.iter()
.map(|(id, v)| {
let get = |k: &str| v[k].as_f64().unwrap_or(0.0);
(id.clone(), [get("p1"), get("q1"), get("p2"), get("q2")])
})
.collect()
}
fn reference_voltages(name: &str) -> std::collections::HashMap<String, f64> {
let text = std::fs::read_to_string(reference_file(name)).expect("reference");
let doc: serde_json::Value = serde_json::from_str(&text).expect("json");
doc["buses"]
.as_object()
.expect("buses")
.iter()
.map(|(id, v)| (id.trim().to_string(), v["v_pu"].as_f64().unwrap_or(1.0)))
.collect()
}
#[test]
fn ac_currents_match_the_reference_load_flow() {
let c = case();
let reference = reference_flows("TestCase12Nodes.pypowsybl.json");
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let ac = evaluate::AcOptions {
shunts: &c.net.shunts,
..Default::default()
};
let result = evaluate::evaluate_ac(&c.crac, &c.network(), &resolution, &[], &ac);
let base = result
.perimeters
.iter()
.find(|p| p.state.contingency.is_none())
.expect("a preventive perimeter");
assert!(!base.severed, "the intact network should solve");
assert!(!base.cnecs.is_empty(), "the CRAC should monitor something");
let reference_voltages = reference_voltages("TestCase12Nodes.pypowsybl.json");
let mut checked = 0;
for cnec in &base.cnecs {
let id = &c.net.branch_ids[cnec.branch];
let Some([p1, q1, ..]) = reference.get(id.trim()).or_else(|| reference.get(id)) else {
continue;
};
assert!(
(cnec.flow_mw - p1).abs() < 0.5,
"{id}: flow {} MW against the reference's {p1} MW",
cnec.flow_mw
);
let bus = c.net.lines.get(cnec.branch).map(|l| l.from);
if let Some(bus) = bus {
let code = c.net.node_codes[bus].trim().to_string();
let Some(v_pu) = reference_voltages.get(&code) else {
continue;
};
let u = c.net.buses[bus].u_rated * v_pu;
let expected = p1.hypot(*q1) * 1e6 / (3f64.sqrt() * u);
assert!(
(cnec.current_a - expected).abs() < 2.0,
"{id}: {} A against {expected} A",
cnec.current_a
);
checked += 1;
}
}
assert!(
checked >= 3,
"expected several comparable branches, got {checked}"
);
let spread = reference_voltages
.values()
.map(|v| (v - 1.0).abs())
.fold(0.0f64, f64::max);
assert!(
spread > 0.01,
"flat voltages would make this test vacuous (spread {spread})"
);
}
#[test]
fn dc_and_ac_disagree_by_the_reactive_flow() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let ac = evaluate::AcOptions {
shunts: &c.net.shunts,
..Default::default()
};
let dc = evaluate::evaluate(&c.crac, &c.network(), &resolution);
let acr = evaluate::evaluate_ac(&c.crac, &c.network(), &resolution, &[], &ac);
let worst = |r: &evaluate::SecurityResult| {
r.perimeters
.iter()
.flat_map(|p| p.cnecs.iter())
.map(|c| c.margin_mw)
.fold(f64::INFINITY, f64::min)
};
let (a, b) = (worst(&dc), worst(&acr));
assert!(
a.is_finite() && b.is_finite(),
"both models should measure something"
);
assert!(
(a - b).abs() > 1e-6,
"AC and DC gave identical margins ({a} vs {b})"
);
for (p, q) in dc.perimeters.iter().zip(&acr.perimeters) {
for (x, y) in p.cnecs.iter().zip(&q.cnecs) {
assert_eq!(x.cnec, y.cnec);
assert!(
y.limit_mw <= x.limit_mw + 1e-6,
"cnec {}: AC limit {} above the DC limit {}",
x.cnec,
y.limit_mw,
x.limit_mw
);
}
}
}
#[test]
fn an_outaged_branch_carries_nothing_in_ac() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let ac = evaluate::AcOptions {
shunts: &c.net.shunts,
..Default::default()
};
let monitored: Vec<usize> = c
.crac
.flow_cnecs
.iter()
.filter_map(|cnec| resolution.branch(&cnec.network_element))
.collect();
let target = *monitored.first().expect("a monitored branch");
let result = evaluate::evaluate_ac(&c.crac, &c.network(), &resolution, &[target], &ac);
for p in &result.perimeters {
for cnec in p.cnecs.iter().filter(|c| c.branch == target) {
assert_eq!(cnec.flow_mw, 0.0, "an open branch should carry no power");
assert_eq!(
cnec.current_a, 0.0,
"an open branch should carry no current"
);
}
}
}
#[test]
fn the_model_selector_dispatches_to_the_two_paths() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let ac = evaluate::AcOptions {
shunts: &c.net.shunts,
..Default::default()
};
let via_dc = evaluate::evaluate_model(
&c.crac,
&c.network(),
&resolution,
&[],
evaluate::FlowModel::Dc,
&ac,
);
let via_ac = evaluate::evaluate_model(
&c.crac,
&c.network(),
&resolution,
&[],
evaluate::FlowModel::Ac,
&ac,
);
let direct_dc = evaluate::evaluate_with(&c.crac, &c.network(), &resolution, &[]);
let direct_ac = evaluate::evaluate_ac(&c.crac, &c.network(), &resolution, &[], &ac);
assert_eq!(
via_dc.perimeters, direct_dc.perimeters,
"the DC arm must be the DC path"
);
assert_eq!(
via_ac.perimeters, direct_ac.perimeters,
"the AC arm must be the AC path"
);
assert_ne!(
via_dc.perimeters, via_ac.perimeters,
"the two arms must differ"
);
assert_eq!(evaluate::FlowModel::default(), evaluate::FlowModel::Dc);
}
#[test]
fn distributing_the_slack_solves_and_agrees_where_losses_are_negligible() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let single = evaluate::AcOptions {
shunts: &c.net.shunts,
..Default::default()
};
let spread = evaluate::AcOptions {
distribute_slack: true,
..single
};
let a = evaluate::evaluate_ac(&c.crac, &c.network(), &resolution, &[], &single);
let b = evaluate::evaluate_ac(&c.crac, &c.network(), &resolution, &[], &spread);
assert_eq!(a.perimeters.len(), b.perimeters.len());
let mut compared = 0;
for (p, q) in a.perimeters.iter().zip(&b.perimeters) {
assert_eq!(
p.severed, q.severed,
"convergence should not differ on {:?}",
p.state
);
for (x, y) in p.cnecs.iter().zip(&q.cnecs) {
assert!(
(x.margin_mw - y.margin_mw).abs() < 1.0,
"cnec {}: {} MW against {} MW",
x.cnec,
x.margin_mw,
y.margin_mw
);
compared += 1;
}
}
assert!(compared > 0, "nothing was compared");
}
#[test]
fn side_two_is_the_far_end_of_the_same_flow() {
let c = case();
let reference = reference_flows("TestCase12Nodes.pypowsybl.json");
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let ac = evaluate::AcOptions {
shunts: &c.net.shunts,
..Default::default()
};
let result = evaluate::evaluate_ac(&c.crac, &c.network(), &resolution, &[], &ac);
let base = result
.perimeters
.iter()
.find(|p| p.state.contingency.is_none())
.expect("a preventive perimeter");
let mut checked = 0;
for cnec in &base.cnecs {
let id = &c.net.branch_ids[cnec.branch];
let Some([p1, _, p2, _]) = reference.get(id.trim()).or_else(|| reference.get(id)) else {
continue;
};
assert!(
(cnec.side_two.0 - -p2).abs() < 0.5,
"{id}: side two {} MW against the reference's {} MW",
cnec.side_two.0,
-p2
);
assert!(
(cnec.flow_mw - cnec.side_two.0).abs() < 0.05 * p1.abs().max(1.0),
"{id}: {} MW in, {} MW out — that is not losses",
cnec.flow_mw,
cnec.side_two.0
);
checked += 1;
}
assert!(checked >= 2, "only {checked} branches checked");
}
#[test]
fn a_dc_far_end_carries_exactly_what_the_near_end_took() {
let c = case();
let resolution = Resolution::new(&c.crac, &c.net.branch_ids);
let result = evaluate::evaluate(&c.crac, &c.network(), &resolution);
let mut checked = 0;
for perimeter in &result.perimeters {
for cnec in &perimeter.cnecs {
assert!(
(cnec.flow_mw - cnec.side_two.0).abs() < 1e-9,
"{}: {} MW in, {} MW out under a lossless model",
c.net.branch_ids[cnec.branch],
cnec.flow_mw,
cnec.side_two.0
);
assert!(cnec.side_two.1 >= 0.0, "a current is a magnitude");
checked += 1;
}
}
assert!(checked > 0, "the CRAC should monitor something");
}
#[test]
fn an_ampere_margin_is_measured_in_amperes() {
let net = ucte::read(ucte_fixture("TestCase12Nodes.uct")).expect("network");
let (crac, _) =
crac_json::read(openrao::path("files/crac/epic5/SL_ep5us1.json")).expect("crac");
let resolution = Resolution::with_buses(&crac, &net.branch_ids, &net.node_codes);
let network = Network {
generation: &net.generation,
buses: &net.buses,
lines: &net.lines,
transformers: &net.transformers,
branch_ids: &net.branch_ids,
bus_ids: &net.node_codes,
initially_open: &net.initially_open,
bus_countries: &net.bus_countries,
shunts: &net.shunts,
tap_changers: &net.tap_changers,
base_mva: net.base_mva,
};
let ac = gridoxide::rao::AcOptions {
shunts: &net.shunts,
..Default::default()
};
let result = gridoxide::rao::evaluate_model(
&crac,
&network,
&resolution,
&net.initially_open,
gridoxide::rao::FlowModel::Ac,
&ac,
);
let cnec = result
.perimeters
.iter()
.flat_map(|p| p.cnecs.iter())
.find(|c| crac.flow_cnecs[c.cnec].id == "FFR2AA1 DDE3AA1 1 - preventive")
.expect("the CRAC's preventive CNEC");
assert!(
(cnec.margin_mw - 500.0).abs() < 5.0,
"megawatt margin {} MW, reference 500",
cnec.margin_mw
);
let limit_a = 2000.0 * 1e6 / (3f64.sqrt() * 380e3);
assert!(
(cnec.margin_a - (limit_a - cnec.current_a)).abs() < 0.5,
"ampere margin {} A is not {} - {}",
cnec.margin_a,
limit_a,
cnec.current_a
);
assert!(
(cnec.margin_a - 871.0).abs() < 5.0,
"ampere margin {} A, reference 871",
cnec.margin_a
);
for v in [380e3, 400e3] {
let converted = cnec.margin_mw * 1e6 / (3f64.sqrt() * v);
assert!(
(cnec.margin_a - converted).abs() > 50.0,
"converting the megawatt margin at {v} V gives {converted} A, which is the \
reading this test exists to rule out"
);
}
}
#[test]
fn the_slack_is_shared_out_in_proportion_to_generation() {
let net = ucte::read(ucte_fixture("TestCase12Nodes.uct")).expect("network");
let (crac, _) =
crac_json::read(openrao::path("files/crac/epic5/SL_ep5us1.json")).expect("crac");
let resolution = Resolution::with_buses(&crac, &net.branch_ids, &net.node_codes);
let network = Network {
buses: &net.buses,
lines: &net.lines,
transformers: &net.transformers,
branch_ids: &net.branch_ids,
bus_ids: &net.node_codes,
initially_open: &net.initially_open,
bus_countries: &net.bus_countries,
shunts: &net.shunts,
generation: &net.generation,
tap_changers: &net.tap_changers,
base_mva: net.base_mva,
};
let mut open = net.initially_open.clone();
for name in ["Open tie-line FR1 FR2", "Open tie-line FR1 FR3"] {
let action = crac
.network_actions
.iter()
.find(|a| a.id == name)
.expect(name);
for elementary in &action.elementary {
for element in elementary.elements() {
if let Some(b) = resolution.branch(element) {
open.push(b);
}
}
}
}
let flow = |ac: &gridoxide::rao::AcOptions<'_>| -> f64 {
gridoxide::rao::evaluate_model(
&crac,
&network,
&resolution,
&open,
gridoxide::rao::FlowModel::Ac,
ac,
)
.perimeters
.iter()
.flat_map(|p| p.cnecs.iter())
.find(|c| crac.flow_cnecs[c.cnec].id == "FFR2AA1 DDE3AA1 1 - preventive")
.map(|c| c.flow_mw)
.expect("the preventive CNEC")
};
let shared = flow(&gridoxide::rao::ac_options(&network));
assert!(
(shared - 1000.0).abs() < 5.0,
"generation-weighted, the flow should be the reference's 1000 MW, not {shared}"
);
let netted = flow(&gridoxide::rao::AcOptions {
shunts: &net.shunts,
distribute_slack: true,
slack_weights: &[],
..Default::default()
});
let single = flow(&gridoxide::rao::AcOptions {
shunts: &net.shunts,
distribute_slack: false,
..Default::default()
});
assert!(
netted > 1100.0 && single > 1100.0,
"weighting by net injection ({netted}) and a single slack ({single}) should both \
still push the make-up through France"
);
}
#[test]
fn generation_is_retained_alongside_the_net_injection() {
let net = ucte::read(ucte_fixture("TestCase12Nodes.uct")).expect("network");
assert_eq!(net.generation.len(), net.buses.len());
let fr1 = net
.node_codes
.iter()
.position(|c| c.trim() == "FFR1AA1")
.expect("FFR1AA1");
assert!((net.generation[fr1] * net.base_mva - 2000.0).abs() < 1e-6);
assert!((net.buses[fr1].p_spec * net.base_mva - 1000.0).abs() < 1e-6);
assert!(
net.generation.iter().all(|g| *g >= 0.0),
"generation is stored positive"
);
}