Grid Topology First-Class Citizen
EnerOS treats grid topology as a first-class citizen data structure of the operating system kernel, rather than an application-layer data file. This means abstractions such as NetworkGraph, Bus, and Branch are natively supported in kernel space, and all upper-layer components—Agents, dispatchers, protection logic, digital twins—share the same single grid topology view, avoiding the overhead of redundant modeling and consistency synchronization.
“Topology as a first-class citizen” is the direct realization of the Power-Native First design philosophy: the physical structure of the power grid is itself part of the operating system’s worldview. Just as the Linux kernel understands file systems and process trees, the EnerOS kernel understands buses, branches, electrical islands, and connectivity.
Design Motivation
Traditional power software models grid topology as application-layer JSON / XML / CIM files that must be reloaded and parsed for every load flow calculation, state estimation, and protection setting. This causes three problems:
- Redundant modeling: Dispatch, operations, and protection each maintain their own copy of the topology, leading to severe state drift
- Event staleness: Topology changes cannot be notified to all subscribers in real time, so decisions are based on stale views
- Performance waste: Each parsing and topology-graph construction consumes 10–100 ms, which cannot support real-time applications
By sinking topology into the kernel space, EnerOS allows all components to access the same topology object through system calls. Change events are broadcast by the kernel, and single-pass topology analysis latency drops below 1 ms.
Architecture Overview
┌─────────────────────────────────────────────────┐
│ Application Layer: DispatchAgent / ProtectionAgent / Twin │
└───────────────────┬─────────────────────────────┘
│ System Calls get_network / subscribe
┌───────────────────┴─────────────────────────────┐
│ eneros-topology (kernel-native topology engine) │
│ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ NetworkGraph│ │ TopologyOps │ │ EventBus │ │
│ └─────────────┘ └─────────────┘ └───────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ Bus / Branch│ │ IslandIndex │ │ VersionLog│ │
│ └─────────────┘ └─────────────┘ └───────────┘ │
└─────────────────────────────────────────────────┘
Core Abstractions
NetworkGraph
NetworkGraph is the entry point for all grid topology operations in EnerOS. It is built on a petgraph-based directed graph structure, augmented with power-system-specific indexes and metadata.
use eneros_topology::{NetworkGraph, Bus, Branch, BusType, BranchParams, Voltage};
let mut network = NetworkGraph::new();
// Add buses
network.add_bus(Bus::new(1, BusType::PQ, Voltage::new(1.0, 0.0)));
network.add_bus(Bus::new(2, BusType::PV, Voltage::new(1.02, 0.0)));
network.add_bus(Bus::new(3, BusType::Slack, Voltage::new(1.05, 0.0)));
// Add branches
network.add_branch(Branch::new(1, 2, BranchParams::line(0.1, 0.2, 0.02)));
network.add_branch(Branch::new(2, 3, BranchParams::transformer(0.05, 0.1, 1.0)));
Bus Field Reference
| Field | Type | Meaning | Default |
|---|---|---|---|
| id | u32 | Bus number, globally unique | Required |
| bus_type | BusType | PQ / PV / Slack / Isolated | PQ |
| voltage | Voltage | Voltage magnitude and phase angle (p.u.) | (0.0, 0.0) |
| area | u16 | Area number it belongs to | 0 |
| zone | u16 | Zone number it belongs to | 0 |
| base_kv | f64 | Base voltage (kV) | 110.0 |
| v_max | f64 | Maximum operating voltage (p.u.) | 1.10 |
| v_min | f64 | Minimum operating voltage (p.u.) | 0.90 |
| name | String | Human-readable name | "" |
Branch Field Reference
| Field | Type | Meaning | Default |
|---|---|---|---|
| from | u32 | Source bus ID | Required |
| to | u32 | Destination bus ID | Required |
| params | BranchParams | Resistance, reactance, shunt admittance, turns ratio | Required |
| status | BranchStatus | Closed / Open / Grounded | Closed |
| rating_mva | f64 | Rated capacity (MVA) | 100.0 |
| emergency_mva | f64 | 15-minute emergency capacity | rating_mva * 1.2 |
| length_km | f64 | Line length (km) | 0.0 |
BranchParams Constructors
use eneros_topology::BranchParams;
// Transmission line Π-model parameters: (r, x, b/2)
let line = BranchParams::line(0.1, 0.2, 0.02);
// Transformer parameters: (r, x, turns ratio)
let xfmr = BranchParams::transformer(0.05, 0.1, 1.0);
// Three-winding transformer (referred to the high-voltage side)
let three_winding = BranchParams::three_winding(
(0.01, 0.15, 1.0),
(0.01, 0.10, 1.0),
(0.01, 0.05, 1.0),
);
// Distributed-parameter line (Bergeron model)
let distributed = BranchParams::distributed(
0.025, // r (Ω/km)
0.3, // x (Ω/km)
0.015, // b (μS/km)
150.0, // length (km)
);
Topology Analysis
The kernel has built-in common topology analysis algorithms, all completed in a single traversal, with no need for the application layer to reimplement them.
let topology = network.topology();
// Electrical island detection
let islands = topology.find_islands();
// Connectivity check
let connected = topology.is_connected(bus1, bus2);
// Shortest path (by hop count)
let path = topology.shortest_path(bus1, bus2)?;
// Shortest path (by impedance)
let path_z = topology.shortest_path_by_impedance(bus1, bus2)?;
// Neighbor nodes
let neighbors = topology.get_neighbors(bus_id);
// Upstream sources
let sources = topology.upstream_sources(bus_id);
// Downstream loads
let loads = topology.downstream_loads(bus_id);
// Supply scope
let scope = topology.supply_scope(bus_id);
TopologyOps API Overview
| Method | Input | Output | Time Complexity |
|---|---|---|---|
| find_islands | - | Vec | O(V+E) |
| is_connected | bus1, bus2 | bool | O(V+E) |
| shortest_path | from, to | Vec | O((V+E) log V) |
| get_neighbors | bus_id | Vec | O(deg) |
| upstream_sources | bus_id | Vec | O(V+E) |
| downstream_loads | bus_id | Vec | O(V+E) |
| supply_scope | bus_id | Scope | O(V+E) |
| find_loops | - | Vec<Vec | O(V+E) |
| cut_set | bus_id | Vec | O(V+E) |
| radial_check | - | RadialReport | O(V+E) |
Topology Changes and Events
Any topology change triggers a broadcast on the kernel event bus; subscribers receive it via subscribe:
use eneros_topology::event::{TopologyChanged, BranchStatusChanged, BusAdded};
// Subscribe to topology changes
ctx.subscribe::<TopologyChanged>().await?;
ctx.subscribe::<BranchStatusChanged>().await?;
// Incremental topology update
network.update_branch(1, 2, |b| b.status = BranchStatus::Open);
// The kernel automatically:
// 1. Invalidates the load flow cache
// 2. Recomputes the electrical island index
// 3. Pushes a BranchStatusChanged event
// 4. Pushes a TopologyChanged event
// 5. Triggers constraint recomputation
// 6. Notifies the digital twin to sync its mirror
Event Types
| Event | Triggered When | Carried Data |
|---|---|---|
| BusAdded | A bus is added | bus_id, voltage |
| BusRemoved | A bus is removed | bus_id |
| BranchStatusChanged | A branch opens/closes | branch_id, old, new |
| TopologyChanged | Any topology change | change summary, affected_buses |
| IslandFormed | A new electrical island forms | island_id, buses |
| IslandMerged | Electrical islands merge | island_a, island_b |
Complete Event Subscription Example
use eneros_topology::event::TopologyChanged;
impl Agent for TopologyListenerAgent {
async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
let mut rx = ctx.subscribe::<TopologyChanged>().await?;
while let Some(event) = rx.recv().await {
println!("Topology change: {} buses affected", event.affected_buses.len());
self.recompute_dispatch(ctx).await?;
}
Ok(())
}
}
Integration with Agents
Agents access topology directly through the kernel API, with no extra modeling required. This is the key distinction between a Power-Native kernel and a general-purpose AgentOS—Agents run on top of the physical world model of the grid from the moment they are born.
use eneros_agent::{Agent, AgentContext, AgentResult};
use eneros_topology::BusType;
struct TopologyAwareAgent {
interval: std::time::Duration,
}
impl Agent for TopologyAwareAgent {
fn name(&self) -> &str { "topology_aware" }
async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
loop {
// Get the full topology
let network = ctx.get_network().await?;
// Analyze the topology
let islands = network.topology().find_islands();
// Make decisions based on topology
for island in &islands {
if island.buses.len() > 1 {
self.optimize_island(island, ctx).await?;
} else {
self.handle_isolated(island, ctx).await?;
}
}
ctx.sleep(self.interval).await;
}
}
}
impl TopologyAwareAgent {
async fn optimize_island(
&mut self,
island: &eneros_topology::Island,
ctx: &mut AgentContext,
) -> AgentResult<()> {
let network = ctx.get_network().await?;
let total_load: f64 = island.buses.iter()
.filter_map(|&b| network.bus_load(b))
.sum();
let total_gen: f64 = island.buses.iter()
.filter_map(|&b| network.bus_generation(b))
.sum();
let balance = total_gen - total_load;
println!("Island {} balance: {:.2} MW", island.id, balance);
Ok(())
}
async fn handle_isolated(
&mut self,
island: &eneros_topology::Island,
ctx: &mut AgentContext,
) -> AgentResult<()> {
println!("Island detected: {:?}", island.buses);
ctx.publish(eneros_topology::event::IslandFormed {
island_id: island.id,
buses: island.buses.clone(),
}).await?;
Ok(())
}
}
Interaction with Load Flow
Topology changes automatically invalidate the load flow cache and push a TopologyChanged event to all subscribers via the event bus, ensuring state consistency.
use eneros_powerflow::{PowerFlow, NewtonRaphson};
// Listen for topology changes; load flow is recomputed automatically
ctx.subscribe::<TopologyChanged>().await?;
let mut pf = NewtonRaphson::new()
.tolerance(1e-8)
.max_iter(50);
// After a topology change, the load flow cache is invalidated automatically;
// the next call recomputes it automatically
let result = pf.solve(&network)?;
println!("Converged iterations: {}", result.iterations);
println!("Max mismatch: {:.2e}", result.max_mismatch);
Persistence and Versioning
Topology objects can be persisted to kernel-managed object storage, with full change history retained:
// Save the current topology
network.save("case_2026_summer").await?;
// Load a historical version
let historical = NetworkGraph::load("case_2025_winter").await?;
// View change history
let history = network.version_log();
for entry in history.iter().rev().take(10) {
println!("{}: {} by {}", entry.timestamp, entry.action, entry.author);
}
// Roll back to a specified version
network.rollback_to("v2025.12.01-1200").await?;
Performance Metrics
| Operation | Latency | Throughput |
|---|---|---|
| Add bus/branch | < 1μs | - |
| Remove bus/branch | < 2μs | - |
| Topology analysis (1000 nodes) | < 1ms | - |
| Electrical island detection (1000 nodes) | < 5ms | - |
| Electrical island detection (100k nodes) | < 80ms | - |
| Shortest path (1000 nodes) | < 100μs | - |
| Connectivity check | < 50μs | - |
| Topology change event broadcast | < 10μs | 100k events/s |
| Topology serialization (CIM XML) | - | 10k buses/ms |
| Topology snapshot write | < 5ms | - |
Test environment: 4 cores / 8GB / Ubuntu 22.04, node count 1000 (IEEE 118 + extensions).
Configuration Parameters
Topology-related configuration in eneros.toml:
[topology]
# Topology cache size (number of topology snapshots)
cache_size = 8
# Whether to enable incremental electrical island indexing
incremental_island_index = true
# Upper limit of subscribers for topology change events
max_subscribers = 1024
# Serialization format: cim_xml / cim_json / cgmes / binary
default_format = "binary"
# Whether to enable audit logging for topology changes
audit_log = true
# Auto-save interval for topology snapshots (seconds)
snapshot_interval_secs = 300
| Parameter | Type | Default | Description |
|---|---|---|---|
| cache_size | u32 | 8 | Number of cached topology snapshots |
| incremental_island_index | bool | true | Enable incremental electrical island index to speed up recomputation after changes |
| max_subscribers | u32 | 1024 | Maximum number of subscribers per topology event |
| default_format | enum | binary | Default serialization format |
| audit_log | bool | true | Whether to log topology change audit logs |
| snapshot_interval_secs | u32 | 300 | Auto-snapshot interval |
Relationship with Other Capabilities
| Related Capability | Interaction |
|---|---|
| Physical Constraint Decision | Topology changes trigger constraint recomputation |
| Equipment Model Library | Buses/branches reference equipment model parameters |
| Time-Series Native Operations | Topology change events are written to time-series storage |
| Digital Twin Engine | Twin mirrors are built on topology |
| Safety Guard | Operation ticket validation depends on topology connectivity |
| Multi-Agent Collaboration | Agents locate their jurisdiction via topology |
| Real-Time Dual Execution Domain | Topology changes are synced to the real-time domain in real time |
| Multi-tenant and Isolation | Topology is isolated per tenant namespace |
Related Documentation
- Physical Constraint Decision — Topology changes trigger constraint recomputation
- Equipment Model Library — Equipment parameters of buses and branches
- Digital Twin Engine — Real-time mirroring based on topology
- Time-Series Native Operations — Topology event ingestion
- EnerOS Introduction — Power-Native First design philosophy