Skip to main content

Load Flow Calculation in Practice

Tutorials

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:

CratePurpose
eneros-topologyNetwork topology modeling (buses, branches, switches)
eneros-powerflowLoad flow solvers (Newton-Raphson, fast decoupled, DC)
eneros-coreBase types (BusType, BranchType, YBusMatrix)
eneros-timeseriesTime-series data persistence
eneros-apiREST/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.TypeVoltage (p.u.)P Injection (MW)Q Injection (MVar)
1Slack1.060Calculated by balanceCalculated by balance
2PV1.04518.3-12.7
3PV1.010-94.2-19.0
4PQ1.019-47.83.9
5PQ1.020-7.6-1.6
6PV1.070-11.2-7.5
7PQ1.0620.00.0
8PV1.0900.00.0
9PQ1.056-29.5-16.6
10PQ1.051-9.0-5.8
11PQ1.057-3.5-1.8
12PQ1.055-6.1-1.6
13PQ1.050-13.5-5.8
14PQ1.036-14.9-5.0
Branch (From-To)TypeR (p.u.)X (p.u.)B/2 (p.u.)Ratio
1-2Line0.019380.059170.05281.0
1-5Line0.054030.223040.04921.0
2-3Line0.046990.197970.04381.0
2-4Line0.058110.176320.03401.0
2-5Line0.056950.173880.03461.0
3-4Line0.067010.171030.01281.0
4-5Line0.013350.042110.01.0
4-7Transformer0.00.209120.00.978
4-9Transformer0.00.556180.00.969
5-6Transformer0.00.252020.00.932
6-11Line0.094980.198900.01.0
6-12Line0.122910.255810.01.0
6-13Line0.066150.130270.01.0
7-8Transformer0.00.176150.00.969
7-9Line0.00.110010.01.0
9-10Line0.031810.084500.01.0
9-14Line0.127110.270380.01.0
10-11Line0.082050.192070.01.0
12-13Line0.220920.199880.01.0
13-14Line0.170930.348020.01.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:

FieldDescription
ybusNode admittance matrix, including branch R/X/B and transformer ratio
p_specActive injection of each node (p_gen - p_load) / base_mva (per-unit)
q_specReactive injection of each node (q_gen - q_load) / base_mva (per-unit)
bus_typesNode type enum (Slack / PV / PQ), which determines the solve variables
v_initialInitial 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:

AlgorithmApplicable ScenarioConvergenceSpeed
NewtonRaphsonTransmission networks, meshed gridsQuadratic convergenceMedium
BackwardForwardSweepDistribution networks, radialLinear convergenceFast
DCReal-time screening, planningLinearFastest

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:

MetricExpectedTolerance
Convergencetrue
Iterations3-5< 20
Max mismatch< 1e-8 p.u.
Slack active power≈ 232.4 MW±1%
Total losses≈ 13.8 MW±1 MW
All bus voltages0.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_initial for warm start, or enable RecycleCache
  • 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