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

SMOL external DSL

Network topologies can also be described in a standalone .smol file — no Python required. smolpy run executes .smol files directly, using the same simulation engine underneath.

network "office-net" {
    adapter host-A ip=10.0.0.1
    adapter server ip=10.0.0.10
    switch  sw1    ports=8 mode=store-and-forward

    link host-A -- sw1 speed=1000  length=5
    link server -- sw1 speed=10000 length=2

    flow host-A -> server rate=8000 size=1518 pattern=constant

    observe throughput  on server every=100
    observe queue_depth on sw1    every=50

    simulate duration=30000 mode=text
}
uv run smolpy run topology.smol

Malformed or semantically invalid .smol files (undeclared node references, wrong node type for a statement, an unsupported metric, ...) produce a clean file:line: message error — no Python traceback. See the SMOL Language Reference for the complete syntax.


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.6.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.6.0
File Size Uploaded
smolpy-0.6.0.tar.gz 151.9 kB Details

Built distribution (wheel)

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

Total release size: 188.0 kB

Release files / smolpy-0.6.0.tar.gz

Download URL smolpy-0.6.0.tar.gz
Size 151.9 kB
Tags Source
SHA-256 checksum
How to use checksums
2848669badac6624094f481e40ae9e48e08ab7e001984dfd76c4221b5e799b79
BLAKE2b-256 checksum
How to use checksums
18418236242a7df7e3e2eefa999c0040f87d5acc83659ae63d4e4fb786f18436
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.6.0-py3-none-any.whl

Download URL smolpy-0.6.0-py3-none-any.whl
Size 36.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8fbd68c0a75602238989a73483fcf2db02906ec812262077da18a105c88f8295
BLAKE2b-256 checksum
How to use checksums
ebc1638658fc885906ccfb9bf3057d8a1aaebaccc9c85eed6e01e65804a60f89
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

This release

0.6.0 This release

2 release files

0.5.0

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