Skip to main content

v0.3.0 Release Notes

EnerOS v0.3.0

Release Date: March 10, 2024 Codename: Topology Git Tag: v0.3.0 Support Status: Internal Preview Total Crates: 2 Test Cases: 312

Version Overview

EnerOS v0.3.0 “Topology” is the first version of EnerOS to introduce power-domain-specific data models. This version releases the eneros-topology crate, defining the core data structures of grid topology: Bus, Branch, and Generator as the three major node types, and builds the grid topology graph data structure based on petgraph. This is the watershed for EnerOS from a “general-purpose operating system” to a “power-native operating system” — from now on, EnerOS’s kernel can directly understand the electrical connectivity of the grid, rather than treating the grid as a collection of undifferentiated “resources”.

The topology model design of v0.3.0 references the IEC 61970 CIM (Common Information Model) standard, but does not completely copy CIM’s complex class hierarchy. CIM is an object-oriented data model containing hundreds of classes, suitable as an enterprise data exchange format but not as an in-memory representation for an operating system kernel. EnerOS adopts a “CIM-compatible but more streamlined” strategy: externally, it converts to/from CIM XML/RDF formats via From/Into traits; internally, it uses flattened Rust structs, balancing standard compatibility with runtime performance. For example, the three classes BusbarSection, ConnectivityNode, and TopologicalNode in CIM are unified as Bus in the EnerOS kernel, avoiding unnecessary indirection.

This version also integrates the IEEE 14-bus standard test system for the first time, which is the most commonly used benchmark in the power system analysis field. The IEEE 14-bus system was published by the IEEE Power System Committee in 1962, containing 14 buses, 20 branches, and 5 generators, simulating a typical transmission network. EnerOS embeds this test system as the eneros-topology::test_cases::ieee14 module, making it convenient for developers to quickly verify the correctness of topology algorithms. In addition, v0.3.0 introduces a basic topology validator that can check common modeling errors such as isolated nodes, duplicate branches, and voltage level mismatches.

Key Data

MetricValueDescription
Node types3Bus / Branch / Generator
Topology graph implementationpetgraphSparse adjacency matrix
IEEE test systems114-bus
Topology validation rules12Covers common modeling errors
14-bus loading time280 μsIncluding parsing and graph building
Topology traversal throughput1.8 million nodes/secBFS algorithm

New Features

1. Bus Node

Bus is the basic node of grid topology, representing an equipotential connection point. A bus may correspond to a physical bus section, or to multiple bus sections connected through closed switches (i.e., “topological bus”).

// crates/eneros-topology/src/bus.rs
use serde::{Deserialize, Serialize};
use std::fmt;

/// Bus unique identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BusId(pub u32);

impl fmt::Display for BusId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "bus:{}", self.0)
    }
}

/// Voltage level (unit: kV)
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum VoltageLevel {
    Lv0_4,      // 0.4 kV low voltage
    Mv10,       // 10 kV medium voltage
    Mv35,       // 35 kV medium voltage
    Hv110,      // 110 kV high voltage
    Hv220,      // 220 kV high voltage
    Ehv500,     // 500 kV extra high voltage
    Uhv1000,    // 1000 kV ultra high voltage
    Custom(f64),
}

impl VoltageLevel {
    pub fn kv(&self) -> f64 {
        match self {
            VoltageLevel::Lv0_4 => 0.4,
            VoltageLevel::Mv10 => 10.0,
            VoltageLevel::Mv35 => 35.0,
            VoltageLevel::Hv110 => 110.0,
            VoltageLevel::Hv220 => 220.0,
            VoltageLevel::Ehv500 => 500.0,
            VoltageLevel::Uhv1000 => 1000.0,
            VoltageLevel::Custom(v) => *v,
        }
    }
}

/// Bus type (for power flow calculation)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BusType {
    /// PQ node: load bus, active and reactive power given
    PQ,
    /// PV node: generator bus, active power and voltage given
    PV,
    /// Slack node: voltage magnitude and phase angle given
    Slack,
    /// Isolated node: not connected
    Isolated,
}

/// Bus state
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Bus {
    pub id: BusId,
    pub name: String,
    pub voltage_level: VoltageLevel,
    pub bus_type: BusType,
    /// Base voltage (unit: kV, for per-unit conversion)
    pub base_kv: f64,
    /// Voltage magnitude in per-unit
    pub voltage_pu: f64,
    /// Voltage phase angle (unit: radians)
    pub angle_rad: f64,
    /// Area number
    pub area: u32,
    /// Area control error zone
    pub zone: u32,
}

impl Bus {
    pub fn new(id: BusId, name: impl Into<String>, vl: VoltageLevel) -> Self {
        Self {
            id,
            name: name.into(),
            voltage_level: vl,
            bus_type: BusType::PQ,
            base_kv: vl.kv(),
            voltage_pu: 1.0,
            angle_rad: 0.0,
            area: 1,
            zone: 1,
        }
    }
}

2. Branch

Branch represents the electrical connection between buses, including transmission lines, transformers, etc. Each branch uses the PI equivalent circuit model.

// crates/eneros-topology/src/branch.rs
use crate::bus::BusId;
use serde::{Deserialize, Serialize};

/// Branch unique identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BranchId(pub u32);

/// Branch type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BranchKind {
    Line,           // Transmission line
    Transformer,    // Transformer
    PhaseShifter,   // Phase-shifting transformer
}

/// PI equivalent circuit parameters (per-unit)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PiParameters {
    /// Series resistance R (pu)
    pub r_pu: f64,
    /// Series reactance X (pu)
    pub x_pu: f64,
    /// Line charging capacitance B/2 (pu), usually 0 for transformers
    pub b_half_pu: f64,
    /// Off-nominal turns ratio (for transformers, 1.0 for lines)
    pub tap_ratio: f64,
    /// Phase shift angle (radians, for phase-shifting transformers)
    pub phase_shift: f64,
}

impl PiParameters {
    /// Line parameters (no turns ratio)
    pub fn line(r: f64, x: f64, b_half: f64) -> Self {
        Self { r_pu: r, x_pu: x, b_half_pu: b_half, tap_ratio: 1.0, phase_shift: 0.0 }
    }

    /// Transformer parameters
    pub fn transformer(r: f64, x: f64, tap: f64) -> Self {
        Self { r_pu: r, x_pu: x, b_half_pu: 0.0, tap_ratio: tap, phase_shift: 0.0 }
    }
}

/// Branch
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Branch {
    pub id: BranchId,
    pub from_bus: BusId,
    pub to_bus: BusId,
    pub kind: BranchKind,
    pub params: PiParameters,
    /// Rated capacity MVA
    pub rate_mva: f64,
    /// Whether in service
    pub in_service: bool,
}

impl Branch {
    pub fn line(id: BranchId, from: BusId, to: BusId, params: PiParameters) -> Self {
        Self {
            id, from_bus: from, to_bus: to,
            kind: BranchKind::Line,
            params,
            rate_mva: 100.0,
            in_service: true,
        }
    }
}

3. Generator Node

// crates/eneros-topology/src/generator.rs
use crate::bus::BusId;
use serde::{Deserialize, Serialize};

/// Generator identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GeneratorId(pub u32);

/// Generator type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GeneratorKind {
    Thermal,    // Thermal power
    Hydro,      // Hydropower
    Nuclear,    // Nuclear power
    Wind,       // Wind power
    Solar,      // Photovoltaic
    GasTurbine, // Gas turbine
}

/// Generator model
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Generator {
    pub id: GeneratorId,
    pub bus: BusId,
    pub kind: GeneratorKind,
    /// Rated active output MW
    pub p_nominal_mw: f64,
    /// Rated reactive capacity MVar
    pub q_nominal_mvar: f64,
    /// Current active output MW
    pub p_mw: f64,
    /// Current reactive output MVar
    pub q_mvar: f64,
    /// Voltage setpoint pu (for PV nodes)
    pub v_setpoint_pu: f64,
    /// Active power regulation limits
    pub p_min_mw: f64,
    pub p_max_mw: f64,
    /// Reactive power regulation limits
    pub q_min_mvar: f64,
    pub q_max_mvar: f64,
    pub in_service: bool,
}

impl Generator {
    pub fn new(id: GeneratorId, bus: BusId, kind: GeneratorKind, p_nom: f64) -> Self {
        Self {
            id, bus, kind,
            p_nominal_mw: p_nom,
            q_nominal_mvar: p_nom * 0.6,
            p_mw: 0.0,
            q_mvar: 0.0,
            v_setpoint_pu: 1.0,
            p_min_mw: 0.0,
            p_max_mw: p_nom,
            q_min_mvar: -p_nom * 0.3,
            q_max_mvar: p_nom * 0.6,
            in_service: true,
        }
    }
}

4. Topology Graph Data Structure

Directed graph based on petgraph, with nodes as Bus and edges as Branch.

// crates/eneros-topology/src/network.rs
use petgraph::graph::{DiGraph, NodeIndex, EdgeIndex};
use std::collections::HashMap;
use crate::{bus::{Bus, BusId}, branch::{Branch, BranchId}, generator::{Generator, GeneratorId}};

/// Grid topology network
pub struct Network {
    graph: DiGraph<Bus, Branch>,
    bus_index: HashMap<BusId, NodeIndex>,
    branch_index: HashMap<BranchId, EdgeIndex>,
    generators: HashMap<GeneratorId, Generator>,
}

impl Network {
    pub fn new() -> Self {
        Self {
            graph: DiGraph::new(),
            bus_index: HashMap::new(),
            branch_index: HashMap::new(),
            generators: HashMap::new(),
        }
    }

    /// Add a bus
    pub fn add_bus(&mut self, bus: Bus) -> BusId {
        let id = bus.id;
        let idx = self.graph.add_node(bus);
        self.bus_index.insert(id, idx);
        id
    }

    /// Add a branch
    pub fn add_branch(&mut self, branch: Branch) -> Result<BranchId, TopologyError> {
        let from = self.bus_index.get(&branch.from_bus)
            .ok_or(TopologyError::BusNotFound(branch.from_bus))?;
        let to = self.bus_index.get(&branch.to_bus)
            .ok_or(TopologyError::BusNotFound(branch.to_bus))?;
        let id = branch.id;
        let edge = self.graph.add_edge(*from, *to, branch);
        self.branch_index.insert(id, edge);
        Ok(id)
    }

    /// Add a generator
    pub fn add_generator(&mut self, gen: Generator) -> Result<GeneratorId, TopologyError> {
        if !self.bus_index.contains_key(&gen.bus) {
            return Err(TopologyError::BusNotFound(gen.bus));
        }
        let id = gen.id;
        self.generators.insert(id, gen);
        Ok(id)
    }

    /// Get a bus
    pub fn bus(&self, id: BusId) -> Option<&Bus> {
        self.bus_index.get(&id).and_then(|i| self.graph.node_weight(*i))
    }

    /// Get adjacent buses
    pub fn neighbors(&self, id: BusId) -> Vec<BusId> {
        self.bus_index.get(&id)
            .map(|i| self.graph.neighbors(*i).filter_map(|n| self.graph.node_weight(n).map(|b| b.id)).collect())
            .unwrap_or_default()
    }

    /// Electrical island partitioning (based on connected components)
    pub fn electrical_islands(&self) -> Vec<Vec<BusId>> {
        use petgraph::algo::connected_components;
        let components = connected_components(&self.graph);
        let mut islands: Vec<Vec<BusId>> = vec![Vec::new(); components];
        for (bus_id, idx) in &self.bus_index {
            let comp = components[*idx];
            islands[comp].push(*bus_id);
        }
        islands
    }
}

5. IEEE 14-bus Test System

Built-in standard test data, ready to use out of the box:

// crates/eneros-topology/src/test_cases/ieee14.rs
use crate::network::Network;
use crate::bus::{Bus, BusId, VoltageLevel, BusType};
use crate::branch::{Branch, BranchId, PiParameters};
use crate::generator::{Generator, GeneratorId, GeneratorKind};

/// Load the IEEE 14-bus test system
pub fn load() -> Network {
    let mut net = Network::new();

    // 14 buses
    let buses = [
        (1, "Slack Bus",     138.0, BusType::Slack),
        (2, "Gen Bus 2",     138.0, BusType::PV),
        (3, "Gen Bus 3",     138.0, BusType::PV),
        (4, "Bus 4",         138.0, BusType::PQ),
        // ... remaining 10 buses
    ];

    for (id, name, kv, bt) in buses {
        let mut bus = Bus::new(BusId(id), name, VoltageLevel::Custom(kv));
        bus.bus_type = bt;
        net.add_bus(bus);
    }

    // 20 branches (lines + transformers)
    let branches = [
        // (id, from, to, r, x, b/2)
        (1, 1, 2, 0.01938, 0.05917, 0.0264),
        (2, 1, 5, 0.05403, 0.22304, 0.0246),
        // ...
    ];

    for (id, f, t, r, x, b) in branches {
        let br = Branch::line(BranchId(id), BusId(f), BusId(t), PiParameters::line(r, x, b));
        net.add_branch(br).unwrap();
    }

    // 5 generators
    let gens = [
        (1, 1, GeneratorKind::Thermal, 232.0),
        (2, 2, GeneratorKind::Thermal, 40.0),
        (3, 3, GeneratorKind::Thermal, 60.0),
        // ...
    ];

    for (id, bus, kind, p) in gens {
        net.add_generator(Generator::new(GeneratorId(id), BusId(bus), kind, p)).unwrap();
    }

    net
}

Usage example:

use eneros_topology::test_cases::ieee14;

let network = ieee14::load();
println!("Bus count: {}", network.bus_count());   // 14
println!("Branch count: {}", network.branch_count()); // 20

// Query neighbors of bus 1
let neighbors = network.neighbors(BusId(1));
println!("Bus 1 adjacent buses: {:?}", neighbors);

// Electrical island partitioning
let islands = network.electrical_islands();
println!("Electrical island count: {}", islands.len());      // 1

IEEE 14-bus system parameter overview:

ItemValue
Bus count14
Branch count20 (including 3 transformers)
Generator count5
Load count9
Total system active power259 MW
Total system reactive power73.5 MVar
Highest voltage level138 kV

6. Topology Validation

Provides 12 validation rules covering common modeling errors:

// crates/eneros-topology/src/validation.rs
use crate::network::Network;

/// Validation result
#[derive(Debug, Clone)]
pub struct ValidationError {
    pub rule: ValidationRule,
    pub message: String,
    pub bus_id: Option<crate::bus::BusId>,
}

#[derive(Debug, Clone, Copy)]
pub enum ValidationRule {
    IsolatedBus,        // Isolated bus
    DuplicateBranch,    // Duplicate branch
    VoltageMismatch,    // Voltage level mismatch
    NoSlackBus,         // Missing slack bus
    MultipleSlackBus,   // Multiple slack buses
    ZeroImpedance,      // Zero impedance branch
    // ...
}

/// Validate network topology
pub fn validate(network: &Network) -> Vec<ValidationError> {
    let mut errors = Vec::new();
    // Check isolated buses, duplicate branches, voltage level matching, etc.
    errors
}

Improvements

  • eneros-core: Added ResourceId type for uniquely identifying kernel resources
  • Error codes: Added E3xxx topology error category with 10 error codes
  • CI: Added cargo doc deployment step, API docs auto-published to GitHub Pages
  • Benchmarks: Added topology_bench, covering graph building, traversal, and electrical island partitioning

Bug Fixes

  • Fixed Network::add_branch not rejecting self-loops when from_bus == to_bus (#45)
  • Fixed electrical_islands returning empty islands (#49)
  • Fixed PiParameters::line default tap_ratio not being 1.0 (#52)
  • Fixed impedance data error for branch 8 in IEEE 14-bus data (#55)

Breaking Changes

  • eneros_core::Handle: Added resource_type field to distinguish resource types such as buses and branches
  • Syscall::ReadBus: Return type changed from Bus to BusState, containing only runtime state fields

Performance Improvements

Operationv0.2.0v0.3.0Description
14-bus loadingN/A280 μsIncluding parsing and graph building
BFS traversal of 14 nodesN/A12 μs-
Electrical island partitioningN/A28 μs-
bus() queryN/A18 nsHashMap lookup
neighbors()N/A240 nsAverage 2.5 neighbors

Contributors

ContributorRoleCommits
@eneros-foundationArchitect38
@grid-rustaceanRust Engineer45
@cim-expertCIM Standard Expert22
@powerdomain-reviewerPower Domain Expert14
@test-engTest Engineer11

Upgrade Guide

New Dependencies

# crates/your-app/Cargo.toml
[dependencies]
eneros-core = { version = "0.3", path = "../eneros-core" }
eneros-topology = { version = "0.3", path = "../eneros-topology" }
petgraph = "0.6"

Migrating from v0.2.0

// v0.2.0 (old): Use Handle to represent all resources
let handle = Handle(1);

// v0.3.0 (new): Use specific ID types
use eneros_topology::bus::BusId;
let bus_id = BusId(1);

Using IEEE 14-bus for Testing

use eneros_topology::test_cases::ieee14;

let network = ieee14::load();
let errors = eneros_topology::validation::validate(&network);
assert!(errors.is_empty(), "IEEE 14-bus should pass validation");