Skip to main content

v0.4.0 Release Notes

EnerOS v0.4.0

Release Date: April 7, 2024 Codename: PowerFlow Git Tag: v0.4.0 Support Status: Internal Preview Total Crates: 3 Test Cases: 487

Version Overview

EnerOS v0.4.0 “PowerFlow” introduces the core algorithm of power system analysis — Power Flow / Load Flow calculation. This version releases the eneros-powerflow crate, implementing a power flow solver based on the Newton-Raphson iterative method, which can solve the voltage magnitude and phase angle of each bus based on the grid topology and node injection power, and then calculate the power flow distribution of each branch. Power flow calculation is the “first-principle problem” of power system analysis — whether for operational mode verification, fault analysis, or economic dispatch, power flow results serve as the basic data.

The Newton-Raphson method is the de facto standard power flow algorithm in the power industry. Its core idea is to use the Jacobian Matrix of node power equations for linearized iteration. Compared with the simple but slow-converging Gauss-Seidel method, the Newton-Raphson method has quadratic convergence, typically converging to 1e-8 precision in 4-8 iterations. The v0.4.0 implementation uses sparse LU decomposition (based on the nalgebra-sparse crate) to solve linear equations, which is key to handling large grids (1000+ buses) — the Jacobian matrix sparsity is typically over 99%, and full dense solving is both slow and memory-wasteful.

v0.4.0 fully supports the handling of PQ, PV, and Slack node types: PQ nodes (load buses) have active and reactive power given with voltage to be solved; PV nodes (generator buses) have active power and voltage magnitude given with reactive power and phase angle to be solved; Slack nodes (balance buses) have voltage magnitude and phase angle given, serving as the power balance point for the entire system. The solver also implements convergence determination (maximum power mismatch < 1e-8 pu) and various stopping conditions (maximum iteration count, divergence detection). This version verified correctness on three standard test systems: IEEE 14-bus, 30-bus, and 118-bus, with errors less than 1e-6 compared to MATPOWER reference solutions.

Key Data

MetricValueDescription
Solver algorithmNewton-RaphsonQuadratic convergence
Linear algebra backendnalgebra-sparseSparse LU
IEEE test systems314/30/118-bus
14-bus solve time1.8 ms4 iterations
118-bus solve time18 ms5 iterations
Convergence precision1e-8 puPower mismatch

New Features

1. Newton-Raphson Power Flow Solver

// crates/eneros-powerflow/src/newton_raphson.rs
use eneros_topology::{network::Network, bus::{BusId, BusType}};
use nalgebra_sparse::CscMatrix;

/// Power flow solver configuration
#[derive(Debug, Clone)]
pub struct PowerFlowConfig {
    /// Maximum iteration count
    pub max_iterations: usize,
    /// Convergence tolerance (power mismatch, pu)
    pub tolerance: f64,
    /// Whether to enable sparse solving
    pub sparse: bool,
    /// Acceleration factor (default 1.0)
    pub acceleration_factor: f64,
}

impl Default for PowerFlowConfig {
    fn default() -> Self {
        Self {
            max_iterations: 50,
            tolerance: 1e-8,
            sparse: true,
            acceleration_factor: 1.0,
        }
    }
}

/// Power flow solver result
#[derive(Debug, Clone)]
pub struct PowerFlowResult {
    /// Voltage magnitude of each bus (pu)
    pub voltage_pu: Vec<f64>,
    /// Voltage phase angle of each bus (radians)
    pub angle_rad: Vec<f64>,
    /// Active power flow of each branch (MW, from end)
    pub branch_p_mw: Vec<f64>,
    /// Reactive power flow of each branch (MVar, from end)
    pub branch_q_mvar: Vec<f64>,
    /// Iteration count
    pub iterations: usize,
    /// Maximum power mismatch
    pub max_mismatch: f64,
    /// Whether converged
    pub converged: bool,
    /// Solve time
    pub elapsed_us: u64,
}

/// Newton-Raphson power flow solver
pub struct NewtonRaphsonSolver<'a> {
    network: &'a Network,
    config: PowerFlowConfig,
}

impl<'a> NewtonRaphsonSolver<'a> {
    pub fn new(network: &'a Network) -> Self {
        Self { network, config: PowerFlowConfig::default() }
    }

    pub fn with_config(mut self, config: PowerFlowConfig) -> Self {
        self.config = config;
        self
    }

    /// Execute power flow calculation
    pub fn solve(&self) -> Result<PowerFlowResult, PowerFlowError> {
        let start = std::time::Instant::now();
        let n = self.network.bus_count();

        // Initialize voltage (flat start: voltage 1.0, phase angle 0)
        let mut voltage = vec![1.0f64; n];
        let mut angle = vec![0.0f64; n];

        // Build node admittance matrix Ybus
        let ybus = self.build_ybus();

        let mut iterations = 0;
        let mut converged = false;
        let mut max_mismatch = f64::MAX;

        while iterations < self.config.max_iterations {
            // Calculate power mismatch ΔP, ΔQ
            let (delta_p, delta_q) = self.compute_mismatch(&voltage, &angle, &ybus);
            max_mismatch = self.max_mismatch(&delta_p, &delta_q);

            if max_mismatch < self.config.tolerance {
                converged = true;
                break;
            }

            // Build Jacobian matrix J
            let jacobian = self.build_jacobian(&voltage, &angle, &ybus);

            // Solve correction equation J * [Δθ, ΔV] = [ΔP, ΔQ]
            let correction = self.solve_linear(&jacobian, &delta_p, &delta_q)?;

            // Update voltage and phase angle
            self.update_state(&mut voltage, &mut angle, &correction);
            iterations += 1;
        }

        // Calculate branch power flow
        let (branch_p, branch_q) = self.compute_branch_flow(&voltage, &angle, &ybus);

        Ok(PowerFlowResult {
            voltage_pu: voltage,
            angle_rad: angle,
            branch_p_mw: branch_p,
            branch_q_mvar: branch_q,
            iterations,
            max_mismatch,
            converged,
            elapsed_us: start.elapsed().as_micros() as u64,
        })
    }
}

2. Jacobian Matrix Construction

The Jacobian matrix is the core of the Newton-Raphson method; it describes the partial derivatives of power mismatch with respect to state variables.

// crates/eneros-powerflow/src/jacobian.rs
use nalgebra_sparse::CscMatrix;

/// 4 submatrices form the Jacobian matrix:
/// J = [ H  N ]
///     [ M  L ]
///
/// H = ∂P/∂θ, N = ∂P/∂V
/// M = ∂Q/∂θ, L = ∂Q/∂V
pub struct JacobianBuilder<'a> {
    network: &'a Network,
    ybus: &'a CscMatrix<Complex>,
}

impl<'a> JacobianBuilder<'a> {
    /// Build H submatrix (∂P/∂θ)
    pub fn build_h(&self, v: &[f64], theta: &[f64]) -> CscMatrix<f64> {
        let n = self.network.bus_count();
        let mut h = CscMatrix::zeros(n, n);
        // Calculate H_ij for each pair of buses (i, j)
        for i in 0..n {
            for j in 0..n {
                let y_ij = self.ybus[(i, j)];
                let h_ij = self.compute_h_ij(i, j, v, theta, y_ij);
                h[(i, j)] = h_ij;
            }
        }
        h
    }

    fn compute_h_ij(&self, i: usize, j: usize, v: &[f64], theta: &[f64], y_ij: Complex) -> f64 {
        if i == j {
            // H_ii = -Q_i - B_ii * V_i^2
            let q_i = self.compute_q_at_bus(i, v, theta);
            let b_ii = self.ybus[(i, i)].im;
            -q_i - b_ii * v[i] * v[i]
        } else {
            // H_ij = V_i * V_j * (G_ij * sin(θ_ij) - B_ij * cos(θ_ij))
            let g_ij = y_ij.re;
            let b_ij = y_ij.im;
            let theta_diff = theta[i] - theta[j];
            v[i] * v[j] * (g_ij * theta_diff.sin() - b_ij * theta_diff.cos())
        }
    }
}

3. PQ/PV/Slack Node Handling

// crates/eneros-powerflow/src/node_handler.rs
use eneros_topology::bus::BusType;

/// Node handler: organizes unknowns and equations by node type
pub struct NodeHandler {
    /// PQ node indices (θ, V to be solved)
    pub pq_buses: Vec<usize>,
    /// PV node indices (θ to be solved, V known)
    pub pv_buses: Vec<usize>,
    /// Slack node indices (θ, V known)
    pub slack_buses: Vec<usize>,
}

impl NodeHandler {
    pub fn from_network(network: &Network) -> Self {
        let mut handler = Self {
            pq_buses: Vec::new(),
            pv_buses: Vec::new(),
            slack_buses: Vec::new(),
        };
        for (idx, bus) in network.buses().enumerate() {
            match bus.bus_type {
                BusType::PQ => handler.pq_buses.push(idx),
                BusType::PV => handler.pv_buses.push(idx),
                BusType::Slack => handler.slack_buses.push(idx),
                BusType::Isolated => {} // Skip isolated nodes
            }
        }
        handler
    }

    /// Total unknowns = 2*|PQ| + |PV| (θ to be solved, V partially to be solved)
    pub fn unknown_count(&self) -> usize {
        2 * self.pq_buses.len() + self.pv_buses.len()
    }
}

4. Convergence Determination

// crates/eneros-powerflow/src/convergence.rs

/// Convergence checker
pub struct ConvergenceChecker {
    tolerance: f64,
}

impl ConvergenceChecker {
    pub fn new(tolerance: f64) -> Self {
        Self { tolerance }
    }

    /// Check whether converged
    pub fn is_converged(&self, delta_p: &[f64], delta_q: &[f64]) -> bool {
        let max_p = delta_p.iter().cloned().fold(0.0f64, f64::max).abs();
        let max_q = delta_q.iter().cloned().fold(0.0f64, f64::max).abs();
        let max_mismatch = max_p.max(max_q);
        max_mismatch < self.tolerance
    }

    /// Detect whether diverged
    pub fn is_diverged(&self, iter: usize, history: &[f64]) -> bool {
        if iter < 3 {
            return false;
        }
        // If mismatch increases for 3 consecutive iterations, determine as diverged
        let n = history.len();
        if n >= 3 {
            history[n-3] < history[n-2] && history[n-2] < history[n-1]
        } else {
            false
        }
    }
}

5. IEEE Standard Tests

// crates/eneros-powerflow/tests/ieee_tests.rs
use eneros_topology::test_cases::{ieee14, ieee30, ieee118};
use eneros_powerflow::newton_raphson::NewtonRaphsonSolver;

#[test]
fn test_ieee14_converges() {
    let network = ieee14::load();
    let solver = NewtonRaphsonSolver::new(&network);
    let result = solver.solve().unwrap();

    assert!(result.converged, "IEEE 14 should converge");
    assert!(result.iterations <= 10, "Iteration count should be <= 10");
    assert!(result.max_mismatch < 1e-8, "Mismatch should be < 1e-8");

    // Compare with MATPOWER reference solution
    assert!((result.voltage_pu[0] - 1.06).abs() < 1e-4, "Bus 1 voltage should be ≈ 1.06");
}

#[test]
fn test_ieee30_converges() {
    let network = ieee30::load();
    let result = NewtonRaphsonSolver::new(&network).solve().unwrap();
    assert!(result.converged);
}

#[test]
fn test_ieee118_converges() {
    let network = ieee118::load();
    let result = NewtonRaphsonSolver::new(&network).solve().unwrap();
    assert!(result.converged);
    assert!(result.iterations <= 10);
}

Solve example:

use eneros_topology::test_cases::ieee14;
use eneros_powerflow::newton_raphson::{NewtonRaphsonSolver, PowerFlowConfig};

let network = ieee14::load();
let solver = NewtonRaphsonSolver::new(&network)
    .with_config(PowerFlowConfig {
        max_iterations: 20,
        tolerance: 1e-8,
        ..Default::default()
    });

let result = solver.solve()?;
println!("Converged: {}, iterations: {}", result.converged, result.iterations);
println!("Bus 1 voltage: {:.4} pu, phase angle: {:.4}°",
    result.voltage_pu[0],
    result.angle_rad[0].to_degrees());

Improvements

  • eneros-topology: Added Network::buses(), Network::branches() iterators
  • eneros-core: Added Complex complex number type (based on num-complex)
  • Error codes: Added E4xxx power flow error category
  • Dependencies: Introduced nalgebra, nalgebra-sparse as linear algebra backend

Bug Fixes

  • Fixed build_ybus calculating admittance incorrectly when transformer turns ratio is not 1.0 (#61)
  • Fixed PV nodes not automatically converting to PQ nodes when reactive power limits are violated (#65)
  • Fixed sign error in branch loss calculation in compute_branch_flow (#68)
  • Fixed parameter error for branch 56 in IEEE 118-bus (#71)

Breaking Changes

  • eneros_topology::Bus: voltage_pu and angle_rad fields moved to runtime state, no longer included in the struct
  • Network: Added bus_count(), branch_count() methods

Performance Improvements

Test Systemv0.3.0v0.4.0IterationsConvergence Precision
IEEE 14N/A1.8 ms41e-8
IEEE 30N/A4.2 ms41e-8
IEEE 118N/A18 ms51e-8

Comparison with MATPOWER (MATLAB implementation):

Test SystemEnerOS v0.4.0MATPOWER 8.0Speedup
IEEE 141.8 ms3.1 ms1.7x
IEEE 304.2 ms7.8 ms1.9x
IEEE 11818 ms42 ms2.3x

Jacobian matrix sparsity:

Test SystemMatrix SizeNon-zero ElementsSparsity
IEEE 1414×145671.4%
IEEE 3030×3011087.8%
IEEE 118118×11854296.1%

Contributors

ContributorRoleCommits
@eneros-foundationArchitect35
@powerflow-expertPower Flow Algorithm Expert48
@grid-rustaceanRust Engineer29
@numerical-analysisNumerical Computation Engineer22
@matpower-compareVerification Engineer9

Upgrade Guide

New Dependencies

[dependencies]
eneros-powerflow = { version = "0.4", path = "../eneros-powerflow" }
nalgebra = "0.32"
nalgebra-sparse = "0.9"
num-complex = "0.4"

Migrating from v0.3.0

The voltage_pu and angle_rad fields in the Bus struct have been removed and are now provided by power flow calculation results:

// v0.3.0 (old)
let bus = network.bus(BusId(1)).unwrap();
println!("Voltage: {}", bus.voltage_pu);

// v0.4.0 (new)
let result = NewtonRaphsonSolver::new(&network).solve()?;
let v = result.voltage_pu[0];
println!("Voltage: {}", v);

Custom Power Flow Configuration

let config = PowerFlowConfig {
    max_iterations: 100,
    tolerance: 1e-10,
    sparse: true,
    acceleration_factor: 1.6,
};
let result = NewtonRaphsonSolver::new(&network)
    .with_config(config)
    .solve()?;