Skip to main content

Digital Twin Engine

Capabilities

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

FieldTypeDefaultDescription
sync_intervalDuration50msSync interval
history_windowDuration3600sHistory window length
fidelityFidelityDetailedModel fidelity
max_forksu328Maximum number of forks
auto_calibratebooltrueAuto model calibration
retention_hoursu3224Mirror retention period

Fidelity Variants

VariantFidelityPerformanceApplicable Scenario
DetailedFull modelSlowProtection validation
StandardStandard modelMediumDispatch decisions
CoarseSimplified modelFastReal-time planning
AutoAdaptiveAdaptiveGeneral 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

FieldTypeDescription
convergedboolWhether it converged
violationsVecList of constraint violations
max_voltage_deviationf64Max voltage deviation (p.u.)
max_frequency_deviationf64Max frequency deviation (Hz)
max_thermal_loadingf64Max thermal loading rate
stability_marginf64Transient stability margin
durationDurationSimulation duration
snapshotsVecSnapshots 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

ModeFidelityDurationApplicable Scenario
SteadyStateSteady-state< 100msDispatch decisions
Transient(dur)Transient detailed< 2sProtection validation
MonteCarlo(n)Probability distributionn × 100msRisk assessment
QuasiDynamicQuasi-dynamic< 500msMedium- and long-term dynamics
LongTermLong-term dynamics< 5sDay-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

FieldTypeDefaultDescription
rangeRange-Replay interval
speedf641.0Replay speed
eventsboolfalseInclude events
measurementsbooltrueInclude measurement data
stepDuration1sSnapshot 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

TypeCalculation MethodUse
State estimationEstimated based on load flow resultsCover blind spots
InterpolationBased on neighboring measurement pointsFill missing data
Physical modelBased on equipment modelsNon-directly measurable quantities
Machine learningBased on historical trainingSoft measurement
CompositeMulti-source fusionHigh 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

VariantCalibration ObjectData Source
LineImpedance(id)Line impedanceSCADA + PMU
TransformerTap(id)Transformer ratioVoltages at both ends
LoadModel(bus)Load model parametersLoad characteristics
GenParameter(id)Generator parametersExcitation response
SensorOffset(id)Sensor zero driftComparison 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

OperationLatencyNote
Mirror sync lag< 50ms50ms cycle
What-If steady-state simulation< 100ms1000 nodes
What-If transient simulation (10s)< 2s1ms step
What-If MonteCarlo (100 samples)< 10sParallel
Historical replay (1 hour)< 5s1s snapshot
Virtual sensor update< 10msSingle sensor
Model calibration (1 hour window)< 30sSingle 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

ScenarioModeValue
Dispatch decision validationWhat-If steady-stateAvoid decision accidents
Protection setting verificationWhat-If transientValidate protection coordination
Incident reviewHistorical replayRoot cause analysis
Planning evaluationMulti-twin comparisonPlan optimization
Equipment healthModel calibrationEarly warning
State completionVirtual sensorsCover blind spots
Risk assessmentMonteCarloProbabilistic analysis
Operation trainingHistorical replay + What-IfImmersive 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
ParameterTypeDefaultDescription
sync_interval_msu3250Sync interval
history_window_secsu323600History window
default_fidelityenumautoDefault model fidelity
max_forksu328Maximum number of forks
auto_calibratebooltrueAuto calibration
calibrate_interval_secsu323600Calibration interval
retention_hoursu3224Mirror retention period
enable_virtual_sensorsbooltrueEnable virtual sensors

Relationship with Other Capabilities

Related CapabilityInteraction
Grid Topology First-Class CitizenThe twin is built on topology
Physical Constraint DecisionWhat-If calls constraint validation
Time-Series Native OperationsReplay depends on time-series data
Equipment Model LibraryThe twin uses equipment models for simulation
Multi-Agent CollaborationAgents call the twin to simulate first
Real-Time Dual Execution DomainSyncs mirror with the real-time domain
Safety GuardDangerous decisions are validated by the twin first