Digital Twin Engine
The EnerOS Digital Twin Engine maintains a real-time mirror of the grid in memory, supporting capabilities such as What-If simulation, historical replay, and virtual sensors. With end-to-end latency < 100ms, it enables Agents to “simulate first, then execute,” minimizing decision risk.
The digital twin is the “sandbox” of the Power-Native kernel: it simulates the effect of decisions on the twin mirror, validates constraints and stability, and then deploys to the real grid. This gives Agents a “trial-and-error space,” avoiding the risks of making decisions directly on the real system.
Architecture Overview
┌─────────────────────────────────────────────────────┐
│ Agent: Simulate first, then execute │
└──────────────────┬──────────────────────────────────┘
│ what_if / replay / fork
┌──────────────────┴──────────────────────────────────┐
│ eneros-twin (Digital Twin Engine) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Mirror │ │ WhatIf │ │ Replay │ │
│ │ Mirror │ │ Sim │ │ History │ │
│ │ maintain │ │ Engine │ │ replay │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ VSensor │ │ Calibrate│ │ Compare │ │
│ │ Virtual │ │ Model │ │ Multi- │ │
│ │ sensor │ │ calibrate│ │ twin cmp │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────┬──────────────────────────────────┘
│ Subscribe to kernel events
┌──────────────────┴──────────────────────────────────┐
│ Kernel: Topology / Equipment / Time-series / Measurement │
└─────────────────────────────────────────────────────┘
Mirror Maintenance
The twin engine subscribes to all topology change, equipment status, and measurement data events, updating the mirror in real time:
use eneros_twin::{DigitalTwin, TwinConfig, Fidelity};
use std::time::Duration;
let mut twin = DigitalTwin::new(TwinConfig {
sync_interval: Duration::from_millis(50),
history_window: Duration::from_secs(3600),
fidelity: Fidelity::Detailed,
max_forks: 8,
auto_calibrate: true,
});
// Automatically subscribe to kernel events and sync
twin.start(&kernel).await?;
// Query mirror status
let status = twin.status();
println!("Mirror lag: {:.1}ms", status.lag_ms);
println!("History window: {:.0}s", status.history_window.as_secs());
println!("Active forks: {}", status.active_forks);
TwinConfig Fields
| Field | Type | Default | Description |
|---|---|---|---|
| sync_interval | Duration | 50ms | Sync interval |
| history_window | Duration | 3600s | History window length |
| fidelity | Fidelity | Detailed | Model fidelity |
| max_forks | u32 | 8 | Maximum number of forks |
| auto_calibrate | bool | true | Auto model calibration |
| retention_hours | u32 | 24 | Mirror retention period |
Fidelity Variants
| Variant | Fidelity | Performance | Applicable Scenario |
|---|---|---|---|
| Detailed | Full model | Slow | Protection validation |
| Standard | Standard model | Medium | Dispatch decisions |
| Coarse | Simplified model | Fast | Real-time planning |
| Auto | Adaptive | Adaptive | General scenarios |
What-If Simulation
Simulates the execution effect of decisions without affecting the real grid:
use eneros_twin::{WhatIf, Scenario, Event, Mode};
use std::time::Duration;
let scenario = Scenario::new()
.event(Event::OpenBranch(branch_id))
.event(Event::IncreaseLoad(bus_id, 50.0))
.event(Event::SetGenerator(gen_id, 80.0))
.duration(Duration::from_secs(60));
let result = twin.what_if(scenario).await?;
// Analyze simulation result
println!("Number of constraint violations: {}", result.violations.len());
for violation in &result.violations {
println!(" - {:?}: {}", violation.kind, violation.message);
}
println!("Max voltage deviation: {:.2}%", result.max_voltage_deviation * 100.0);
println!("Max frequency deviation: {:.2} Hz", result.max_frequency_deviation);
println!("Converged: {}", result.converged);
WhatIfResult Fields
| Field | Type | Description |
|---|---|---|
| converged | bool | Whether it converged |
| violations | Vec | List of constraint violations |
| max_voltage_deviation | f64 | Max voltage deviation (p.u.) |
| max_frequency_deviation | f64 | Max frequency deviation (Hz) |
| max_thermal_loading | f64 | Max thermal loading rate |
| stability_margin | f64 | Transient stability margin |
| duration | Duration | Simulation duration |
| snapshots | Vec | Snapshots at key moments |
Simulation Strategies
Supports both steady-state and transient simulation modes:
// Steady-state: fast, suitable for dispatch decisions
let steady = twin.what_if(scenario.clone())
.mode(Mode::SteadyState)
.await?;
// Transient: precise, suitable for protection validation
let transient = twin.what_if(scenario)
.mode(Mode::Transient(Duration::from_secs(10)))
.step(Duration::from_millis(1)) // Simulation step
.await?;
// Probabilistic simulation: considers uncertainty
let monte_carlo = twin.what_if(scenario)
.mode(Mode::MonteCarlo { samples: 100 })
.await?;
Simulation Mode Comparison
| Mode | Fidelity | Duration | Applicable Scenario |
|---|---|---|---|
| SteadyState | Steady-state | < 100ms | Dispatch decisions |
| Transient(dur) | Transient detailed | < 2s | Protection validation |
| MonteCarlo(n) | Probability distribution | n × 100ms | Risk assessment |
| QuasiDynamic | Quasi-dynamic | < 500ms | Medium- and long-term dynamics |
| LongTerm | Long-term dynamics | < 5s | Day-ahead dispatch simulation |
Historical Replay
Replays the grid state at any point in time, supporting event-level precise reproduction:
use eneros_twin::{Replay, Range};
let mut replay = twin.replay()
.range(Range::between(t_start, t_end))
.speed(10.0) // 10x speed
.events(true) // Include events
.start().await?;
while let Some(snapshot) = replay.next().await? {
// Run analysis on the snapshot
let result = analyze(&snapshot)?;
println!("{}: V_max = {:.4}", snapshot.timestamp, result.max_voltage);
}
// Jump to a specific time
replay.seek_to(specific_time).await?;
// Pause/resume
replay.pause().await?;
replay.resume().await?;
ReplayConfig Fields
| Field | Type | Default | Description |
|---|---|---|---|
| range | Range | - | Replay interval |
| speed | f64 | 1.0 | Replay speed |
| events | bool | false | Include events |
| measurements | bool | true | Include measurement data |
| step | Duration | 1s | Snapshot interval |
Virtual Sensors
Deploy virtual measurement points in the twin mirror to cover locations where real sensors are not installed:
use eneros_twin::VirtualSensor;
use std::time::Duration;
let sensor = VirtualSensor::new("virtual.bus_5.voltage")
.calculated_by(|network| {
// Estimated based on state estimation
network.estimate_voltage(bus_5)
})
.update_interval(Duration::from_millis(100))
.quality(Quality::Calculated);
twin.deploy_sensor(sensor).await?;
// Query like a real measurement point
let v = timeseries.query("virtual.bus_5.voltage").await?;
// Deploy multiple virtual sensors
let sensors = vec![
VirtualSensor::new("virtual.bus_5.voltage")
.calculated_by(|n| n.estimate_voltage(5)),
VirtualSensor::new("virtual.branch_3.flow")
.calculated_by(|n| n.estimate_flow(3)),
VirtualSensor::new("virtual.gen_2.thermal")
.calculated_by(|n| n.estimate_gen_temp(2)),
];
twin.deploy_sensors(sensors).await?;
Virtual Sensor Types
| Type | Calculation Method | Use |
|---|---|---|
| State estimation | Estimated based on load flow results | Cover blind spots |
| Interpolation | Based on neighboring measurement points | Fill missing data |
| Physical model | Based on equipment models | Non-directly measurable quantities |
| Machine learning | Based on historical training | Soft measurement |
| Composite | Multi-source fusion | High reliability |
Model Calibration
The twin engine continuously compares real measurements with simulation results and automatically calibrates model parameters:
use eneros_twin::{CalibrationTarget, CalibrationMethod};
let calibration = twin.calibrate()
.target(CalibrationTarget::LineImpedance(branch_id))
.method(CalibrationMethod::LeastSquares)
.window(Duration::from_secs(3600))
.tolerance(1e-4)
.run().await?;
println!("Before calibration: z={:.4}, after: z={:.4}",
calibration.before, calibration.after);
println!("Residual: {:.2e}", calibration.residual);
println!("Confidence: {:.1}%", calibration.confidence * 100.0);
// Batch calibration
let targets = vec![
CalibrationTarget::LineImpedance(branch_1),
CalibrationTarget::TransformerTap(xfmr_2),
CalibrationTarget::LoadModel(bus_3),
];
let report = twin.calibrate_batch(targets).await?;
CalibrationTarget Types
| Variant | Calibration Object | Data Source |
|---|---|---|
| LineImpedance(id) | Line impedance | SCADA + PMU |
| TransformerTap(id) | Transformer ratio | Voltages at both ends |
| LoadModel(bus) | Load model parameters | Load characteristics |
| GenParameter(id) | Generator parameters | Excitation response |
| SensorOffset(id) | Sensor zero drift | Comparison with standard source |
Multiple Twin Instances
Supports running multiple twin instances in parallel to compare different strategies:
// Fork multiple twin instances
let twin_a = twin.fork("strategy_a").await?;
let twin_b = twin.fork("strategy_b").await?;
let twin_c = twin.fork("strategy_c").await?;
// Simulate on three twins respectively
let r_a = twin_a.what_if(scenario_a).await?;
let r_b = twin_b.what_if(scenario_b).await?;
let r_c = twin_c.what_if(scenario_c).await?;
// Compare results
let comparison = twin.compare(&[&r_a, &r_b, &r_c]).await?;
println!("Plan | Violations | Max Voltage Deviation | Stability Margin");
for (i, r) in comparison.results.iter().enumerate() {
println!(" {} | {} | {:.2}% | {:.2}%",
i, r.violations.len(),
r.max_voltage_deviation * 100.0,
r.stability_margin * 100.0);
}
// Select the optimal plan
let best = comparison.best();
println!("Optimal plan: {}", best.label);
// Release forks
twin_a.drop().await?;
twin_b.drop().await?;
twin_c.drop().await?;
Integration with Agents
Agents call the twin engine to “simulate first, then execute”:
use eneros_twin::Scenario;
impl Agent for CautiousDispatchAgent {
async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
loop {
let state = ctx.snapshot().await?;
let decision = self.compute_dispatch(&state)?;
// Simulate on the twin first
let scenario = Scenario::from_decision(&decision);
let result = ctx.twin().what_if(scenario).await?;
if result.violations.is_empty() && result.stability_margin > 0.1 {
// Safe, execute
ctx.execute(decision).await?;
} else {
// Unsafe, re-plan
let adjusted = self.adjust_decision(decision, &result)?;
ctx.execute(adjusted).await?;
}
ctx.sleep(self.interval).await;
}
}
}
Performance Metrics
| Operation | Latency | Note |
|---|---|---|
| Mirror sync lag | < 50ms | 50ms cycle |
| What-If steady-state simulation | < 100ms | 1000 nodes |
| What-If transient simulation (10s) | < 2s | 1ms step |
| What-If MonteCarlo (100 samples) | < 10s | Parallel |
| Historical replay (1 hour) | < 5s | 1s snapshot |
| Virtual sensor update | < 10ms | Single sensor |
| Model calibration (1 hour window) | < 30s | Single parameter |
| Multi-twin comparison (3 instances) | < 500ms | - |
| Fork creation | < 50ms | - |
| Fork release | < 5ms | - |
Test environment: 4 cores / 8GB / Ubuntu 22.04, node count 1000.
Application Scenarios
| Scenario | Mode | Value |
|---|---|---|
| Dispatch decision validation | What-If steady-state | Avoid decision accidents |
| Protection setting verification | What-If transient | Validate protection coordination |
| Incident review | Historical replay | Root cause analysis |
| Planning evaluation | Multi-twin comparison | Plan optimization |
| Equipment health | Model calibration | Early warning |
| State completion | Virtual sensors | Cover blind spots |
| Risk assessment | MonteCarlo | Probabilistic analysis |
| Operation training | Historical replay + What-If | Immersive training |
Configuration Parameters
Twin-related configuration in eneros.toml:
[twin]
# Sync interval (ms)
sync_interval_ms = 50
# History window (s)
history_window_secs = 3600
# Default model fidelity: detailed / standard / coarse / auto
default_fidelity = "auto"
# Maximum number of forks
max_forks = 8
# Whether to enable auto calibration
auto_calibrate = true
# Calibration interval (s)
calibrate_interval_secs = 3600
# Mirror retention period (hours)
retention_hours = 24
# Whether to enable virtual sensors
enable_virtual_sensors = true
| Parameter | Type | Default | Description |
|---|---|---|---|
| sync_interval_ms | u32 | 50 | Sync interval |
| history_window_secs | u32 | 3600 | History window |
| default_fidelity | enum | auto | Default model fidelity |
| max_forks | u32 | 8 | Maximum number of forks |
| auto_calibrate | bool | true | Auto calibration |
| calibrate_interval_secs | u32 | 3600 | Calibration interval |
| retention_hours | u32 | 24 | Mirror retention period |
| enable_virtual_sensors | bool | true | Enable virtual sensors |
Relationship with Other Capabilities
| Related Capability | Interaction |
|---|---|
| Grid Topology First-Class Citizen | The twin is built on topology |
| Physical Constraint Decision | What-If calls constraint validation |
| Time-Series Native Operations | Replay depends on time-series data |
| Equipment Model Library | The twin uses equipment models for simulation |
| Multi-Agent Collaboration | Agents call the twin to simulate first |
| Real-Time Dual Execution Domain | Syncs mirror with the real-time domain |
| Safety Guard | Dangerous decisions are validated by the twin first |
Related Documentation
- Grid Topology First-Class Citizen — The twin is built on topology
- Physical Constraint Decision — What-If calls constraint validation
- Time-Series Native Operations — Replay depends on time-series data
- Equipment Model Library — Equipment models drive simulation
- EnerOS Introduction — Power-Native First design philosophy