Load Flow Calculation in Practice
This tutorial demonstrates how to use EnerOS to perform load flow calculation on the IEEE 14-bus standard test system, covering the entire workflow of network modeling, solving, result analysis, violation checking, persistence, and visualization.
Load flow calculation is the foundation of power system analysis, used to solve the steady-state operating point (voltage magnitude and phase angle of each bus, and power distribution of each branch) under a given network structure and boundary conditions. EnerOS pushes the load flow solver down to the kernel layer (the eneros-powerflow crate); all Agents and upper-layer applications can invoke it directly via system calls without re-modeling.
Prerequisites
Ensure that EnerOS has been built according to Installation and Build, and that eneros-api runs normally. This tutorial involves the following crates:
| Crate | Purpose |
|---|---|
eneros-topology | Network topology modeling (buses, branches, switches) |
eneros-powerflow | Load flow solvers (Newton-Raphson, fast decoupled, DC) |
eneros-core | Base types (BusType, BranchType, YBusMatrix) |
eneros-timeseries | Time-series data persistence |
eneros-api | REST/WebSocket interface and visualization frontend |
Create a new binary crate and add dependencies:
[package]
name = "ieee14-tutorial"
version = "0.1.0"
edition = "2021"
[dependencies]
eneros-topology = { path = "../eneros/crates/eneros-topology" }
eneros-powerflow = { path = "../eneros/crates/eneros-powerflow" }
eneros-core = { path = "../eneros/crates/eneros-core" }
eneros-timeseries = { path = "../eneros/crates/eneros-timeseries" }
tokio = { version = "1", features = ["full"] }
IEEE 14-bus System Overview
The IEEE 14-bus is a standard test system published by the Institute of Electrical and Electronics Engineers, widely used for load flow algorithm validation. Its topology is as follows:
- 14 buses: 1 Slack (swing bus), 4 PV (voltage-controlled buses), 9 PQ (load buses)
- 20 branches: 16 lines + 4 transformers (including 3 non-standard-ratio transformers)
- Base capacity: 100 MVA
- Bus 9: equipped with a 19 MVar shunt capacitor
| Bus No. | Type | Voltage (p.u.) | P Injection (MW) | Q Injection (MVar) |
|---|---|---|---|---|
| 1 | Slack | 1.060 | Calculated by balance | Calculated by balance |
| 2 | PV | 1.045 | 18.3 | -12.7 |
| 3 | PV | 1.010 | -94.2 | -19.0 |
| 4 | PQ | 1.019 | -47.8 | 3.9 |
| 5 | PQ | 1.020 | -7.6 | -1.6 |
| 6 | PV | 1.070 | -11.2 | -7.5 |
| 7 | PQ | 1.062 | 0.0 | 0.0 |
| 8 | PV | 1.090 | 0.0 | 0.0 |
| 9 | PQ | 1.056 | -29.5 | -16.6 |
| 10 | PQ | 1.051 | -9.0 | -5.8 |
| 11 | PQ | 1.057 | -3.5 | -1.8 |
| 12 | PQ | 1.055 | -6.1 | -1.6 |
| 13 | PQ | 1.050 | -13.5 | -5.8 |
| 14 | PQ | 1.036 | -14.9 | -5.0 |
| Branch (From-To) | Type | R (p.u.) | X (p.u.) | B/2 (p.u.) | Ratio |
|---|---|---|---|---|---|
| 1-2 | Line | 0.01938 | 0.05917 | 0.0528 | 1.0 |
| 1-5 | Line | 0.05403 | 0.22304 | 0.0492 | 1.0 |
| 2-3 | Line | 0.04699 | 0.19797 | 0.0438 | 1.0 |
| 2-4 | Line | 0.05811 | 0.17632 | 0.0340 | 1.0 |
| 2-5 | Line | 0.05695 | 0.17388 | 0.0346 | 1.0 |
| 3-4 | Line | 0.06701 | 0.17103 | 0.0128 | 1.0 |
| 4-5 | Line | 0.01335 | 0.04211 | 0.0 | 1.0 |
| 4-7 | Transformer | 0.0 | 0.20912 | 0.0 | 0.978 |
| 4-9 | Transformer | 0.0 | 0.55618 | 0.0 | 0.969 |
| 5-6 | Transformer | 0.0 | 0.25202 | 0.0 | 0.932 |
| 6-11 | Line | 0.09498 | 0.19890 | 0.0 | 1.0 |
| 6-12 | Line | 0.12291 | 0.25581 | 0.0 | 1.0 |
| 6-13 | Line | 0.06615 | 0.13027 | 0.0 | 1.0 |
| 7-8 | Transformer | 0.0 | 0.17615 | 0.0 | 0.969 |
| 7-9 | Line | 0.0 | 0.11001 | 0.0 | 1.0 |
| 9-10 | Line | 0.03181 | 0.08450 | 0.0 | 1.0 |
| 9-14 | Line | 0.12711 | 0.27038 | 0.0 | 1.0 |
| 10-11 | Line | 0.08205 | 0.19207 | 0.0 | 1.0 |
| 12-13 | Line | 0.22092 | 0.19988 | 0.0 | 1.0 |
| 13-14 | Line | 0.17093 | 0.34802 | 0.0 | 1.0 |
Step 1: Build the Network Topology
Use eneros_topology::NetworkGraph to build the IEEE 14-bus network. The Slack bus specifies voltage magnitude and phase angle, PV buses specify voltage magnitude and active power injection, and PQ buses specify active/reactive load. Below is the complete bus and branch definition (without any omissions):
use eneros_topology::{NetworkGraph, Bus, Branch, BusType, BranchType};
use eneros_core::ElementId;
/// Build the network topology of the IEEE 14-bus standard test system
fn build_ieee14_network() -> NetworkGraph {
let mut graph = NetworkGraph::new();
// ===================== Bus definitions =====================
// Bus 1: Slack (swing bus), V=1.06 p.u., θ=0°
graph.bus_add(Bus {
id: 1,
name: "Bus1_Slack".into(),
bus_type: BusType::Slack,
voltage_kv: 138.0,
zone_id: 1,
bus_type_pf: BusType::Slack,
p_gen: 0.0,
q_gen: 0.0,
p_load: 0.0,
q_load: 0.0,
v_pu: 1.060,
});
// Bus 2: PV, P_inj=18.3 MW (generation 40 - load 21.7), V=1.045 p.u.
graph.bus_add(Bus {
id: 2,
name: "Bus2_PV".into(),
bus_type: BusType::PV,
voltage_kv: 138.0,
zone_id: 1,
bus_type_pf: BusType::PV,
p_gen: 40.0,
q_gen: 0.0,
p_load: 21.7,
q_load: 12.7,
v_pu: 1.045,
});
// Bus 3: PV (synchronous condenser), P_inj=-94.2 MW, V=1.010 p.u.
graph.bus_add(Bus {
id: 3,
name: "Bus3_PV".into(),
bus_type: BusType::PV,
voltage_kv: 138.0,
zone_id: 1,
bus_type_pf: BusType::PV,
p_gen: 0.0,
q_gen: 0.0,
p_load: 94.2,
q_load: 19.0,
v_pu: 1.010,
});
// Bus 4: PQ, P_inj=-47.8 MW, Q_inj=3.9 MVar (capacitive load)
graph.bus_add(Bus {
id: 4,
name: "Bus4_PQ".into(),
bus_type: BusType::PQ,
voltage_kv: 138.0,
zone_id: 1,
bus_type_pf: BusType::PQ,
p_gen: 0.0,
q_gen: 0.0,
p_load: 47.8,
q_load: -3.9,
v_pu: 1.019,
});
// Bus 5: PQ, P_inj=-7.6 MW, Q_inj=-1.6 MVar
graph.bus_add(Bus {
id: 5,
name: "Bus5_PQ".into(),
bus_type: BusType::PQ,
voltage_kv: 138.0,
zone_id: 1,
bus_type_pf: BusType::PQ,
p_gen: 0.0,
q_gen: 0.0,
p_load: 7.6,
q_load: 1.6,
v_pu: 1.020,
});
// Bus 6: PV, P_inj=-11.2 MW, V=1.070 p.u.
graph.bus_add(Bus {
id: 6,
name: "Bus6_PV".into(),
bus_type: BusType::PV,
voltage_kv: 13.8,
zone_id: 2,
bus_type_pf: BusType::PV,
p_gen: 0.0,
q_gen: 0.0,
p_load: 11.2,
q_load: 7.5,
v_pu: 1.070,
});
// Bus 7: PQ, no load
graph.bus_add(Bus {
id: 7,
name: "Bus7_PQ".into(),
bus_type: BusType::PQ,
voltage_kv: 13.8,
zone_id: 2,
bus_type_pf: BusType::PQ,
p_gen: 0.0,
q_gen: 0.0,
p_load: 0.0,
q_load: 0.0,
v_pu: 1.062,
});
// Bus 8: PV (synchronous condenser), V=1.090 p.u.
graph.bus_add(Bus {
id: 8,
name: "Bus8_PV".into(),
bus_type: BusType::PV,
voltage_kv: 13.8,
zone_id: 2,
bus_type_pf: BusType::PV,
p_gen: 0.0,
q_gen: 0.0,
p_load: 0.0,
q_load: 0.0,
v_pu: 1.090,
});
// Bus 9: PQ, P_inj=-29.5 MW, equipped with a 19 MVar shunt capacitor
graph.bus_add(Bus {
id: 9,
name: "Bus9_PQ".into(),
bus_type: BusType::PQ,
voltage_kv: 13.8,
zone_id: 2,
bus_type_pf: BusType::PQ,
p_gen: 0.0,
q_gen: 0.0,
p_load: 29.5,
q_load: 16.6,
v_pu: 1.056,
});
// Bus 10: PQ, P_inj=-9.0 MW
graph.bus_add(Bus {
id: 10,
name: "Bus10_PQ".into(),
bus_type: BusType::PQ,
voltage_kv: 13.8,
zone_id: 2,
bus_type_pf: BusType::PQ,
p_gen: 0.0,
q_gen: 0.0,
p_load: 9.0,
q_load: 5.8,
v_pu: 1.051,
});
// Bus 11: PQ, P_inj=-3.5 MW
graph.bus_add(Bus {
id: 11,
name: "Bus11_PQ".into(),
bus_type: BusType::PQ,
voltage_kv: 13.8,
zone_id: 2,
bus_type_pf: BusType::PQ,
p_gen: 0.0,
q_gen: 0.0,
p_load: 3.5,
q_load: 1.8,
v_pu: 1.057,
});
// Bus 12: PQ, P_inj=-6.1 MW
graph.bus_add(Bus {
id: 12,
name: "Bus12_PQ".into(),
bus_type: BusType::PQ,
voltage_kv: 13.8,
zone_id: 2,
bus_type_pf: BusType::PQ,
p_gen: 0.0,
q_gen: 0.0,
p_load: 6.1,
q_load: 1.6,
v_pu: 1.055,
});
// Bus 13: PQ, P_inj=-13.5 MW
graph.bus_add(Bus {
id: 13,
name: "Bus13_PQ".into(),
bus_type: BusType::PQ,
voltage_kv: 13.8,
zone_id: 2,
bus_type_pf: BusType::PQ,
p_gen: 0.0,
q_gen: 0.0,
p_load: 13.5,
q_load: 5.8,
v_pu: 1.050,
});
// Bus 14: PQ, P_inj=-14.9 MW
graph.bus_add(Bus {
id: 14,
name: "Bus14_PQ".into(),
bus_type: BusType::PQ,
voltage_kv: 13.8,
zone_id: 2,
bus_type_pf: BusType::PQ,
p_gen: 0.0,
q_gen: 0.0,
p_load: 14.9,
q_load: 5.0,
v_pu: 1.036,
});
// ===================== Branch definitions =====================
// Helper: add a line
let add_line = |g: &mut NetworkGraph, id: ElementId, from: ElementId, to: ElementId,
r: f64, x: f64, b: f64| {
g.branch_add(Branch {
id,
name: format!("Line_{}_{}", from, to),
from_bus: from,
to_bus: to,
branch_type: BranchType::Line,
status: true,
r, x, b,
tap_ratio: 1.0,
});
};
// Helper: add a transformer
let add_tx = |g: &mut NetworkGraph, id: ElementId, from: ElementId, to: ElementId,
x: f64, tap: f64| {
g.branch_add(Branch {
id,
name: format!("Tx_{}_{}", from, to),
from_bus: from,
to_bus: to,
branch_type: BranchType::Transformer,
status: true,
r: 0.0, x, b: 0.0,
tap_ratio: tap,
});
};
// 7 lines (main grid backbone)
add_line(&mut graph, 101, 1, 2, 0.01938, 0.05917, 0.0528);
add_line(&mut graph, 102, 1, 5, 0.05403, 0.22304, 0.0492);
add_line(&mut graph, 103, 2, 3, 0.04699, 0.19797, 0.0438);
add_line(&mut graph, 104, 2, 4, 0.05811, 0.17632, 0.0340);
add_line(&mut graph, 105, 2, 5, 0.05695, 0.17388, 0.0346);
add_line(&mut graph, 106, 3, 4, 0.06701, 0.17103, 0.0128);
add_line(&mut graph, 107, 4, 5, 0.01335, 0.04211, 0.0);
// 3 transformers (4-7, 4-9, 5-6), with non-standard ratios
add_tx(&mut graph, 201, 4, 7, 0.20912, 0.978);
add_tx(&mut graph, 202, 4, 9, 0.55618, 0.969);
add_tx(&mut graph, 203, 5, 6, 0.25202, 0.932);
// 6-11, 6-12, 6-13 lines
add_line(&mut graph, 108, 6, 11, 0.09498, 0.19890, 0.0);
add_line(&mut graph, 109, 6, 12, 0.12291, 0.25581, 0.0);
add_line(&mut graph, 110, 6, 13, 0.06615, 0.13027, 0.0);
// 7-8 transformer (with non-standard ratio)
add_tx(&mut graph, 204, 7, 8, 0.17615, 0.969);
// 7-9, 9-10, 9-14, 10-11, 12-13, 13-14 lines
add_line(&mut graph, 111, 7, 9, 0.0, 0.11001, 0.0);
add_line(&mut graph, 112, 9, 10, 0.03181, 0.08450, 0.0);
add_line(&mut graph, 113, 9, 14, 0.12711, 0.27038, 0.0);
add_line(&mut graph, 114, 10, 11, 0.08205, 0.19207, 0.0);
add_line(&mut graph, 115, 12, 13, 0.22092, 0.19988, 0.0);
add_line(&mut graph, 116, 13, 14, 0.17093, 0.34802, 0.0);
graph
}
fn main() {
let network = build_ieee14_network();
println!("Bus count: {}", network.bus_count());
println!("Branch count: {}", network.branch_count());
println!("Connected zones: {}", network.zone_count());
}
After execution, the output should be:
Bus count: 14
Branch count: 20
Connected zones: 1
:::tip Use the built-in IEEE 14 data
eneros-powerflow already ships with the complete IEEE 14-bus standard data, which can be obtained directly via the ieee14() function without manual construction. This section shows the manual construction process to help readers understand the complete flow of network modeling.
:::
Step 2: Convert to Solver Input
NetworkGraph::to_solver_input(base_mva) exports the topology as the four-tuple required by the load flow solver:
use eneros_core::BusTypeNR;
let base_mva = 100.0;
let (ybus, p_spec, q_spec, bus_types, v_initial) = network.to_solver_input(base_mva);
println!("Y-Bus dimension: {}x{}", ybus.size(), ybus.size());
println!("Non-zero elements: {}", ybus.nnz());
println!("P_spec (p.u.): {:?}", p_spec);
println!("bus_types: {:?}", bus_types);
Example output:
Y-Bus dimension: 14x14
Non-zero elements: 56
P_spec (p.u.): [0.0, 0.183, -0.942, -0.478, ...]
bus_types: [Slack, PV, PV, PQ, PQ, PV, PQ, PV, PQ, PQ, PQ, PQ, PQ, PQ]
The export process performs the following:
| Field | Description |
|---|---|
ybus | Node admittance matrix, including branch R/X/B and transformer ratio |
p_spec | Active injection of each node (p_gen - p_load) / base_mva (per-unit) |
q_spec | Reactive injection of each node (q_gen - q_load) / base_mva (per-unit) |
bus_types | Node type enum (Slack / PV / PQ), which determines the solve variables |
v_initial | Initial voltage magnitude (from Bus::v_pu), used for warm start |
Step 3: Configure and Run the Load Flow Solve
EnerOS provides three load flow algorithms:
| Algorithm | Applicable Scenario | Convergence | Speed |
|---|---|---|---|
NewtonRaphson | Transmission networks, meshed grids | Quadratic convergence | Medium |
BackwardForwardSweep | Distribution networks, radial | Linear convergence | Fast |
DC | Real-time screening, planning | Linear | Fastest |
For IEEE 14-bus, Newton-Raphson is recommended:
use eneros_powerflow::{PowerFlowSolver, PowerFlowAlgorithm};
let solver = PowerFlowSolver::new(50, 1e-8)
.with_algorithm(PowerFlowAlgorithm::NewtonRaphson);
let result = solver.solve_with_initial(
&ybus,
&p_spec,
&q_spec,
&bus_types,
Some(&v_initial),
)?;
println!("Converged: {}", result.converged);
println!("Iterations: {}", result.iterations);
println!("Max mismatch: {:.2e}", result.max_mismatch);
println!("Total losses: {:.4} MW", result.total_losses * base_mva);
Example output:
Converged: true
Iterations: 4
Max mismatch: 3.21e-09
Total losses: 13.8392 MW
Enable Q Limits
The reactive output of a PV bus may exceed generator capacity. When Q limits are enabled, PV buses that violate limits are automatically converted to PQ buses:
use eneros_powerflow::QLimits;
let mut q_limits = QLimits::new();
// Generator 2: Q ∈ [-50, 50] MVar
q_limits.add(1, -0.50, 0.50); // bus_idx=1 (i.e. bus 2), in p.u.
// Generator 6: Q ∈ [-25, 25] MVar
q_limits.add(5, -0.25, 0.25);
let result_q = solver.solve_with_options(
&ybus, &p_spec, &q_spec, &bus_types,
Some(&v_initial), Some(&q_limits), None,
)?;
println!("Total losses after Q limits: {:.4} MW", result_q.total_losses * base_mva);
Use Recycle Cache for Acceleration
For consecutive solves with an unchanged topology (e.g. time-series load flow), you can reuse the previous voltage result as the initial value to reduce iterations:
use eneros_powerflow::RecycleCache;
let mut cache = RecycleCache::new();
// First solve
let r1 = solver.solve_with_options(
&ybus, &p_spec, &q_spec, &bus_types,
None, None, None,
)?;
cache.update(
&r1.bus_results.iter().map(|b| b.voltage_magnitude).collect::<Vec<_>>(),
&r1.bus_results.iter().map(|b| b.voltage_angle).collect::<Vec<_>>(),
);
// Second solve (after slight load adjustment), reusing the cache
let p_spec_2: Vec<f64> = p_spec.iter().map(|p| p * 1.05).collect();
let r2 = solver.solve_with_options(
&ybus, &p_spec_2, &q_spec, &bus_types,
None, None, Some(&cache),
)?;
println!("Iterations after reusing cache: {}", r2.iterations);
Step 4: View and Export Results
Print the voltage magnitude and phase angle bus by bus, and compare with the IEEE standard reference values:
println!("{:>6} {:>10} {:>10} {:>12} {:>10}",
"Bus", "V(pu)", "θ(°)", "P_inj(MW)", "Q(MVar)");
println!("{}", "-".repeat(54));
for br in &result.bus_results {
let angle_deg = br.voltage_angle.to_degrees();
println!("{:>6} {:>10.4} {:>10.4} {:>12.4} {:>10.4}",
br.bus_id + 1, // back to 1-based
br.voltage_magnitude,
angle_deg,
br.p_injection * base_mva,
br.q_injection * base_mva,
);
}
Example output:
Bus V(pu) θ(°) P_inj(MW) Q(MVar)
------------------------------------------------------
1 1.0600 0.0000 232.3863 -16.5467
2 1.0450 -4.9826 18.3000 33.5234
3 1.0100 -12.7242 -94.2000 25.1468
4 1.0187 -10.3129 -47.8000 3.9000
5 1.0202 -8.7813 -7.6000 -1.6000
6 1.0700 -14.2210 -11.2000 12.7312
7 1.0620 -13.3620 0.0000 0.0000
8 1.0900 -13.3620 0.0000 17.5654
9 1.0564 -14.9385 -29.5000 -16.6000
10 1.0514 -15.0982 -9.0000 -5.8000
11 1.0569 -14.7953 -3.5000 -1.8000
12 1.0552 -15.0736 -6.1000 -1.6000
13 1.0505 -15.1548 -13.5000 -5.8000
14 1.0356 -16.0336 -14.9000 -5.0000
Branch Power Flow and Losses
println!("{:>10} {:>10} {:>10} {:>10} {:>10}",
"Branch", "P_from(MW)", "Q_from", "P_to(MW)", "Loss(MW)");
println!("{}", "-".repeat(56));
for br in &result.branch_results {
println!("{:>4}-{::<5} {:>10.4} {:>10.4} {:>10.4} {:>10.4}",
br.from_bus + 1, br.to_bus + 1,
br.p_from * base_mva,
br.q_from * base_mva,
br.p_to * base_mva,
br.loss_mw * base_mva,
);
}
println!("\nTotal system losses: {:.4} MW", result.total_losses * base_mva);
Step 5: Violation Check
Constraint checking is the “law” enforced by the EnerOS kernel. Even in tutorial scenarios, you should proactively check whether voltages and branch loading rates violate limits:
fn check_violations(result: &eneros_powerflow::PowerFlowResult, base_mva: f64) {
// Voltage violation check (normal operating range 0.95 ~ 1.05 p.u., emergency range 0.90 ~ 1.10)
let v_min_normal = 0.95;
let v_max_normal = 1.05;
let mut v_violations = 0;
for bus in &result.bus_results {
if bus.voltage_magnitude < v_min_normal || bus.voltage_magnitude > v_max_normal {
eprintln!("[Voltage violation] Bus {}: V={:.4} pu (allowed range {}-{})",
bus.bus_id + 1, bus.voltage_magnitude, v_min_normal, v_max_normal);
v_violations += 1;
}
}
// Branch overload check
let loading_limit = 100.0; // %
let mut b_violations = 0;
for branch in &result.branch_results {
if branch.loading_percent > loading_limit {
eprintln!("[Branch overload] Branch {}-{}: {:.1}% (> {}%)",
branch.from_bus + 1, branch.to_bus + 1,
branch.loading_percent, loading_limit);
b_violations += 1;
}
}
println!("\nViolation statistics: voltage={}, branch={}", v_violations, b_violations);
if v_violations == 0 && b_violations == 0 {
println!("✓ All constraints satisfied");
}
}
check_violations(&result, base_mva);
Example output:
[Voltage violation] Bus 1: V=1.0600 pu (allowed range 0.95-1.05)
[Voltage violation] Bus 6: V=1.0700 pu (allowed range 0.95-1.05)
[Voltage violation] Bus 8: V=1.0900 pu (allowed range 0.95-1.05)
Violation statistics: voltage=3, branch=0
:::warning Bus 1, 6, and 8 are PV buses; their voltages are controlled by generators/condensers to 1.06 / 1.07 / 1.09, so exceeding the 0.95-1.05 range of normal load buses is expected. In real engineering, constraints should be set separately according to bus type. :::
Step 6: Persistence and Visualization
Write the results into the time-series engine for historical replay and trend analysis:
use eneros_timeseries::{TimeseriesEngine, WriteOptions};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ts = TimeseriesEngine::open("/var/lib/eneros/ts")?;
let opts = WriteOptions::default().tags([
("system", "ieee14"),
("scenario", "base_case"),
]);
// Write voltage time series bus by bus
for bus in &result.bus_results {
let point_name = format!("ieee14.bus{}.voltage", bus.bus_id + 1);
ts.write_point(&point_name, bus.voltage_magnitude, &opts).await?;
}
// Write system-level metrics
ts.write_point("ieee14.total_losses_mw",
result.total_losses * base_mva, &opts).await?;
ts.write_point("ieee14.iterations",
result.iterations as f64, &opts).await?;
println!("✓ Wrote {} measurement points", result.bus_results.len() + 1);
Ok(())
}
Trigger a visualization frontend refresh through the REST API:
let client = eneros_api::Client::new("https://api.eneros.internal");
// Publish load flow results to the event bus
client.publish("/topics/powerflow/ieee14", &result).await?;
// Trigger the visualization frontend to refresh the one-line diagram
client.post("/api/visualization/refresh")
.json(&serde_json::json!({
"network_id": "ieee14",
"result_id": result.id(),
"layers": ["voltage", "loading", "loss"],
}))
.await?;
Step 7: Complete End-to-End Example
Integrate the above steps into a complete runnable program:
use eneros_powerflow::{ieee14, PowerFlowSolver, PowerFlowAlgorithm};
fn main() -> eneros_core::Result<()> {
// 1. Load the IEEE 14-bus standard data
let data = ieee14();
println!("Loaded network: {} buses, {} branches", data.buses.len(), data.branches.len());
// 2. Convert to solver input
let (ybus, p_spec, q_spec, bus_types) = data.to_solver_input();
let v_initial: Vec<f64> = data.buses.iter().map(|b| b.v_pu).collect();
// 3. Solve
let solver = PowerFlowSolver::new(100, 1e-8)
.with_algorithm(PowerFlowAlgorithm::NewtonRaphson);
let result = solver.solve_with_initial(
&ybus, &p_spec, &q_spec, &bus_types, Some(&v_initial))?;
// 4. Output key metrics
println!("Converged: {}, iterations: {}, mismatch: {:.2e}",
result.converged, result.iterations, result.max_mismatch);
println!("Total losses: {:.4} MW", result.total_losses * data.base_mva);
// 5. Slack bus active power (should be close to 232.4 MW)
let slack_p = result.bus_results[0].p_injection * data.base_mva;
println!("Slack active power: {:.2} MW (expected ≈232.4 MW)", slack_p);
// 6. Violation check
let overloaded = result.branch_results.iter()
.filter(|b| b.loading_percent > 100.0)
.count();
println!("Overloaded branch count: {}", overloaded);
Ok(())
}
Validation
Validate the correctness of the solve against the following metrics:
| Metric | Expected | Tolerance |
|---|---|---|
| Convergence | true | — |
| Iterations | 3-5 | < 20 |
| Max mismatch | < 1e-8 p.u. | — |
| Slack active power | ≈ 232.4 MW | ±1% |
| Total losses | ≈ 13.8 MW | ±1 MW |
| All bus voltages | 0.9-1.1 p.u. | — |
| Branch loading | < 100% | — |
Debugging Tips
- Non-convergence: check whether PV buses have sufficient reactive capacity, and enable Q limits
- Excessive losses: check whether transformer ratios are correct (0.978/0.969/0.932/0.969)
- Abnormal voltage: check the shunt capacitor on bus 9 (19 MVar = 0.19 p.u.)
- Too many iterations: use
v_initialfor warm start, or enableRecycleCache - Y-Bus error: confirm that the branch B value is the total charging susceptance (not B/2)
Command-Line Validation
# Run unit tests
cargo test -p eneros-powerflow test_ieee14 -- --nocapture
# View detailed output
RUST_LOG=eneros_powerflow=debug cargo run --release
Next Steps
- Dispatch Agent Development — build automatic dispatch based on load flow results
- Grid Topology First-Class Citizen — dive deeper into topology modeling
- Physics-Constrained Decisions — detailed explanation of the constraint engine
- Time-Series Storage Engine — time-series data persistence