Skip to main content

A DEVS(Discrete Event System Specification) Modeling & Simulation environment with journaling functionality

Project description

pyjevsim

PyPI Python Docs License: MIT

Introduction

pyjevsim is a DEVS (discrete event system specification) modeling and simulation environment with built-in journaling. It supports snapshot and restore of individual models or the full simulation engine, virtual-time and real-time execution, and HLA federate integration via a stepped execution mode. Compatible with Python 3.10+.

Full documentation: https://pyjevsim.readthedocs.io/en/latest/

What's new in 2.0

  • Two-phase tick. SysExecutor evaluates every imminent model's output() first, then routes outputs and applies transitions — fixing confluent-event ordering under Parallel-DEVS semantics.
  • HLA stepped execution. step(granted_time) and get_next_event_time() let an IEEE 1516-2010 RTI federate drive pyjevsim without owning the main loop.
  • V_TIME jump-to-next-event. The virtual-time scheduler hops directly to the next scheduled event instead of advancing by a fixed time_resolution, eliminating idle ticks on sparse models.
  • Opt-in uncaught-message tracking for debugging dangling outputs.
  • DEVStone benchmark suite with cross-engine comparison adapters.

Installing

From PyPI (recommended):

pip install pyjevsim

From source:

git clone https://github.com/eventsim/pyjevsim
cd pyjevsim
pip install -e .

Dependencies

  • Python >= 3.10
  • dill >= 0.3.6 (installed automatically) — used for model serialization and restoration.

pytest is required only to run the test suite and is declared under the dev extra:

pip install pyjevsim[dev]

Quick Start

A minimal generator → sink simulation:

from pyjevsim.behavior_model import BehaviorModel
from pyjevsim.definition import ExecutionType, Infinite
from pyjevsim.system_executor import SysExecutor
from pyjevsim.system_message import SysMessage


class Gen(BehaviorModel):
    def __init__(self, name):
        super().__init__(name)
        self.init_state("Generate")
        self.insert_state("Generate", 1)
        self.insert_output_port("out")

    def ext_trans(self, port, msg): pass
    def int_trans(self): pass
    def output(self, md):
        msg = SysMessage(self.get_name(), "out")
        msg.insert("tick")
        md.insert_message(msg)
    def time_advance(self):
        return 1


class Sink(BehaviorModel):
    def __init__(self, name):
        super().__init__(name)
        self.init_state("Idle")
        self.insert_state("Idle", Infinite)
        self.insert_input_port("in")

    def ext_trans(self, port, msg):
        print(f"received: {msg.retrieve()}")
    def int_trans(self): pass
    def output(self, md): pass
    def time_advance(self):
        return Infinite


se = SysExecutor(1, ex_mode=ExecutionType.V_TIME)
gen = Gen("g")
sink = Sink("s")
se.register_entity(gen)
se.register_entity(sink)
se.coupling_relation(gen, "out", sink, "in")
se.simulate(5)

See the quick-start guide for structural models, snapshots, and HLA stepped execution.

Examples

The examples/ directory contains:

  • banksim/ — bank queue simulation demonstrating BehaviorModel, StructuralModel, and snapshot/restore.
  • atsim/ — anti-torpedo simulator with self-propelled and stationary decoy models.
  • mwmsim/ — municipal waste management agent-based model.

Output messages are shared by reference

When a model's output port has multiple downstream subscribers, every subscriber receives the same SysMessage object. pyjevsim does not deep-copy outputs during propagation — and neither does any other major Python DEVS engine (xdevs.py and PythonPDEVS share references the same way; benchmark/aliasing_test.py empirically demonstrates this for all four engines in the comparison set). Treat received messages as immutable; if your model needs to mutate a payload, copy it on the receiver side:

def ext_trans(self, port, msg):
    payload = list(msg.retrieve())   # local copy, safe to mutate
    payload.append(my_local_data)
    ...

See benchmark/results/ALIASING.md for the full investigation and per-engine source pointers.

Benchmarks

The benchmark/ directory contains a DEVStone suite plus adapters that run the same workload against other Python DEVS engines so the pyjevsim baseline can be tracked over time.

benchmark/
├── devstone/                     # original pyjevsim-only DEVStone (flat)
│   ├── atomic.py
│   └── topology.py
├── engines/                      # cross-engine canonical DEVStone
│   ├── common.py                 # shared RunResult dataclass
│   ├── pyjevsim/                 # adapter for this repo
│   ├── xdevs/                    # adapter for xdevs.py (pip install xdevs)
│   ├── pypdevs/                  # adapter for PythonPDEVS minimal kernel
│   └── reference/                # hand-rolled flat-FEL engine (floor)
├── run_devstone.py               # pyjevsim-only runner
├── run_compare.py                # cross-engine comparison runner
└── results/
    ├── BASELINE.md               # captured baseline numbers
    ├── baseline.csv
    └── devstone_sweep.csv

pyjevsim-only sweep

python -m benchmark.run_devstone --sweep \
    --output benchmark/results/devstone_sweep.csv

Cross-engine comparison

pip install xdevs                                       # optional
python -m benchmark.run_compare --list-engines
python -m benchmark.run_compare \
    --output benchmark/results/baseline.csv

Sparse-time baseline

run_sparse runs a tiny periodic-generator-plus-sink topology while sweeping the inter-event simulated period. Holds the work constant at 100 events; only the simulated-time gap between events varies. Isolates per-tick overhead in V_TIME mode (see benchmark/results/SPARSE.md):

python -m benchmark.run_sparse --output benchmark/results/sparse.csv

Output aliasing test

benchmark/aliasing_test.py empirically demonstrates that all four engines share output value references across multiple subscribers — see benchmark/results/ALIASING.md. The prevailing convention is "treat received values as immutable; copy on the receiver if you need to mutate".

Current baseline (best-of-three, no synthetic CPU work) — see benchmark/results/BASELINE.md:

variant d × w pyjevsim tr/s xdevs tr/s pypdevs tr/s reference tr/s
LI 4 × 4 175 k 689 k 765 k 1.68 M
HI 4 × 4 233 k 546 k 888 k 2.00 M
HO 4 × 4 241 k 757 k 918 k 1.97 M

Use --int-cycles N / --ext-cycles N to inject synthetic CPU work per transition and shift the measurement toward user-code cost.

Debugging Uncaught Output Messages

By default SysExecutor drops output messages that hit a port with no downstream coupling — the simulator stays on its fast path and the events disappear silently. When wiring up a model graph it is often useful to know which events are leaking; pass track_uncaught=True and they get routed to the built-in DefaultMessageCatcher (accessible as se.dmc) so you can observe them:

se = SysExecutor(1, ex_mode=ExecutionType.V_TIME, track_uncaught=True)

The flag costs ~10-15% throughput on dense graphs with many dangling outputs (every uncoupled emit pays for one ext_trans + reschedule on the catcher), so leave it off in production runs.

Execution Modes

SysExecutor supports three execution modes via ExecutionType:

Mode Description
V_TIME Virtual time — simulation runs as fast as possible
R_TIME Real time — simulation paces itself to wall-clock time
HLA_TIME HLA/RTI-controlled time — time advancement is driven externally
from pyjevsim.system_executor import SysExecutor
from pyjevsim.definition import ExecutionType

se = SysExecutor(1, ex_mode=ExecutionType.V_TIME)

Multi-threading Support

SysExecutor provides thread-safe APIs for multi-threaded simulation environments where external threads inject events while the simulation runs.

Pause / Resume

Pause the simulation to allow external threads to accumulate events, then resume.

se.pause_sim()    # Pauses the simulation loop
# External threads can safely call insert_external_event() while paused
se.resume_sim()   # Resumes the simulation loop

External Event Injection

Insert events from external threads into the simulation. Thread-safe via internal synchronization.

se.insert_external_event("port_name", message, scheduled_time=0)

Output Event Callback

Register a callback to be notified when output events are generated, avoiding polling.

se.set_output_event_callback(lambda: print("output ready"))
events = se.handle_external_output_event()

HLA/RTI Integration (HLA_TIME Mode)

For HLA/RTI-controlled simulations, use HLA_TIME mode with step() and get_next_event_time().

se = SysExecutor(1, ex_mode=ExecutionType.HLA_TIME)
se.register_entity(model)
se.init_sim()

# RTI-driven loop
while not se.is_terminated():
    next_time = se.get_next_event_time()
    # ... request time advance from RTI, wait for grant ...
    granted_time = ...  # time granted by RTI
    output_events = se.step(granted_time)
    # ... publish output_events to RTI ...

step(granted_time)

Runs one RTI-granted simulation step using the same Parallel-DEVS four-phase tick that the standalone V_TIME path uses, so HLA federates get correct δ_int / δ_ext / δ_con semantics:

  • Every event whose req_time <= granted_time fires inside the call.
  • Multiple cascade rounds at the same simulated instant complete in one step() (sigma=0 chains do not require multiple grants).
  • During each round, global_time reflects the actual event time so models observe correct simulated time inside their transitions.
  • Per IEEE 1516-2010, global_time lands at granted_time when the call returns, even if the last processed event was earlier.
  • Returns the output_event_queue contents drained during this step (a deque of (time, message) tuples) so the federate can republish them as RTI interactions.

get_next_event_time()

Returns the earliest scheduled event time across the FEL and the external-event queue. Use it to compute the Time Advance Request value for the RTI.

Federate ambassador

pyjevsim ships the simulator-side hooks (above) but not an RTI ambassador. Wire step / get_next_event_time / insert_external_event / set_output_event_callback into the federate ambassador of your chosen IEEE 1516-2010 RTI client.

Graceful Termination

se.terminate_simulation()  # Sets SIMULATION_TERMINATED state
se.is_terminated()         # Returns True if terminated

Signal handlers (SIGTERM, SIGINT) automatically invoke terminate_simulation() on all registered SysExecutor instances.

License

Author: Changbeom Choi (@cbchoi)
Copyright (c) 2014-2020 Handong Global University
Copyright (c) 2021-2024 Hanbat National University
License: MIT. The full license text is available at:

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyjevsim-2.0.1.tar.gz (38.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyjevsim-2.0.1-py3-none-any.whl (45.0 kB view details)

Uploaded Python 3

File details

Details for the file pyjevsim-2.0.1.tar.gz.

File metadata

  • Download URL: pyjevsim-2.0.1.tar.gz
  • Upload date:
  • Size: 38.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pyjevsim-2.0.1.tar.gz
Algorithm Hash digest
SHA256 007558c1493cba60af121848d485e3d9e58b7a9f11928452ecb6606e090e7a0f
MD5 0809b908a6c0bcdd272c6b7290ab46fc
BLAKE2b-256 ce825c23f692696eb802faa0ded9f26a0673bb8b3d1a023f6988634d30b432c8

See more details on using hashes here.

File details

Details for the file pyjevsim-2.0.1-py3-none-any.whl.

File metadata

  • Download URL: pyjevsim-2.0.1-py3-none-any.whl
  • Upload date:
  • Size: 45.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pyjevsim-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 693f0823d9615715816807c0fd96fa2c7c4d0b171d6cb4d4b3ce0c20eb5062c4
MD5 bb7b98e3de1599350998c7087af791b2
BLAKE2b-256 bb3403e023192dac6adc7d23d786b9ec3fe902c0dd0f1be8a0f759f664f0d745

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page