Skip to main content

SMOLPy

CI PyPI version Python License Docs

Python rewrite of SMOL — a Network Description Language and Discrete-Event Simulator for industrial Measurement-Diagnostics-Control (MDC) networks.

SMOLPy lets you describe a network topology in pure Python, define traffic flows, and run a discrete-event simulation (powered by SimPy) that produces real metric time-series. A built-in Dear PyGui desktop dashboard shows the topology and live metric charts as the simulation runs.

📖 Full documentation: jwszolek.github.io/SMOLPy


Install

uv sync          # installs all runtime + dev dependencies

Quick start

from smolpy import Network

net    = Network("office-net")
host_a = net.adapter("host-A", ip="10.0.0.1")
server = net.adapter("server",  ip="10.0.0.10")
sw1    = net.switch("sw1", ports=8, mode="store-and-forward")

net.link(host_a, sw1, speed=1_000,  length=5)   # speed in Mb/s, length in metres
net.link(server, sw1, speed=10_000, length=2)

host_a.sends(to=server, rate=8_000, size=1_518, pattern="constant")

net.observe("throughput",  on=server, every=100)   # sample every 100 ms
net.observe("queue_depth", on=sw1,    every=50)

result = net.simulate(duration=30_000, live=True)  # 30 s simulation with live dashboard
result.report()                                     # print summary table to terminal

Run it:

uv run smolpy run my_script.py

Dashboard

When live=True the simulation runs in a background thread while a full-screen Dear PyGui window opens immediately.

Topology panel (left)

Each node is drawn as a coloured circle:

Colour Node type
Blue Adapter (host / server / NIC)
Green Switch
Orange Hub

Node fill changes dynamically during the simulation:

Appearance Meaning
Dim (faded) Node idle — no traffic yet
Pulsing bright fill Node actively transmitting (bytes_sent > 0)
Solid bright fill Node forwarding traffic (switch / hub)
Pulsing amber outer ring Node actively receiving data (bytes_received > 0)

Animated particles flow along every link to show live traffic direction.

Metrics panel (right)

One chart per observed metric. All series update in real time. Axes auto-scale to fit the data.

Simulation controls

Three controls appear in the title bar during a live simulation:

Control Effect
⏸ Pause Freezes simulation time; dashboard stays interactive. Click again to resume.
▶ Resume Continues from the exact pause point.
⏹ Stop Ends the simulation early; plots freeze at the last collected sample.

Status indicator:

  • ● Simulating… — running
  • ● Paused — paused by user
  • ● Done — completed normally
  • ● Stopped — ended by user

DSL reference

Topology builders

adapter = net.adapter("name", ip="10.0.0.1")          # NIC / host / server
switch  = net.switch("name",  ports=16, mode="store-and-forward")
hub     = net.hub("name",     ports=8)
broker  = net.mqtt_broker("name", ip="10.0.2.1")       # MQTT message broker
slave   = net.modbus_slave("name", ip="10.0.0.10", unit_id=1)  # Modbus TCP slave
net.link(a, b, speed=1_000, length=10)                 # Mb/s and metres

Multiple switches can be chained to model hierarchical topologies:

core_sw = net.switch("core-sw", ports=16, mode="store-and-forward")
edge_sw = net.switch("edge-sw", ports=8,  mode="store-and-forward")
net.link(edge_sw, core_sw, speed=1_000, length=5)      # inter-switch uplink

Traffic

# Basic Ethernet send
src.sends(to=dst, rate=8_000, size=1_518, pattern="constant")

# Delayed start (useful for staggered scenarios)
src.sends(to=dst, rate=8_000, size=1_518, pattern="constant", delay_ms=5_000)

# MQTT publish (sensor-style, constant-rate)
sensor.publishes(to=broker, topic="plant/temp", rate=1.0, payload=20, qos=1)
sensor.publishes(to=broker, topic="plant/temp", rate=1.0, payload=20, qos=0, delay_ms=2_000)

# Broker topic routing — must be called before simulate()
broker.routes("plant/temp", to=[server])

# Modbus TCP poll (master → slave, Read Holding Registers)
plc.polls(slave, register=40001, count=10, rate=1.0)
plc.polls(slave, register=30001, count=5, rate=2.0, delay_ms=500)
Parameter Type Description
to Adapter Destination adapter
rate float Frames per second
size int | "imix" Frame size in bytes, or Internet Mix distribution
pattern str "constant", "poisson", or "bursty"
delay_ms float Simulation time before this flow starts (default 0)

publishes() parameters

Parameter Type Description
to MQTTBroker Target broker
topic str MQTT topic string
rate float Messages per second (default 1.0)
payload int Payload bytes (default 20)
qos int 0 = fire-and-forget, 1 = PUBACK acknowledgement
delay_ms float Simulation time before publishing starts (default 0)

polls() parameters

Parameter Type Description
slave ModbusSlave Target slave to poll
register int Starting holding-register address
count int Number of registers to read
rate float Polls per second (default 1.0)
delay_ms float Simulation time before polling starts (default 0)

Traffic patterns

Pattern Description
"constant" Fixed inter-frame gap — models a saturated link
"poisson" Exponentially distributed gaps — models random/bursty traffic
"bursty" Pareto-distributed burst lengths — models ON/OFF sources

Frame sizes

Value Description
integer Fixed size in bytes (e.g. 512, 1_518)
"imix" 40 % × 64 B, 57 % × 594 B, 3 % × 1 518 B

Observations

net.observe(metric, on=node, every=interval_ms)
Metric Unit Observed on
throughput Mb/s Adapter
latency µs Adapter
frame_loss % Adapter
bytes_sent MB Adapter (sender)
bytes_received MB Adapter (receiver)
queue_depth frames Switch
utilization % Any node
collision_rate /s Hub
broker_queue msgs MQTTBroker
modbus_latency µs Adapter (master)

Simulation

result = net.simulate(duration=30_000)              # headless — silent, fastest
result = net.simulate(duration=30_000, text=True)   # rich text dashboard in terminal
result = net.simulate(duration=30_000, live=True)   # full Dear PyGui desktop window

result.report()   # print summary table (avg / min / max per metric)
result.plot()     # open static dashboard for a completed result

# Export metric time-series (format inferred from extension)
result.export("results.csv")    # long CSV: time_ms, metric, value
result.export("results.json")   # JSON dict of lists-of-pairs
result.export("out.csv", format="csv")   # explicit format override

Text mode (text=True) displays a live updating table in the terminal — no display server or GUI toolkit required. Ideal for headless servers, SSH sessions, and CI environments.

Quick demo

smolpy demo          # built-in 3-client scenario, text mode, no script needed

MQTT publish-subscribe

SMOLPy models application-layer MQTT traffic on top of the standard Ethernet/IP/TCP wire model.

What is modelled

  • Publisher adapters call publishes() to emit periodic MQTT PUBLISH frames at a fixed rate toward an MQTTBroker node.
  • The broker receives PUBLISH frames and fans out one copy to each registered subscriber per topic (routes()). QoS 0 delivers silently; QoS 1 additionally sends a PUBACK frame (58 bytes) back toward the publisher.
  • Subscriber adapters receive forwarded copies just like normal Ethernet frames; all standard metrics (throughput, latency, bytes_received) apply.
  • broker_queue samples the broker's inbound store depth — unprocessed PUBLISH frames waiting to be forwarded. A non-zero and rising queue indicates the broker or its downstream link is becoming a bottleneck.

Frame size formula

frame_size = 54 (Ethernet+IPv4+TCP) + 2 (MQTT fixed header) + 2 (topic-length field) + len(topic) + (2 if qos > 0 else 0) + payload_bytes

A typical small sensor message (topic="plant/temperature", payload=20, qos=1) produces a 96-byte frame, roughly 16× smaller than a maximum-size bulk frame (1 518 B).

Dashboard

MQTTBroker nodes appear as purple circles in the topology panel.


Modbus TCP master/slave polling

SMOLPy also models Modbus TCP — a request/response protocol, unlike MQTT's fire-and-forget publish/subscribe. Because Modbus TCP already wraps its PDU in a standard Ethernet/IP/TCP frame (no serial bus, no gateway required), a ModbusSlave is a first-class network node wired into the same switch fabric as any Adapter.

What is modelled

  • The master is a regular Adapter — it calls polls() to periodically send Read Holding Registers (FC 03) requests toward a ModbusSlave.
  • Each slave is addressed by its own unit_id (1–247); it replies only to requests matching its unit_id and ignores everything else. Multiple sensors can share one switch, each polled independently by the same master.
  • Every poll is a request/response round trip — both frames traverse the network and contend for bandwidth, unlike MQTT where only the publisher-to-broker leg carries the payload.
  • modbus_latency measures poll round-trip time (RTT): the time from when the master sends a request to when the matching response arrives back. This is the key metric for judging whether a polling interval is achievable on a given network.

Frame size formula

request_size  = 65 bytes                                  (constant — Ethernet/IP/TCP + MBAP + FC + address + quantity)
response_size = 62 + 2 × register_count bytes              (grows with the number of registers read)

Dashboard

ModbusSlave nodes appear as coral red circles in the topology panel.


Simulation engine

  • MAC-learning switch — each switch pre-seeds its forwarding table from the topology wiring, eliminating spurious flooding toward silent endpoints (e.g. a server that only receives). Dynamic learning still operates for traffic through intermediate switches.
  • Store-and-forward model — transmission delay + propagation delay per hop.
  • Queuing — each link direction is an independent SimPy Store; queue_depth reports buffered frames at the switch's outbound ports.
  • Traffic shaping — constant, Poisson, and Pareto-burst patterns; IMIX frame-size distribution.
  • Live mode — simulation runs in 200 chunks (~8 s total wall time); the dashboard reads shared metric arrays between chunks via Python's GIL.

Examples

See examples/README.md for nine ready-to-run scenarios covering single-switch saturation, oversubscription, two-tier access bottlenecks, MQTT publish-subscribe, and Modbus TCP polling.

uv run smolpy run examples/example.py
uv run smolpy run examples/example_two_tier.py

Release files for smolpy 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for smolpy 0.5.0
File Size Uploaded
smolpy-0.5.0.tar.gz 143.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for smolpy 0.5.0
File Interpreter ABI Platform
smolpy-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 172.5 kB

Release files / smolpy-0.5.0.tar.gz

Download URL smolpy-0.5.0.tar.gz
Size 143.1 kB
Tags Source
SHA-256 checksum
How to use checksums
28f453e549b7374966a5998c2952b37d6f2e94ce00467d87af56c378c5c3e7f2
BLAKE2b-256 checksum
How to use checksums
bf227a74d492ace527aec62bcd08a6fa8fc93cd205441ebb1ffe23b03f5334a0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / smolpy-0.5.0-py3-none-any.whl

Download URL smolpy-0.5.0-py3-none-any.whl
Size 29.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
46b7fb457cc3f5692c3d42e4d6e986b59626c8beffe5d07d016656212e6ee1bb
BLAKE2b-256 checksum
How to use checksums
ad34d80022b92b1cba953dc625a4c7a5ec9945d1dd9ce0bfeefbcafc59a17529
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

0.6.0

2 release files

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.1.0

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page