Skip to content

Kafka Digital Twin (Standard Declarative API)

This example demonstrates how to integrate dynamic-des into a full event-driven architecture using the declarative Standard API (SimulationContext).

By replacing the local connectors with KafkaIngress and KafkaEgress, the simulation becomes a fully detached microservice. It listens for external JSON commands to mutate its state, and streams telemetry and strictly-typed Pydantic events to outbound topics.


Code

This script connects the simulation to Kafka topics and utilizes Pydantic models for structured event logging.

import logging
import time
from pydantic import BaseModel
from dynamic_des import SimulationContext, KafkaAdminConnector, KafkaEgress, KafkaIngress

logging.basicConfig(level=logging.INFO)

# 1. Define Strongly-Typed Event Payloads
class TaskEvent(BaseModel):
    """
    Thanks to duck-typing, we can pass this Pydantic model directly into
    our event-decorated tasks. The KafkaEgress layer handles the extraction!
    """
    path_id: str
    status: str

def run():
    BOOTSTRAP_SERVERS = "localhost:9092"
    sim_id = "Line_A"

    # 2. Bootstrap Kafka Topics
    admin_connector = KafkaAdminConnector(bootstrap_servers=BOOTSTRAP_SERVERS)
    admin_connector.create_topics([
        {"name": "sim-config"}, {"name": "sim-telemetry"}, {"name": "sim-events"}
    ])
    time.sleep(2)

    # 3. Setup Environment with Kafka Connectors
    app = (
        SimulationContext(sim_id=sim_id, factor=1.0, random_seed=42)
        .add_resource("lathe", current_cap=1, max_cap=10)
        .add_arrival("standard", dist="exponential", rate=1.0)
        .add_service("milling", dist="normal", mean=3.0, std=0.5)
        .add_ingress(KafkaIngress(topic="sim-config", bootstrap_servers=BOOTSTRAP_SERVERS))
        .add_egress(KafkaEgress(
            telemetry_topic="sim-telemetry",
            event_topic="sim-events",
            bootstrap_servers=BOOTSTRAP_SERVERS,
        ))
    )

    # 4. Define Simulation Logic using Decorators
    @app.arrival_loop("standard")
    def arrival_process(context: SimulationContext):
        task_id = 0
        while True:
            yield context.wait_for_arrival("standard")
            context.spawn(work_task(task_id))
            task_id += 1

    @app.task(service_id="milling", resource_id="lathe")
    def work_task(task_id: int):
        # We can return a Pydantic model directly!
        return TaskEvent(path_id="Line_A.service.milling", status="finished")

    @app.telemetry_loop(interval=2.0)
    def telemetry_monitor(context: SimulationContext):
        res = context.get_resource("lathe")
        context.env.publish_telemetry("Line_A.lathe.capacity", res.capacity)
        context.env.publish_telemetry("Line_A.lathe.queue_length", len(res.queue.items))

    # 5. Run the Simulation
    print("Simulation started. Listening to Kafka...")
    try:
        app.run()
    except KeyboardInterrupt:
        pass

if __name__ == "__main__":
    run()