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
| Metric | Value | Description |
|---|---|---|
| Node types | 3 | Bus / Branch / Generator |
| Topology graph implementation | petgraph | Sparse adjacency matrix |
| IEEE test systems | 1 | 14-bus |
| Topology validation rules | 12 | Covers common modeling errors |
| 14-bus loading time | 280 μs | Including parsing and graph building |
| Topology traversal throughput | 1.8 million nodes/sec | BFS 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:
| Item | Value |
|---|---|
| Bus count | 14 |
| Branch count | 20 (including 3 transformers) |
| Generator count | 5 |
| Load count | 9 |
| Total system active power | 259 MW |
| Total system reactive power | 73.5 MVar |
| Highest voltage level | 138 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: AddedResourceIdtype for uniquely identifying kernel resources- Error codes: Added E3xxx topology error category with 10 error codes
- CI: Added
cargo docdeployment 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_branchnot rejecting self-loops whenfrom_bus == to_bus(#45) - Fixed
electrical_islandsreturning empty islands (#49) - Fixed
PiParameters::linedefaulttap_rationot being 1.0 (#52) - Fixed impedance data error for branch 8 in IEEE 14-bus data (#55)
Breaking Changes
eneros_core::Handle: Addedresource_typefield to distinguish resource types such as buses and branchesSyscall::ReadBus: Return type changed fromBustoBusState, containing only runtime state fields
Performance Improvements
| Operation | v0.2.0 | v0.3.0 | Description |
|---|---|---|---|
| 14-bus loading | N/A | 280 μs | Including parsing and graph building |
| BFS traversal of 14 nodes | N/A | 12 μs | - |
| Electrical island partitioning | N/A | 28 μs | - |
bus() query | N/A | 18 ns | HashMap lookup |
neighbors() | N/A | 240 ns | Average 2.5 neighbors |
Contributors
| Contributor | Role | Commits |
|---|---|---|
| @eneros-foundation | Architect | 38 |
| @grid-rustacean | Rust Engineer | 45 |
| @cim-expert | CIM Standard Expert | 22 |
| @powerdomain-reviewer | Power Domain Expert | 14 |
| @test-eng | Test Engineer | 11 |
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");