Skip to main content

Grid Topology First-Class Citizen

Capabilities

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

FieldTypeMeaningDefault
idu32Bus number, globally uniqueRequired
bus_typeBusTypePQ / PV / Slack / IsolatedPQ
voltageVoltageVoltage magnitude and phase angle (p.u.)(0.0, 0.0)
areau16Area number it belongs to0
zoneu16Zone number it belongs to0
base_kvf64Base voltage (kV)110.0
v_maxf64Maximum operating voltage (p.u.)1.10
v_minf64Minimum operating voltage (p.u.)0.90
nameStringHuman-readable name""

Branch Field Reference

FieldTypeMeaningDefault
fromu32Source bus IDRequired
tou32Destination bus IDRequired
paramsBranchParamsResistance, reactance, shunt admittance, turns ratioRequired
statusBranchStatusClosed / Open / GroundedClosed
rating_mvaf64Rated capacity (MVA)100.0
emergency_mvaf6415-minute emergency capacityrating_mva * 1.2
length_kmf64Line 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

MethodInputOutputTime Complexity
find_islands-VecO(V+E)
is_connectedbus1, bus2boolO(V+E)
shortest_pathfrom, toVecO((V+E) log V)
get_neighborsbus_idVecO(deg)
upstream_sourcesbus_idVecO(V+E)
downstream_loadsbus_idVecO(V+E)
supply_scopebus_idScopeO(V+E)
find_loops-Vec<Vec>O(V+E)
cut_setbus_idVecO(V+E)
radial_check-RadialReportO(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

EventTriggered WhenCarried Data
BusAddedA bus is addedbus_id, voltage
BusRemovedA bus is removedbus_id
BranchStatusChangedA branch opens/closesbranch_id, old, new
TopologyChangedAny topology changechange summary, affected_buses
IslandFormedA new electrical island formsisland_id, buses
IslandMergedElectrical islands mergeisland_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

OperationLatencyThroughput
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μs100k 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
ParameterTypeDefaultDescription
cache_sizeu328Number of cached topology snapshots
incremental_island_indexbooltrueEnable incremental electrical island index to speed up recomputation after changes
max_subscribersu321024Maximum number of subscribers per topology event
default_formatenumbinaryDefault serialization format
audit_logbooltrueWhether to log topology change audit logs
snapshot_interval_secsu32300Auto-snapshot interval

Relationship with Other Capabilities

Related CapabilityInteraction
Physical Constraint DecisionTopology changes trigger constraint recomputation
Equipment Model LibraryBuses/branches reference equipment model parameters
Time-Series Native OperationsTopology change events are written to time-series storage
Digital Twin EngineTwin mirrors are built on topology
Safety GuardOperation ticket validation depends on topology connectivity
Multi-Agent CollaborationAgents locate their jurisdiction via topology
Real-Time Dual Execution DomainTopology changes are synced to the real-time domain in real time
Multi-tenant and IsolationTopology is isolated per tenant namespace