Skip to content

Digital Twin Pattern

SPO can be evaluated inside digital-twin architectures as a coherence-analysis and control-proposal layer. It can monitor configured synchronisation metrics and emit reviewable proposals when a simulated or measured system leaves its declared regime; it does not directly actuate the physical system.

Architecture

Physical System ──► Sensors ──► Phase Extraction (P/I/S)
                              ┌── SPO Engine ──┐
                              │  UPDE dynamics  │
                              │  15+ monitors   │
                              │  Regime FSM     │
                              │  Supervisor     │
                              └───────┬─────────┘
                              ┌───────┴─────────┐
                              │   Actuation      │
                              │   (K, α, ζ, Ψ)  │
                              └───────┬─────────┘
Physical System ◄── Actuators ◄── Control Commands

In a candidate integration, the digital twin runs SPO in parallel with the physical system. Sensor data feeds caller-owned oscillator extractors; proposed commands pass through the actuation mapper and remain behind an external approval and execution boundary.

Binding Contract

Before a simulator, service twin, or hardware twin exchanges data with SPO, export the binding as a versioned contract:

from scpn_phase_orchestrator.binding import (
    build_digital_twin_binding_contract,
    load_binding_spec,
)

spec = load_binding_spec("domainpacks/digital_twin_nchannel/binding_spec.yaml")
contract = build_digital_twin_binding_contract(spec)
contract_payload = contract.to_audit_record()

The contract records binding name/version, timing, layer oscillator IDs, actuator limits, N-channel algebra, default sync capabilities, and a stable contract_hash. It is a compatibility artefact: consumers can compare hashes and capability names before accepting telemetry or proposed actions. It does not open a network connection or apply control.

Adapters should validate every transport payload against that contract before runtime use:

from scpn_phase_orchestrator.binding import (
    build_digital_twin_sync_envelope,
    validate_digital_twin_sync_envelope,
)

envelope = build_digital_twin_sync_envelope(
    contract,
    capability="state_snapshot",
    direction="twin_to_spo",
    sequence=1,
    payload={"layer": "machine_cells", "R": 0.91},
)
validation = validate_digital_twin_sync_envelope(contract, envelope)
if not validation.accepted:
    raise ValueError(validation.reason)

This gives REST, gRPC, Kafka, file, and hardware adapters the same compatibility gate without coupling the binding layer to any specific transport.

For deterministic file replay, use the JSONL adapter:

from scpn_phase_orchestrator.binding import (
    read_digital_twin_sync_jsonl,
    write_digital_twin_sync_jsonl,
)

write_digital_twin_sync_jsonl("sync.jsonl", [envelope])
replay_report = read_digital_twin_sync_jsonl(contract, "sync.jsonl")

The replay report separates accepted validations from malformed JSON, invalid envelope shapes, and contract-validation rejections. This is useful for adapter smoke tests and audit replay before introducing a live network transport.

The same accepted/rejected validation records can be reduced to a stable operator evidence payload:

from scpn_phase_orchestrator.binding import build_digital_twin_operator_evidence

evidence = build_digital_twin_operator_evidence(
    contract,
    replay_report.accepted,
    rejected=replay_report.rejected,
)

The evidence record reports accepted/rejected counts, capability and direction counts, latest sequence, maximum absolute twin residual, mismatch reasons, adapter health, and an operator status (healthy, warning, degraded, or critical). Live REST/gRPC/Kafka/hardware adapters and replayed JSONL files therefore expose the same dashboard fields.

For runtime-facing tests that should not touch disk, use the in-memory adapter:

from scpn_phase_orchestrator.binding import DigitalTwinSyncMemoryAdapter

adapter = DigitalTwinSyncMemoryAdapter.for_contract(contract)
validation = adapter.submit(envelope)
accepted_batch = adapter.drain()

The adapter queues accepted envelopes in order and returns rejection reasons for invalid submissions. For HTTP integrations, use the dependency-free REST boundary adapter inside a framework route:

from scpn_phase_orchestrator.binding import DigitalTwinSyncRestAdapter

adapter = DigitalTwinSyncRestAdapter.for_contract(
    contract,
    sync_capabilities=("state_snapshot", "audit_replay"),
)
response = adapter.handle_post(
    envelope.to_audit_record(),
    headers={"authorization": "Bearer ..."},
)
accepted_batch = adapter.drain()

This helper does not start a web server. It enforces the adapter manifest, authentication posture, JSON envelope shape, contract hash, declared capability, direction, and non-empty payload before a FastAPI, Flask, or gateway endpoint hands data to SPO runtime code.

For gRPC integrations, use the same boundary pattern after decoding protobuf fields inside a servicer:

from scpn_phase_orchestrator.binding import DigitalTwinSyncGrpcAdapter

adapter = DigitalTwinSyncGrpcAdapter.for_contract(contract)
response = adapter.handle_unary(
    envelope.to_audit_record(),
    metadata={"authorization": "Bearer ..."},
)
accepted_batch = adapter.drain()

The gRPC helper returns status names such as OK, UNAUTHENTICATED, INVALID_ARGUMENT, and FAILED_PRECONDITION so a real servicer can map the result to framework-native status handling without duplicating contract logic.

For Kafka or compatible message buses, pass decoded message records through the broker-neutral boundary:

from scpn_phase_orchestrator.binding import DigitalTwinSyncKafkaAdapter

adapter = DigitalTwinSyncKafkaAdapter.for_contract(
    contract,
    topic="spo.digital_twin.sync",
)
response = adapter.handle_message(
    {"topic": "spo.digital_twin.sync", "value": envelope.to_audit_record()},
    headers={"authorization": "Bearer ..."},
)
accepted_batch = adapter.drain()

The Kafka helper does not import a broker client, commit offsets, or open network connections. It checks the topic, auth header, decoded value shape, contract, direction, and payload before caller-controlled offset handling.

For hardware integrations, validate decoded device frames before any driver or actuator layer can see them:

from scpn_phase_orchestrator.binding import DigitalTwinSyncHardwareAdapter

adapter = DigitalTwinSyncHardwareAdapter.for_contract(
    contract,
    device_ids=("pynq-loopback-0",),
)
response = adapter.handle_frame(
    {
        "device_id": "pynq-loopback-0",
        "safety_interlock": True,
        "value": envelope.to_audit_record(),
    },
    headers={"authorization": "Bearer ..."},
)
accepted_batch = adapter.drain()

The hardware helper never opens device files, writes registers, toggles GPIO, or applies actuation. It checks the registered device ID, explicit safety interlock, auth header, decoded frame value, contract, direction, and payload; hardware_write_permitted is always False in its response and audit record.

Before enabling a concrete adapter, publish a reviewable manifest:

from scpn_phase_orchestrator.binding import build_digital_twin_adapter_manifest

compatibility = build_digital_twin_adapter_manifest(
    contract,
    name="grpc-live",
    transport="grpc",
    sync_capabilities=("state_snapshot", "audit_replay"),
    supports_replay=True,
    requires_auth=True,
)

The manifest check rejects undeclared capabilities, live transports without authentication, and offline transports that cannot replay payloads.

Implementation

from scpn_phase_orchestrator.binding.loader import load_binding_spec
from scpn_phase_orchestrator.runtime.server import SimulationState

# 1. Load domain binding
spec = load_binding_spec("domainpacks/power_grid/binding_spec.yaml")
twin = SimulationState(spec)

# 2. Feed sensor data each tick
while True:
    sensor_data = read_sensors()       # your data source
    state = twin.step()                # advance twin
    R = state["R_global"]
    regime = state["regime"]

    # 3. Act on supervisor decisions
    if regime == "critical":
        increase_coupling()            # your actuator
    elif regime == "degraded":
        log_warning(R)

    # 4. Audit trail for compliance
    audit_logger.log_step(state)

Candidate deployment surfaces

Mode Implemented surface Evidence required before deployment
Embedded (Python library) in-process simulation and proposal API target-host timing, memory, failure-mode, and actuation-boundary tests
REST API (FastAPI server) state, metrics, review, and bounded mutation routes authentication, load, latency, rate-limit, and network-policy evidence
gRPC service servicer and message contracts owned server lifecycle, transport security, load, and jitter evidence
WASM (browser) standalone educational Kuramoto stepper browser compatibility and workload-specific timing evidence
Container Docker/Helm artefacts image provenance, vulnerability review, health, rollback, and capacity evidence

No portable latency is claimed for these surfaces. Measure the complete deployment, including ingestion, scheduling, network, audit, and downstream approval work.

Candidate domain studies

  • Tokamak plasma: evaluate whether MHD-mode phase features add reviewed evidence; no disruption or precursor claim is established.
  • Power grid: compare swing-equation simulations and modal estimates with trusted grid tools; no general cascade-prediction claim is established.
  • Manufacturing: test whether phase relationships add value beyond SPC baselines before proposing any process adjustment.
  • Traffic: simulate signal-timing policies before hardware-in-loop and field safety evaluation.

Each candidate follows the same review path: extract phases → run the model → audit metrics → evaluate a bounded proposal. External actuation is a separate, unauthorised step until domain evidence and operator approval exist.