pyjevsim
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 (IEEE 1516-2010) federate integration with pluggable RTI backends. Version 2.2.0 includes adapters for Pitch pRTI and the open-source Portico RTI, plus an experimental native-Python GORTI adapter. Compatible with Python 3.10+.
Full documentation: https://pyjevsim.readthedocs.io/en/latest/
Changes in 2.2
- Portico backend. The live HLA adapter now supports Portico 2.1.4 in
addition to Pitch pRTI. Portico-specific handling covers its
HLAunicodeStringrepresentation and receive-order reflection behavior. - Experimental GORTI backend. A native-Python adapter supports selected
interaction, object-attribute, and regulating/constrained logical-time paths
through GORTI's source-installed
rti1516eSDK and a separately builtrtid, without Java or JPype. - AT/SIM reference data. The two-federate AT/SIM example includes two 30-tick scenarios, complete 180-row reference trajectories, and commands for offline and optional live-RTI comparison.
- Documented service scope. The HLA guide now includes architecture and sequence diagrams, the implemented IEEE 1516 service subset, logical-time behavior, backend extension guidance, related projects, and known limitations.
- Direction checks and failure reporting. Binding directions are checked at runtime, Portico examples use a bundled same-JVM configuration, and live AT/SIM runs report missing peer data or worker failures.
What's new in 2.1
- Pluggable RTI backends. A new
RTIConnectorinterface (pyjevsim.hla) defines the extension boundary through which an RTI can drive a pyjevsim federate without embedding RTI calls in model code. A minimal backend implements two abstract methods; a live HLA adapter also supplies lifecycle/declaration and inbound callback hooks. Direction enforcement, FOM codec, callback dispatch and the join/resign state machine are inherited. Ships an in-process bus (inprocess) for multi-federate testing and a Pitch pRTI (IEEE 1516-2010) backend (pitch, via JPype). Pick one by name withcreate_rti(...). - HLA ping-pong example (
examples/hla_pingpong/): two federates (ping/pong) exchanging interactions and synchronizing an object attribute — runnable offline (no Java) or against a live RTI. Verified live against Pitch pRTI Free 5.5.2. - Unified DEVS tick. V_TIME, R_TIME and HLA_TIME now share one
two-phase tick body. An imminent model with input at the same simulated
instant uses
con_transin each mode.
What's new in 2.0
- Two-phase tick.
SysExecutorevaluates every imminent model'soutput()first, then routes outputs and applies transitions — fixing confluent-event ordering under Parallel-DEVS semantics. - HLA stepped execution.
step(granted_time)andget_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:
pip install pyjevsim
This installs the latest published release. Use the source checkout below to test changes that have not yet been tagged.
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:
python -m pip install "pyjevsim[dev]"
The Java-backed Pitch and Portico adapters need JPype, declared under the
hla-java extra (the older hla-pitch name remains as a compatible alias).
The loopback and inprocess backends need nothing beyond the core package:
python -m pip install "pyjevsim[hla-java]"
The experimental GORTI backend is not installed by a pyjevsim extra because
GORTI's Python SDK is not currently published on PyPI. Install the SDK from a
GORTI source checkout before selecting the gorti backend:
python -m pip install -e C:\path\to\gorti\pysdk
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)
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
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)
The duration passed to insert_state() is the time advance for that state;
the executor does not call a model-defined time_advance() method.
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.hla_pingpong/— two HLA federates (ping/pong) demonstrating federation join/resign, interaction exchange, and object-attribute synchronization. Run offline (run_inprocess.py, no Java) or against a live Pitch pRTI (run_pitch.py).hla_atsim/— theatsimanti-torpedo scenario split into two HLA federates (surfaceship + torpedo) exchanging positions as HLA object attributes. Its sorted, formatted application-state rows exactly reproduce a committed single-executor reference for both decoy scenarios; the offline comparison needs no Java or proprietary RTI (verify_equivalence.py).
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. Treat received messages as immutable;
if a model needs to change a payload, copy it on the receiver side. The
comparison in benchmark/aliasing_test.py illustrates this behavior.
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 test scope and usage notes.
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 # generated output, gitignored
└── devstone_sweep.csv # generated output, gitignored
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 checks whether multiple subscribers receive the
same output value object. pyjevsim shares that value by reference. The script
also contains optional xdevs, PythonPDEVS, and reference-engine adapters;
details are in
benchmark/results/ALIASING.md.
Treat a received value as immutable, or copy it before changing it.
The historical cross-engine record contains one capture from 2026-05-05 and its limitations. Run the command above to measure the current commit and retain its generated CSV with the environment details.
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 a message emitted on a port with no downstream
coupling. Pass track_uncaught=True to route those messages to the built-in
DefaultMessageCatcher, available as se.dmc:
se = SysExecutor(1, ex_mode=ExecutionType.V_TIME, track_uncaught=True)
Each captured message invokes the catcher's ext_trans and reschedules it, so
this diagnostic mode adds work for every uncoupled output. Enable it only when
that information is needed.
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 protects its pause state and external-event queue with a
condition lock. Model transitions still run on the simulation thread.
Pause / Resume
pause_sim() stops the simulation loop; external threads may continue to add
events. resume_sim() wakes the paused loop.
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_external_event() adds an event to the protected input queue. The
scheduled time is relative to the executor's current global_time.
se.insert_external_event("port_name", message, scheduled_time=0)
Output Event Callback
set_output_event_callback() registers a no-argument callback that runs when
an external output event is queued. handle_external_output_event() returns
a copied snapshot and clears the queue under the same lock.
se.set_output_event_callback(lambda: print("output ready"))
events = se.handle_external_output_event()
HLA/RTI Integration
pyjevsim integrates with HLA (IEEE 1516-2010) at two levels: a high-level
pluggable RTI backend layer (pyjevsim.hla) that runs ordinary DEVS
models as federates, and the low-level HLA_TIME stepping hooks for
custom federate ambassadors.
The repository's HLA validation and reproducibility guide collects the architecture diagrams, exact equivalence criterion, full expected traces, RTI/service coverage, limitations, and related projects.
Pluggable RTI backends (pyjevsim.hla)
Your BehaviorModel declares HLA bindings on its ports (an
HLAInteraction or HLAAttribute per FOM id); an HLAExecutorFactory
bridges those ports to a transport; and a Federate drives the
time-advance loop. The transport is chosen by name — the same models run
on the shipped backends when their FOM and required service subset are
supported:
from pyjevsim import SysExecutor, ExecutionType
from pyjevsim.hla import (
create_rti, available_rtis, HLAExecutorFactory, HLAInteraction, Federate,
)
print(available_rtis()) # ['gorti', 'inprocess', 'loopback', 'pitch', 'portico']
transport = create_rti("inprocess") # or "pitch" / "portico" / "gorti"
sys_exec = SysExecutor(1, ex_mode=ExecutionType.HLA_TIME)
sys_exec.exec_factory = HLAExecutorFactory(
transport, {"chatter": {"out": HLAInteraction("Comm.Msg", direction="out")}}
)
sys_exec.register_entity(my_model)
fed = Federate(sys_exec, transport)
fed.join("MyFederation", "chatter", fom_paths=["Comm.xml"])
fed.publish(HLAInteraction("Comm.Msg", direction="out"))
fed.run_until(end_time=60.0, lookahead=1.0)
fed.resign()
The run_until argument historically named lookahead is the grant-request
increment. The backend owns the distinct HLA regulating interval and time-unit
mapping: Pitch uses its configured interval; Portico uses one internal RTI
sub-step; GORTI maps pyjevsim logical time directly to RTI logical time.
Built-in backends:
| Name | Use | Dependency |
|---|---|---|
loopback |
self-mirror, single-federate unit tests | none |
inprocess |
multi-federate in-process bus (tests/demos) | none |
pitch |
Pitch pRTI IEEE 1516-2010, live federation | python -m pip install "pyjevsim[hla-java]" + Java ≥ 11 + a running CRC |
portico |
Portico (open source) IEEE 1516-2010, live federation | python -m pip install "pyjevsim[hla-java]" + Java ≥ 11 + portico.jar (no CRC) |
gorti |
experimental GORTI native-Python client for selected interaction, object-attribute, and logical-time paths | source-install the SDK with python -m pip install -e C:\path\to\gorti\pysdk + a reachable rtid |
The Java-backed live backends program against the standard hla.rti1516e
API; the
portico backend subclasses the pitch implementation and adapts its
HLAunicodeString codec and receive-order delivery of time-stamped
reflections. See
pyjevsim/hla/backends/portico.py.
The gorti backend uses GORTI's rti1516e Python SDK directly and supports
interactions, object attributes, and regulating/constrained logical time
without a JVM.
Recorded GORTI AT/SIM checks cover object instance registration/discovery, six-attribute update/reflection, and logical-time grants. They do not establish release-grade GORTI support, a public multi-instance or explicit delete-object API, complete HLA Object Management, or formal IEEE 1516 conformance.
Adding your own RTI (CERTI, OpenRTI, MÄK, …): subclass
RTIConnector and implement _do_send + _do_request_time_advance
(the minimal abstract surface). A live HLA adapter also overrides join,
declaration, cleanup, and invokes _emit from its receive callback. Then call
register_rti("name", factory).
See docs/hla/rti_interface.md for the full
guide and examples/hla_pingpong/ for a
working two-federate example.
Low-level stepping (HLA_TIME mode)
If you prefer to wire pyjevsim into your own federate ambassador, use
HLA_TIME mode directly 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 two-phase Parallel-DEVS tick as the standalone V_TIME path:
- Every event whose
req_time <= granted_timefires 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_timeis the event time read by model transitions. - After processing, pyjevsim sets
global_timetogranted_time, even if the last processed event was earlier. - Returns the
output_event_queuecontents drained during this step (adequeof(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 ready-made Pitch pRTI and Portico backends, the experimental
GORTI adapter described above, and an
RTIConnector interface for adding others. If instead you want to embed
the simulator into an existing federate ambassador, wire step /
get_next_event_time / insert_external_event /
set_output_event_callback into the ambassador of your chosen IEEE
1516-2010 RTI client directly — this is the core path used by pitch and its
portico subclass.
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.
Support and contributing
Use GitHub Issues for bug reports, feature requests, and usage questions. See CONTRIBUTING.md for the development setup, test commands, and pull-request guidelines. Security reports should follow SECURITY.md.
License
Author: Changbeom Choi (@cbchoi)
Copyright (c) 2014-2020 Handong Global University
Copyright (c) 2021-2026 Hanbat National University
License: MIT. The full license text is available at:
Release files for pyjevsim 2.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyjevsim-2.2.0.tar.gz | 61.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyjevsim-2.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 132.5 kB
Release files / pyjevsim-2.2.0.tar.gz
| Download URL | pyjevsim-2.2.0.tar.gz |
|---|---|
| Size | 61.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f59e9d4f11800818bbf7dea710d2362efb4695ef21a8982900aa64307b7b8f15
|
|
BLAKE2b-256 checksum How to use checksums |
19f65e6d7bb3f48d6a7e59721a356516a6fc82d78616355ad2887470ff4a90c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Release files / pyjevsim-2.2.0-py3-none-any.whl
| Download URL | pyjevsim-2.2.0-py3-none-any.whl |
|---|---|
| Size | 70.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7b13d73f89b24644b2d35245dc8505b9b8f7e9da798e29858d813c35effe5497
|
|
BLAKE2b-256 checksum How to use checksums |
f376851fd4adaccbbbcebd40fb69e32c522182a59f738bd35fb97b0cecbaeb35
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|