smolpy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. smolpy-0.1.0/.github/workflows/ci.yml +45 -0
  2. smolpy-0.1.0/.github/workflows/publish.yml +48 -0
  3. smolpy-0.1.0/.gitignore +27 -0
  4. smolpy-0.1.0/PKG-INFO +268 -0
  5. smolpy-0.1.0/README.md +251 -0
  6. smolpy-0.1.0/examples/README.md +341 -0
  7. smolpy-0.1.0/examples/example.py +22 -0
  8. smolpy-0.1.0/examples/example_10_clients.py +68 -0
  9. smolpy-0.1.0/examples/example_12_clients.py +71 -0
  10. smolpy-0.1.0/examples/example_20_clients.py +71 -0
  11. smolpy-0.1.0/examples/example_file_transfer.py +82 -0
  12. smolpy-0.1.0/examples/example_mqtt.py +79 -0
  13. smolpy-0.1.0/examples/example_staggered_transfer.py +80 -0
  14. smolpy-0.1.0/examples/example_two_tier.py +98 -0
  15. smolpy-0.1.0/pyproject.toml +49 -0
  16. smolpy-0.1.0/src/smolpy/__init__.py +4 -0
  17. smolpy-0.1.0/src/smolpy/cli.py +178 -0
  18. smolpy-0.1.0/src/smolpy/dsl/__init__.py +7 -0
  19. smolpy-0.1.0/src/smolpy/dsl/adapter.py +89 -0
  20. smolpy-0.1.0/src/smolpy/dsl/hub.py +11 -0
  21. smolpy-0.1.0/src/smolpy/dsl/link.py +27 -0
  22. smolpy-0.1.0/src/smolpy/dsl/mqtt_broker.py +23 -0
  23. smolpy-0.1.0/src/smolpy/dsl/network.py +168 -0
  24. smolpy-0.1.0/src/smolpy/dsl/node.py +18 -0
  25. smolpy-0.1.0/src/smolpy/dsl/observation.py +26 -0
  26. smolpy-0.1.0/src/smolpy/dsl/switch.py +17 -0
  27. smolpy-0.1.0/src/smolpy/sim/__init__.py +0 -0
  28. smolpy-0.1.0/src/smolpy/sim/engine.py +580 -0
  29. smolpy-0.1.0/src/smolpy/viz/__init__.py +0 -0
  30. smolpy-0.1.0/src/smolpy/viz/dashboard.py +464 -0
  31. smolpy-0.1.0/src/smolpy/viz/text_dashboard.py +134 -0
  32. smolpy-0.1.0/tests/__init__.py +0 -0
  33. smolpy-0.1.0/tests/test_dsl.py +45 -0
  34. smolpy-0.1.0/tests/test_simulation.py +242 -0
  35. smolpy-0.1.0/uv.lock +1438 -0
@@ -0,0 +1,45 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint-and-test:
11
+ name: Python ${{ matrix.python-version }}
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.11", "3.12"]
18
+
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v4
24
+ with:
25
+ version: "latest"
26
+ enable-cache: true
27
+
28
+ - name: Set up Python ${{ matrix.python-version }}
29
+ run: uv python install ${{ matrix.python-version }}
30
+
31
+ - name: Install dependencies
32
+ run: uv sync --all-extras
33
+
34
+ - name: Lint — ruff check
35
+ run: uv run ruff check src/
36
+
37
+ - name: Lint — ruff format check
38
+ run: uv run ruff format --check src/
39
+
40
+ - name: Type check — mypy
41
+ run: uv run mypy src/smolpy/
42
+ continue-on-error: true # treat type errors as warnings until stubs are complete
43
+
44
+ - name: Run tests
45
+ run: uv run pytest tests/ -v --tb=short
@@ -0,0 +1,48 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distribution
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0 # needed for hatch-vcs to read git tags
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v4
20
+ with:
21
+ version: "latest"
22
+
23
+ - name: Build package
24
+ run: uv build
25
+
26
+ - name: Upload dist
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ publish:
33
+ name: Publish to PyPI
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment: pypi
37
+ permissions:
38
+ id-token: write # required for OIDC trusted publishing
39
+
40
+ steps:
41
+ - name: Download dist
42
+ uses: actions/download-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+
47
+ - name: Publish to PyPI
48
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,27 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+
13
+ # uv
14
+ .uv/
15
+
16
+ # Test / coverage
17
+ .pytest_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ # Editors
22
+ .vscode/
23
+ .idea/
24
+ *.swp
25
+
26
+ # macOS
27
+ .DS_Store
smolpy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.4
2
+ Name: smolpy
3
+ Version: 0.1.0
4
+ Summary: Network simulation DSL and discrete-event simulator
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: dash>=2.17
7
+ Requires-Dist: dearpygui>=1.6
8
+ Requires-Dist: networkx>=3.3
9
+ Requires-Dist: plotly>=5.22
10
+ Requires-Dist: rich>=13
11
+ Requires-Dist: simpy>=4.1
12
+ Provides-Extra: dev
13
+ Requires-Dist: mypy; extra == 'dev'
14
+ Requires-Dist: pytest>=8; extra == 'dev'
15
+ Requires-Dist: ruff; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # SMOLPy
19
+
20
+ Python rewrite of SMOL — a Network Description Language and Discrete-Event Simulator for industrial Measurement-Diagnostics-Control (MDC) networks.
21
+
22
+ 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.
23
+
24
+ ---
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ uv sync # installs all runtime + dev dependencies
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ from smolpy import Network
38
+
39
+ net = Network("office-net")
40
+ host_a = net.adapter("host-A", ip="10.0.0.1")
41
+ server = net.adapter("server", ip="10.0.0.10")
42
+ sw1 = net.switch("sw1", ports=8, mode="store-and-forward")
43
+
44
+ net.link(host_a, sw1, speed=1_000, length=5) # speed in Mb/s, length in metres
45
+ net.link(server, sw1, speed=10_000, length=2)
46
+
47
+ host_a.sends(to=server, rate=8_000, size=1_518, pattern="constant")
48
+
49
+ net.observe("throughput", on=server, every=100) # sample every 100 ms
50
+ net.observe("queue_depth", on=sw1, every=50)
51
+
52
+ result = net.simulate(duration=30_000, live=True) # 30 s simulation with live dashboard
53
+ result.report() # print summary table to terminal
54
+ ```
55
+
56
+ Run it:
57
+
58
+ ```bash
59
+ uv run smolpy run my_script.py
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Dashboard
65
+
66
+ When `live=True` the simulation runs in a background thread while a full-screen Dear PyGui window opens immediately.
67
+
68
+ ### Topology panel (left)
69
+
70
+ Each node is drawn as a coloured circle:
71
+
72
+ | Colour | Node type |
73
+ |---|---|
74
+ | Blue | Adapter (host / server / NIC) |
75
+ | Green | Switch |
76
+ | Orange | Hub |
77
+
78
+ Node fill changes dynamically during the simulation:
79
+
80
+ | Appearance | Meaning |
81
+ |---|---|
82
+ | Dim (faded) | Node idle — no traffic yet |
83
+ | Pulsing bright fill | Node actively **transmitting** (`bytes_sent > 0`) |
84
+ | Solid bright fill | Node forwarding traffic (switch / hub) |
85
+ | Pulsing amber outer ring | Node actively **receiving** data (`bytes_received > 0`) |
86
+
87
+ Animated particles flow along every link to show live traffic direction.
88
+
89
+ ### Metrics panel (right)
90
+
91
+ One chart per observed metric. All series update in real time. Axes auto-scale to fit the data.
92
+
93
+ ### Simulation controls
94
+
95
+ Three controls appear in the title bar during a live simulation:
96
+
97
+ | Control | Effect |
98
+ |---|---|
99
+ | **⏸ Pause** | Freezes simulation time; dashboard stays interactive. Click again to resume. |
100
+ | **▶ Resume** | Continues from the exact pause point. |
101
+ | **⏹ Stop** | Ends the simulation early; plots freeze at the last collected sample. |
102
+
103
+ Status indicator:
104
+ - **● Simulating…** — running
105
+ - **● Paused** — paused by user
106
+ - **● Done** — completed normally
107
+ - **● Stopped** — ended by user
108
+
109
+ ---
110
+
111
+ ## DSL reference
112
+
113
+ ### Topology builders
114
+
115
+ ```python
116
+ adapter = net.adapter("name", ip="10.0.0.1") # NIC / host / server
117
+ switch = net.switch("name", ports=16, mode="store-and-forward")
118
+ hub = net.hub("name", ports=8)
119
+ broker = net.mqtt_broker("name", ip="10.0.2.1") # MQTT message broker
120
+ net.link(a, b, speed=1_000, length=10) # Mb/s and metres
121
+ ```
122
+
123
+ Multiple switches can be chained to model hierarchical topologies:
124
+
125
+ ```python
126
+ core_sw = net.switch("core-sw", ports=16, mode="store-and-forward")
127
+ edge_sw = net.switch("edge-sw", ports=8, mode="store-and-forward")
128
+ net.link(edge_sw, core_sw, speed=1_000, length=5) # inter-switch uplink
129
+ ```
130
+
131
+ ### Traffic
132
+
133
+ ```python
134
+ # Basic Ethernet send
135
+ src.sends(to=dst, rate=8_000, size=1_518, pattern="constant")
136
+
137
+ # Delayed start (useful for staggered scenarios)
138
+ src.sends(to=dst, rate=8_000, size=1_518, pattern="constant", delay_ms=5_000)
139
+
140
+ # MQTT publish (sensor-style, constant-rate)
141
+ sensor.publishes(to=broker, topic="plant/temp", rate=1.0, payload=20, qos=1)
142
+ sensor.publishes(to=broker, topic="plant/temp", rate=1.0, payload=20, qos=0, delay_ms=2_000)
143
+
144
+ # Broker topic routing — must be called before simulate()
145
+ broker.routes("plant/temp", to=[server])
146
+ ```
147
+
148
+ | Parameter | Type | Description |
149
+ |---|---|---|
150
+ | `to` | Adapter | Destination adapter |
151
+ | `rate` | float | Frames per second |
152
+ | `size` | int \| `"imix"` | Frame size in bytes, or Internet Mix distribution |
153
+ | `pattern` | str | `"constant"`, `"poisson"`, or `"bursty"` |
154
+ | `delay_ms` | float | Simulation time before this flow starts (default 0) |
155
+
156
+ **`publishes()` parameters**
157
+
158
+ | Parameter | Type | Description |
159
+ |---|---|---|
160
+ | `to` | MQTTBroker | Target broker |
161
+ | `topic` | str | MQTT topic string |
162
+ | `rate` | float | Messages per second (default 1.0) |
163
+ | `payload` | int | Payload bytes (default 20) |
164
+ | `qos` | int | 0 = fire-and-forget, 1 = PUBACK acknowledgement |
165
+ | `delay_ms` | float | Simulation time before publishing starts (default 0) |
166
+
167
+ **Traffic patterns**
168
+
169
+ | Pattern | Description |
170
+ |---|---|
171
+ | `"constant"` | Fixed inter-frame gap — models a saturated link |
172
+ | `"poisson"` | Exponentially distributed gaps — models random/bursty traffic |
173
+ | `"bursty"` | Pareto-distributed burst lengths — models ON/OFF sources |
174
+
175
+ **Frame sizes**
176
+
177
+ | Value | Description |
178
+ |---|---|
179
+ | integer | Fixed size in bytes (e.g. `512`, `1_518`) |
180
+ | `"imix"` | 40 % × 64 B, 57 % × 594 B, 3 % × 1 518 B |
181
+
182
+ ### Observations
183
+
184
+ ```python
185
+ net.observe(metric, on=node, every=interval_ms)
186
+ ```
187
+
188
+ | Metric | Unit | Observed on |
189
+ |---|---|---|
190
+ | `throughput` | Mb/s | Adapter |
191
+ | `latency` | µs | Adapter |
192
+ | `frame_loss` | % | Adapter |
193
+ | `bytes_sent` | MB | Adapter (sender) |
194
+ | `bytes_received` | MB | Adapter (receiver) |
195
+ | `queue_depth` | frames | Switch |
196
+ | `utilization` | % | Any node |
197
+ | `collision_rate` | /s | Hub |
198
+ | `broker_queue` | msgs | MQTTBroker |
199
+
200
+ ### Simulation
201
+
202
+ ```python
203
+ result = net.simulate(duration=30_000) # headless — silent, fastest
204
+ result = net.simulate(duration=30_000, text=True) # rich text dashboard in terminal
205
+ result = net.simulate(duration=30_000, live=True) # full Dear PyGui desktop window
206
+
207
+ result.report() # print summary table (avg / min / max per metric)
208
+ result.plot() # open static dashboard for a completed result
209
+
210
+ # Export metric time-series (format inferred from extension)
211
+ result.export("results.csv") # long CSV: time_ms, metric, value
212
+ result.export("results.json") # JSON dict of lists-of-pairs
213
+ result.export("out.csv", format="csv") # explicit format override
214
+ ```
215
+
216
+ **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.
217
+
218
+ ### Quick demo
219
+
220
+ ```bash
221
+ smolpy demo # built-in 3-client scenario, text mode, no script needed
222
+ ```
223
+
224
+ ---
225
+
226
+ ## MQTT publish-subscribe
227
+
228
+ SMOLPy models application-layer MQTT traffic on top of the standard Ethernet/IP/TCP wire model.
229
+
230
+ ### What is modelled
231
+
232
+ - **Publisher adapters** call `publishes()` to emit periodic MQTT PUBLISH frames at a fixed rate toward an `MQTTBroker` node.
233
+ - **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.
234
+ - **Subscriber adapters** receive forwarded copies just like normal Ethernet frames; all standard metrics (`throughput`, `latency`, `bytes_received`) apply.
235
+ - **`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.
236
+
237
+ ### Frame size formula
238
+
239
+ ```
240
+ frame_size = 54 (Ethernet+IPv4+TCP) + 2 (MQTT fixed header) + 2 (topic-length field) + len(topic) + (2 if qos > 0 else 0) + payload_bytes
241
+ ```
242
+
243
+ 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).
244
+
245
+ ### Dashboard
246
+
247
+ `MQTTBroker` nodes appear as **purple** circles in the topology panel.
248
+
249
+ ---
250
+
251
+ ## Simulation engine
252
+
253
+ - **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.
254
+ - **Store-and-forward model** — transmission delay + propagation delay per hop.
255
+ - **Queuing** — each link direction is an independent SimPy Store; `queue_depth` reports buffered frames at the switch's outbound ports.
256
+ - **Traffic shaping** — constant, Poisson, and Pareto-burst patterns; IMIX frame-size distribution.
257
+ - **Live mode** — simulation runs in 200 chunks (~8 s total wall time); the dashboard reads shared metric arrays between chunks via Python's GIL.
258
+
259
+ ---
260
+
261
+ ## Examples
262
+
263
+ See [`examples/README.md`](examples/README.md) for eight ready-to-run scenarios covering single-switch saturation, oversubscription, two-tier access bottlenecks, and MQTT publish-subscribe.
264
+
265
+ ```bash
266
+ uv run smolpy run examples/example.py
267
+ uv run smolpy run examples/example_two_tier.py
268
+ ```
smolpy-0.1.0/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # SMOLPy
2
+
3
+ Python rewrite of SMOL — a Network Description Language and Discrete-Event Simulator for industrial Measurement-Diagnostics-Control (MDC) networks.
4
+
5
+ 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.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ uv sync # installs all runtime + dev dependencies
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Quick start
18
+
19
+ ```python
20
+ from smolpy import Network
21
+
22
+ net = Network("office-net")
23
+ host_a = net.adapter("host-A", ip="10.0.0.1")
24
+ server = net.adapter("server", ip="10.0.0.10")
25
+ sw1 = net.switch("sw1", ports=8, mode="store-and-forward")
26
+
27
+ net.link(host_a, sw1, speed=1_000, length=5) # speed in Mb/s, length in metres
28
+ net.link(server, sw1, speed=10_000, length=2)
29
+
30
+ host_a.sends(to=server, rate=8_000, size=1_518, pattern="constant")
31
+
32
+ net.observe("throughput", on=server, every=100) # sample every 100 ms
33
+ net.observe("queue_depth", on=sw1, every=50)
34
+
35
+ result = net.simulate(duration=30_000, live=True) # 30 s simulation with live dashboard
36
+ result.report() # print summary table to terminal
37
+ ```
38
+
39
+ Run it:
40
+
41
+ ```bash
42
+ uv run smolpy run my_script.py
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Dashboard
48
+
49
+ When `live=True` the simulation runs in a background thread while a full-screen Dear PyGui window opens immediately.
50
+
51
+ ### Topology panel (left)
52
+
53
+ Each node is drawn as a coloured circle:
54
+
55
+ | Colour | Node type |
56
+ |---|---|
57
+ | Blue | Adapter (host / server / NIC) |
58
+ | Green | Switch |
59
+ | Orange | Hub |
60
+
61
+ Node fill changes dynamically during the simulation:
62
+
63
+ | Appearance | Meaning |
64
+ |---|---|
65
+ | Dim (faded) | Node idle — no traffic yet |
66
+ | Pulsing bright fill | Node actively **transmitting** (`bytes_sent > 0`) |
67
+ | Solid bright fill | Node forwarding traffic (switch / hub) |
68
+ | Pulsing amber outer ring | Node actively **receiving** data (`bytes_received > 0`) |
69
+
70
+ Animated particles flow along every link to show live traffic direction.
71
+
72
+ ### Metrics panel (right)
73
+
74
+ One chart per observed metric. All series update in real time. Axes auto-scale to fit the data.
75
+
76
+ ### Simulation controls
77
+
78
+ Three controls appear in the title bar during a live simulation:
79
+
80
+ | Control | Effect |
81
+ |---|---|
82
+ | **⏸ Pause** | Freezes simulation time; dashboard stays interactive. Click again to resume. |
83
+ | **▶ Resume** | Continues from the exact pause point. |
84
+ | **⏹ Stop** | Ends the simulation early; plots freeze at the last collected sample. |
85
+
86
+ Status indicator:
87
+ - **● Simulating…** — running
88
+ - **● Paused** — paused by user
89
+ - **● Done** — completed normally
90
+ - **● Stopped** — ended by user
91
+
92
+ ---
93
+
94
+ ## DSL reference
95
+
96
+ ### Topology builders
97
+
98
+ ```python
99
+ adapter = net.adapter("name", ip="10.0.0.1") # NIC / host / server
100
+ switch = net.switch("name", ports=16, mode="store-and-forward")
101
+ hub = net.hub("name", ports=8)
102
+ broker = net.mqtt_broker("name", ip="10.0.2.1") # MQTT message broker
103
+ net.link(a, b, speed=1_000, length=10) # Mb/s and metres
104
+ ```
105
+
106
+ Multiple switches can be chained to model hierarchical topologies:
107
+
108
+ ```python
109
+ core_sw = net.switch("core-sw", ports=16, mode="store-and-forward")
110
+ edge_sw = net.switch("edge-sw", ports=8, mode="store-and-forward")
111
+ net.link(edge_sw, core_sw, speed=1_000, length=5) # inter-switch uplink
112
+ ```
113
+
114
+ ### Traffic
115
+
116
+ ```python
117
+ # Basic Ethernet send
118
+ src.sends(to=dst, rate=8_000, size=1_518, pattern="constant")
119
+
120
+ # Delayed start (useful for staggered scenarios)
121
+ src.sends(to=dst, rate=8_000, size=1_518, pattern="constant", delay_ms=5_000)
122
+
123
+ # MQTT publish (sensor-style, constant-rate)
124
+ sensor.publishes(to=broker, topic="plant/temp", rate=1.0, payload=20, qos=1)
125
+ sensor.publishes(to=broker, topic="plant/temp", rate=1.0, payload=20, qos=0, delay_ms=2_000)
126
+
127
+ # Broker topic routing — must be called before simulate()
128
+ broker.routes("plant/temp", to=[server])
129
+ ```
130
+
131
+ | Parameter | Type | Description |
132
+ |---|---|---|
133
+ | `to` | Adapter | Destination adapter |
134
+ | `rate` | float | Frames per second |
135
+ | `size` | int \| `"imix"` | Frame size in bytes, or Internet Mix distribution |
136
+ | `pattern` | str | `"constant"`, `"poisson"`, or `"bursty"` |
137
+ | `delay_ms` | float | Simulation time before this flow starts (default 0) |
138
+
139
+ **`publishes()` parameters**
140
+
141
+ | Parameter | Type | Description |
142
+ |---|---|---|
143
+ | `to` | MQTTBroker | Target broker |
144
+ | `topic` | str | MQTT topic string |
145
+ | `rate` | float | Messages per second (default 1.0) |
146
+ | `payload` | int | Payload bytes (default 20) |
147
+ | `qos` | int | 0 = fire-and-forget, 1 = PUBACK acknowledgement |
148
+ | `delay_ms` | float | Simulation time before publishing starts (default 0) |
149
+
150
+ **Traffic patterns**
151
+
152
+ | Pattern | Description |
153
+ |---|---|
154
+ | `"constant"` | Fixed inter-frame gap — models a saturated link |
155
+ | `"poisson"` | Exponentially distributed gaps — models random/bursty traffic |
156
+ | `"bursty"` | Pareto-distributed burst lengths — models ON/OFF sources |
157
+
158
+ **Frame sizes**
159
+
160
+ | Value | Description |
161
+ |---|---|
162
+ | integer | Fixed size in bytes (e.g. `512`, `1_518`) |
163
+ | `"imix"` | 40 % × 64 B, 57 % × 594 B, 3 % × 1 518 B |
164
+
165
+ ### Observations
166
+
167
+ ```python
168
+ net.observe(metric, on=node, every=interval_ms)
169
+ ```
170
+
171
+ | Metric | Unit | Observed on |
172
+ |---|---|---|
173
+ | `throughput` | Mb/s | Adapter |
174
+ | `latency` | µs | Adapter |
175
+ | `frame_loss` | % | Adapter |
176
+ | `bytes_sent` | MB | Adapter (sender) |
177
+ | `bytes_received` | MB | Adapter (receiver) |
178
+ | `queue_depth` | frames | Switch |
179
+ | `utilization` | % | Any node |
180
+ | `collision_rate` | /s | Hub |
181
+ | `broker_queue` | msgs | MQTTBroker |
182
+
183
+ ### Simulation
184
+
185
+ ```python
186
+ result = net.simulate(duration=30_000) # headless — silent, fastest
187
+ result = net.simulate(duration=30_000, text=True) # rich text dashboard in terminal
188
+ result = net.simulate(duration=30_000, live=True) # full Dear PyGui desktop window
189
+
190
+ result.report() # print summary table (avg / min / max per metric)
191
+ result.plot() # open static dashboard for a completed result
192
+
193
+ # Export metric time-series (format inferred from extension)
194
+ result.export("results.csv") # long CSV: time_ms, metric, value
195
+ result.export("results.json") # JSON dict of lists-of-pairs
196
+ result.export("out.csv", format="csv") # explicit format override
197
+ ```
198
+
199
+ **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.
200
+
201
+ ### Quick demo
202
+
203
+ ```bash
204
+ smolpy demo # built-in 3-client scenario, text mode, no script needed
205
+ ```
206
+
207
+ ---
208
+
209
+ ## MQTT publish-subscribe
210
+
211
+ SMOLPy models application-layer MQTT traffic on top of the standard Ethernet/IP/TCP wire model.
212
+
213
+ ### What is modelled
214
+
215
+ - **Publisher adapters** call `publishes()` to emit periodic MQTT PUBLISH frames at a fixed rate toward an `MQTTBroker` node.
216
+ - **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.
217
+ - **Subscriber adapters** receive forwarded copies just like normal Ethernet frames; all standard metrics (`throughput`, `latency`, `bytes_received`) apply.
218
+ - **`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.
219
+
220
+ ### Frame size formula
221
+
222
+ ```
223
+ frame_size = 54 (Ethernet+IPv4+TCP) + 2 (MQTT fixed header) + 2 (topic-length field) + len(topic) + (2 if qos > 0 else 0) + payload_bytes
224
+ ```
225
+
226
+ 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).
227
+
228
+ ### Dashboard
229
+
230
+ `MQTTBroker` nodes appear as **purple** circles in the topology panel.
231
+
232
+ ---
233
+
234
+ ## Simulation engine
235
+
236
+ - **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.
237
+ - **Store-and-forward model** — transmission delay + propagation delay per hop.
238
+ - **Queuing** — each link direction is an independent SimPy Store; `queue_depth` reports buffered frames at the switch's outbound ports.
239
+ - **Traffic shaping** — constant, Poisson, and Pareto-burst patterns; IMIX frame-size distribution.
240
+ - **Live mode** — simulation runs in 200 chunks (~8 s total wall time); the dashboard reads shared metric arrays between chunks via Python's GIL.
241
+
242
+ ---
243
+
244
+ ## Examples
245
+
246
+ See [`examples/README.md`](examples/README.md) for eight ready-to-run scenarios covering single-switch saturation, oversubscription, two-tier access bottlenecks, and MQTT publish-subscribe.
247
+
248
+ ```bash
249
+ uv run smolpy run examples/example.py
250
+ uv run smolpy run examples/example_two_tier.py
251
+ ```