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.
- smolpy-0.1.0/.github/workflows/ci.yml +45 -0
- smolpy-0.1.0/.github/workflows/publish.yml +48 -0
- smolpy-0.1.0/.gitignore +27 -0
- smolpy-0.1.0/PKG-INFO +268 -0
- smolpy-0.1.0/README.md +251 -0
- smolpy-0.1.0/examples/README.md +341 -0
- smolpy-0.1.0/examples/example.py +22 -0
- smolpy-0.1.0/examples/example_10_clients.py +68 -0
- smolpy-0.1.0/examples/example_12_clients.py +71 -0
- smolpy-0.1.0/examples/example_20_clients.py +71 -0
- smolpy-0.1.0/examples/example_file_transfer.py +82 -0
- smolpy-0.1.0/examples/example_mqtt.py +79 -0
- smolpy-0.1.0/examples/example_staggered_transfer.py +80 -0
- smolpy-0.1.0/examples/example_two_tier.py +98 -0
- smolpy-0.1.0/pyproject.toml +49 -0
- smolpy-0.1.0/src/smolpy/__init__.py +4 -0
- smolpy-0.1.0/src/smolpy/cli.py +178 -0
- smolpy-0.1.0/src/smolpy/dsl/__init__.py +7 -0
- smolpy-0.1.0/src/smolpy/dsl/adapter.py +89 -0
- smolpy-0.1.0/src/smolpy/dsl/hub.py +11 -0
- smolpy-0.1.0/src/smolpy/dsl/link.py +27 -0
- smolpy-0.1.0/src/smolpy/dsl/mqtt_broker.py +23 -0
- smolpy-0.1.0/src/smolpy/dsl/network.py +168 -0
- smolpy-0.1.0/src/smolpy/dsl/node.py +18 -0
- smolpy-0.1.0/src/smolpy/dsl/observation.py +26 -0
- smolpy-0.1.0/src/smolpy/dsl/switch.py +17 -0
- smolpy-0.1.0/src/smolpy/sim/__init__.py +0 -0
- smolpy-0.1.0/src/smolpy/sim/engine.py +580 -0
- smolpy-0.1.0/src/smolpy/viz/__init__.py +0 -0
- smolpy-0.1.0/src/smolpy/viz/dashboard.py +464 -0
- smolpy-0.1.0/src/smolpy/viz/text_dashboard.py +134 -0
- smolpy-0.1.0/tests/__init__.py +0 -0
- smolpy-0.1.0/tests/test_dsl.py +45 -0
- smolpy-0.1.0/tests/test_simulation.py +242 -0
- 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
|
smolpy-0.1.0/.gitignore
ADDED
|
@@ -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
|
+
```
|