edgeengine-aware 0.4.0__py3-none-any.whl
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.
- edgeengine_aware/__init__.py +93 -0
- edgeengine_aware/actions.py +194 -0
- edgeengine_aware/agriculture.py +191 -0
- edgeengine_aware/application.py +222 -0
- edgeengine_aware/communication.py +99 -0
- edgeengine_aware/config.py +942 -0
- edgeengine_aware/deployment.py +411 -0
- edgeengine_aware/domains.py +284 -0
- edgeengine_aware/energy.py +195 -0
- edgeengine_aware/env.py +441 -0
- edgeengine_aware/indoor.py +186 -0
- edgeengine_aware/industrial.py +192 -0
- edgeengine_aware/interfaces.py +171 -0
- edgeengine_aware/metrics.py +131 -0
- edgeengine_aware/observation.py +490 -0
- edgeengine_aware/policies.py +285 -0
- edgeengine_aware/process.py +136 -0
- edgeengine_aware/rendering.py +199 -0
- edgeengine_aware/reward.py +92 -0
- edgeengine_aware/rl.py +368 -0
- edgeengine_aware/scenarios.py +131 -0
- edgeengine_aware/sensing.py +45 -0
- edgeengine_aware/traces.py +636 -0
- edgeengine_aware-0.4.0.dist-info/METADATA +739 -0
- edgeengine_aware-0.4.0.dist-info/RECORD +28 -0
- edgeengine_aware-0.4.0.dist-info/WHEEL +5 -0
- edgeengine_aware-0.4.0.dist-info/licenses/LICENSE +21 -0
- edgeengine_aware-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edgeengine-aware
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: EdgeEngine AWARE: application- and energy-aware simulation environment for reinforcement learning in energy-harvesting Edge IoT systems.
|
|
5
|
+
Author-email: Riccardo Berta <riccardo.berta@unige.it>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/measurify/EdgeEngine-AWARE
|
|
8
|
+
Project-URL: Repository, https://github.com/measurify/EdgeEngine-AWARE
|
|
9
|
+
Project-URL: Documentation, https://github.com/measurify/EdgeEngine-AWARE#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/measurify/EdgeEngine-AWARE/issues
|
|
11
|
+
Keywords: reinforcement-learning,gymnasium,iot,edge,energy-harvesting,tinyml,smart-agriculture,indoor-air-quality,predictive-maintenance
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: System :: Hardware
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: gymnasium>=0.29
|
|
26
|
+
Requires-Dist: matplotlib>=3.7
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: jupyter; extra == "dev"
|
|
30
|
+
Requires-Dist: nbformat; extra == "dev"
|
|
31
|
+
Requires-Dist: nbconvert; extra == "dev"
|
|
32
|
+
Provides-Extra: rl
|
|
33
|
+
Requires-Dist: stable-baselines3>=2.0; extra == "rl"
|
|
34
|
+
Requires-Dist: sb3-contrib>=2.0; extra == "rl"
|
|
35
|
+
Requires-Dist: torch; extra == "rl"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# EdgeEngine AWARE
|
|
39
|
+
|
|
40
|
+
[](https://github.com/measurify/EdgeEngine-AWARE/actions/workflows/ci.yml)
|
|
41
|
+
[](https://pypi.org/project/edgeengine-aware/)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
**Application- and Energy-Aware Simulation Environment for Reinforcement Learning in
|
|
45
|
+
Energy-Harvesting Edge IoT Systems**
|
|
46
|
+
|
|
47
|
+
EdgeEngine AWARE is a Python simulator, packaged as a standard
|
|
48
|
+
[Gymnasium](https://gymnasium.farama.org/) environment, of a small **energy-harvesting
|
|
49
|
+
embedded node** that monitors a physical quantity and reports it to a remote application over
|
|
50
|
+
a low-power radio link. It exists to **train and compare decision policies** — hand-written
|
|
51
|
+
rules or reinforcement-learning (RL) agents — that decide, every 15 minutes, how to spend a
|
|
52
|
+
very small and uncertain energy budget. Three application domains are built in: a
|
|
53
|
+
solar-powered soil-moisture node in a field (the reference case used throughout this README),
|
|
54
|
+
a CO₂ node in a classroom powered by the ceiling lights, and a machine-monitoring node powered
|
|
55
|
+
by the heat of the motor it watches (§10).
|
|
56
|
+
|
|
57
|
+
The project also contains what is needed *after* training: a frozen export format for
|
|
58
|
+
policies, a firmware-style controller, a C runtime that reproduces the Python decision
|
|
59
|
+
logic on a microcontroller, and a way to replay recorded weather data through the same
|
|
60
|
+
simulator.
|
|
61
|
+
|
|
62
|
+
This README is written for a reader who knows Python but not necessarily RL, IoT
|
|
63
|
+
hardware or radio engineering. Terms in *italics* on first use are defined in the
|
|
64
|
+
[glossary](#glossary) at the end.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Contents
|
|
69
|
+
|
|
70
|
+
1. [The problem in plain words](#1-the-problem-in-plain-words)
|
|
71
|
+
2. [The one design rule](#2-the-one-design-rule-the-policy-must-not-cheat)
|
|
72
|
+
3. [Installation](#3-installation)
|
|
73
|
+
4. [Quick start](#4-quick-start)
|
|
74
|
+
5. [What happens in one step](#5-what-happens-in-one-step)
|
|
75
|
+
6. [Observation, action, reward, metrics](#6-observation-action-reward-metrics)
|
|
76
|
+
7. [Configuration, scenarios and domain randomisation](#7-configuration-scenarios-and-domain-randomisation)
|
|
77
|
+
8. [Built-in policies](#8-built-in-policies)
|
|
78
|
+
9. [Training and comparing RL agents](#9-training-and-comparing-rl-agents)
|
|
79
|
+
10. [Three application domains](#10-three-application-domains)
|
|
80
|
+
11. [Recorded traces instead of models](#11-recorded-traces-instead-of-models)
|
|
81
|
+
12. [From simulation to a microcontroller](#12-from-simulation-to-a-microcontroller)
|
|
82
|
+
13. [Notebooks, scripts and tools](#13-notebooks-scripts-and-tools)
|
|
83
|
+
14. [Repository layout](#14-repository-layout)
|
|
84
|
+
15. [Tests](#15-tests)
|
|
85
|
+
16. [Documentation](#16-documentation)
|
|
86
|
+
17. [What is not modelled](#17-what-is-not-modelled)
|
|
87
|
+
18. [Glossary](#glossary)
|
|
88
|
+
19. [Citation](#citation)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 1. The problem in plain words
|
|
93
|
+
|
|
94
|
+
Imagine a matchbox-sized device stuck in the soil of a field:
|
|
95
|
+
|
|
96
|
+
* It has a **soil-moisture sensor**. Reading it costs energy. A quick, cheap reading is
|
|
97
|
+
noisy; a careful reading (longer warm-up, averaging) is accurate but costs six times more.
|
|
98
|
+
* It has a **radio** that can send the latest reading to a gateway several hundred metres
|
|
99
|
+
away. Sending costs energy too, and the radio has three settings: *fast* (cheap, but the
|
|
100
|
+
message often gets lost when the link is poor), *standard*, and *robust* (expensive, almost
|
|
101
|
+
always gets through). Whether a message arrived is known from an acknowledgement (*ACK*).
|
|
102
|
+
* It has a **small battery** (300 J, about a 25 mAh cell) charged by a **tiny solar cell**
|
|
103
|
+
that yields about 55 J on an average day and much less on a cloudy one. Just staying alive
|
|
104
|
+
(sleeping, keeping the clock running) costs about 17 J per day.
|
|
105
|
+
* A remote **application** (think: the farmer's dashboard) wants to know the soil moisture
|
|
106
|
+
— accurately, and quickly when it matters, i.e. when the soil is getting dry and
|
|
107
|
+
irrigation decisions are due. It tells the node how urgent its needs are (a *priority*
|
|
108
|
+
level: routine, elevated, urgent).
|
|
109
|
+
|
|
110
|
+
Every 15 minutes the node wakes up and must decide two things: **how well to look** (do not
|
|
111
|
+
sense / cheap reading / accurate reading) and **whether and how loudly to speak** (do not
|
|
112
|
+
transmit / transmit fast / standard / robust). The score it is judged on is *how right and
|
|
113
|
+
how fresh the application's picture of the field is*, minus the energy it spends and minus
|
|
114
|
+
penalties for draining the battery. This is a sequential decision problem under uncertainty
|
|
115
|
+
— exactly what reinforcement learning is for — but also one where a well-designed set of
|
|
116
|
+
rules does very well, which is why the project ships both and compares them honestly.
|
|
117
|
+
|
|
118
|
+
## 2. The one design rule: the policy must not cheat
|
|
119
|
+
|
|
120
|
+
A simulator knows everything: the true soil moisture, tomorrow's clouds, whether the next
|
|
121
|
+
radio message will get through. A real device knows almost nothing of this. If a policy is
|
|
122
|
+
trained on information that the device will not have, it will not work in the field.
|
|
123
|
+
|
|
124
|
+
EdgeEngine AWARE therefore separates three kinds of quantities and enforces the separation
|
|
125
|
+
in code and tests:
|
|
126
|
+
|
|
127
|
+
| kind | examples | who may use it |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| **hidden ground truth** (simulator only) | true soil moisture, true irradiance, radio channel state, future rain | the world dynamics, the reward, the plots |
|
|
130
|
+
| **measurable on the node** | stored energy, harvested power in the last interval, the noisy sensor reading and its age, whether the last message was acknowledged, the priority received from the application | the node, i.e. `NodeState` |
|
|
131
|
+
| **policy input** | 18 numbers in [0, 1], all computed from the measurable quantities | the policy |
|
|
132
|
+
|
|
133
|
+
The code that turns measurable quantities into the 18-number *observation*
|
|
134
|
+
(`observation.py`), and the code that decides which requested action is affordable
|
|
135
|
+
(`actions.plan_execution`), are **shared** between the simulator (`env.py`), the
|
|
136
|
+
firmware-style controller (`deployment.NodeController`) and the C runtime (`firmware/`). The
|
|
137
|
+
same policy object therefore runs unchanged in all three places. A test
|
|
138
|
+
(`tests/test_env.py::test_observation_contains_no_privileged_information`) changes every
|
|
139
|
+
hidden variable and checks that the observation does not move.
|
|
140
|
+
|
|
141
|
+
## 3. Installation
|
|
142
|
+
|
|
143
|
+
Requirements: Python 3.11 or newer.
|
|
144
|
+
|
|
145
|
+
**To use the environment** (train or evaluate policies in your own code), install it from
|
|
146
|
+
PyPI. The package contains the simulator only — about 100 kB, depending on `numpy`,
|
|
147
|
+
`gymnasium` and `matplotlib`:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install edgeengine-aware
|
|
151
|
+
pip install "edgeengine-aware[rl]" # adds stable-baselines3, sb3-contrib, torch (CPU is enough) for RL training
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**To work with the repository** (notebooks, weather traces, trained policy bundles, the C
|
|
155
|
+
runtime, the tests), clone it and install in "editable" mode, so that edits to the source
|
|
156
|
+
are picked up without reinstalling. A C compiler (`cc`/`gcc`/`clang`) is optional and only
|
|
157
|
+
needed for the firmware equivalence tests.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
git clone https://github.com/measurify/EdgeEngine-AWARE.git
|
|
161
|
+
cd EdgeEngine-AWARE
|
|
162
|
+
python -m venv .venv && source .venv/bin/activate # optional but recommended
|
|
163
|
+
pip install -e ".[dev]" # numpy, gymnasium, matplotlib + pytest, jupyter, nbformat, nbconvert
|
|
164
|
+
pip install -e ".[rl]" # only for RL training
|
|
165
|
+
pytest # 114 tests, 15-60 s depending on the installed extras
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Everything outside the `edgeengine_aware/` package — `data/traces/` (recorded weather),
|
|
169
|
+
`examples/bundles/` (trained policies), `examples/*.ipynb`, `firmware/`, `tools/` — is only
|
|
170
|
+
in the repository, not in the pip package. Releases are tagged `vX.Y.Z` on GitHub and
|
|
171
|
+
published to PyPI automatically (`.github/workflows/publish.yml`).
|
|
172
|
+
|
|
173
|
+
## 4. Quick start
|
|
174
|
+
|
|
175
|
+
**Create the environment and take one step.** `gym.make` returns the environment wrapped in
|
|
176
|
+
Gymnasium's standard wrappers; `env.unwrapped` is the underlying `EdgeEngineAwareEnv`.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
import gymnasium as gym
|
|
180
|
+
import edgeengine_aware as ea # importing the package registers "EdgeEngineAware-v0"
|
|
181
|
+
|
|
182
|
+
env = gym.make("EdgeEngineAware-v0")
|
|
183
|
+
obs, info = env.reset(seed=0) # obs: 18 float32 values in [0, 1]
|
|
184
|
+
obs, reward, terminated, truncated, info = env.step(env.action_space.sample())
|
|
185
|
+
|
|
186
|
+
print(env.unwrapped.obs_builder.describe()) # what each of the 18 numbers means, from code
|
|
187
|
+
print(info["reward_components"]) # utility, energy costs and penalties, separately
|
|
188
|
+
print(info["metrics"]["average_aoi_s"]) # running episode metrics (here: mean age of information, s)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Run the rule-based controller for a whole 7-day episode** and print the metrics. The
|
|
192
|
+
controller needs the *node profile* (the hardware constants: energy per operation, radio link
|
|
193
|
+
budget table) to choose the radio mode; without it, it always uses the standard mode.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
import edgeengine_aware as ea
|
|
197
|
+
from edgeengine_aware.policies import RuleBasedPolicy, run_episode
|
|
198
|
+
|
|
199
|
+
cfg = ea.default_config()
|
|
200
|
+
env = ea.EdgeEngineAwareEnv(cfg, render_mode="human") # "human" opens a live Matplotlib dashboard
|
|
201
|
+
profile = ea.NodeProfile.from_config(cfg)
|
|
202
|
+
result = run_episode(env, RuleBasedPolicy(profile=profile), seed=1, render=True, render_every=96)
|
|
203
|
+
print(env.metrics.summary())
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Typical output (seed 1, default configuration): total reward ≈ 106, 185 transmissions of
|
|
207
|
+
which 160 delivered (86 %), minimum battery level 47 %, no brown-out, mean age of
|
|
208
|
+
information at the application 0.86 h.
|
|
209
|
+
|
|
210
|
+
**Compare the baselines without graphics:**
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
python examples/compare_policies.py --seeds 10
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Open the guided notebook** (environment tour, one episode step by step, metrics, policy
|
|
217
|
+
comparison):
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
jupyter notebook examples/baseline_policy.ipynb
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## 5. What happens in one step
|
|
224
|
+
|
|
225
|
+
One call to `env.step(action)` advances the world by one decision interval (15 minutes by
|
|
226
|
+
default) in this order:
|
|
227
|
+
|
|
228
|
+
1. **Feasibility.** Using only the energy it can read from its battery gauge, the node sets
|
|
229
|
+
aside the always-on consumption of the coming interval and a small brown-out reserve
|
|
230
|
+
(2 % of capacity) and checks which requested operations it can afford. Sensing is served
|
|
231
|
+
first, then transmission. Unaffordable operations are *rejected*: not executed, no energy
|
|
232
|
+
spent, a small penalty. A transmission with nothing to send (no stored reading) is also
|
|
233
|
+
rejected. See `docs/actions.md`.
|
|
234
|
+
2. **Sensing.** If requested and affordable, the sensor samples the *true* soil moisture with
|
|
235
|
+
the noise of the chosen level; the reading is stored on the node together with its
|
|
236
|
+
timestamp and a quality tag.
|
|
237
|
+
3. **Transmission.** If requested and affordable, the latest stored reading is sent with the
|
|
238
|
+
chosen radio mode. Energy is spent whether or not the message arrives. Delivery is random,
|
|
239
|
+
with a probability that depends on the radio mode and on the current (hidden) state of the
|
|
240
|
+
link. On delivery the application updates its picture of the field; the node learns the
|
|
241
|
+
outcome from the acknowledgement.
|
|
242
|
+
4. **Energy update.** Consumption (always-on + sensing + transmission) is drawn from the
|
|
243
|
+
battery first, then the energy harvested during the interval is added, capped at the
|
|
244
|
+
capacity (excess is counted as *wasted*). If the always-on load itself cannot be served
|
|
245
|
+
the node *browns out*.
|
|
246
|
+
5. **World update.** Clock, soil, weather, radio channel and application priority move to
|
|
247
|
+
the next interval.
|
|
248
|
+
6. **Reward.** How good the application's picture now is (compared with the new truth), plus
|
|
249
|
+
a bonus for the packet that improved it, minus energy costs and penalties.
|
|
250
|
+
7. **Observation.** The node's bookkeeping is refreshed with measurable quantities only and
|
|
251
|
+
the 18-number vector for the next decision is built.
|
|
252
|
+
|
|
253
|
+
The full timeline, with the formulas, is in `docs/modeling.md`.
|
|
254
|
+
|
|
255
|
+
## 6. Observation, action, reward, metrics
|
|
256
|
+
|
|
257
|
+
**Observation** — `Box(0, 1, (18,), float32)`. Every component is normalised to [0, 1] and
|
|
258
|
+
comes from something a microcontroller can measure or compute:
|
|
259
|
+
|
|
260
|
+
| group | components |
|
|
261
|
+
|---|---|
|
|
262
|
+
| energy | battery *SoC*; harvested power in the last interval; smoothed recent harvest (*EWMA*) |
|
|
263
|
+
| time | time of day as sine and cosine |
|
|
264
|
+
| stored reading | value; quality tag (0 none / 0.2 cheap / 0.8 accurate); age |
|
|
265
|
+
| communication | time since the last acknowledged uplink; the node's own estimate of how old the application's information is; the value it last delivered; smoothed fraction of recent uplinks acknowledged (*link quality*); *path-loss* estimate from the last ACK |
|
|
266
|
+
| application | requested priority (0 / 0.5 / 1) |
|
|
267
|
+
| node-side judgement | *importance* of the stored reading (how close it is to the stress thresholds) |
|
|
268
|
+
| hardware profile | energy of a cheap reading, of an accurate reading, of one standard uplink, all relative to the battery capacity |
|
|
269
|
+
|
|
270
|
+
Full table with normalisation formulas and hardware sources: `docs/observation.md`.
|
|
271
|
+
|
|
272
|
+
**Action** — `MultiDiscrete([3, 4])`, i.e. a pair of integers:
|
|
273
|
+
|
|
274
|
+
| component | values |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `sensing_level` | 0 = do not sense, 1 = cheap reading (0.10 J, noise σ 0.04), 2 = accurate reading (0.60 J, σ 0.01) |
|
|
277
|
+
| `transmit` | 0 = do not transmit, 1 = fast mode (0.30 J), 2 = standard (0.60 J), 3 = robust (1.20 J) |
|
|
278
|
+
|
|
279
|
+
For algorithms that need a single integer (DQN, tabular methods) `rl.FlatActionWrapper`
|
|
280
|
+
exposes the same choices as `Discrete(12)` (`12 = 3 × 4`; it grows if you configure more
|
|
281
|
+
radio modes). See `docs/actions.md`.
|
|
282
|
+
|
|
283
|
+
**Reward** — a sum of separately reported terms, available in `info["reward_components"]`:
|
|
284
|
+
|
|
285
|
+
| term | sign | meaning |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| tracking utility | + | every step: how close the application's last value is to the true moisture, weighted up to 3× near the stress thresholds |
|
|
288
|
+
| packet bonus | + | on delivery: signed improvement of the application's error, plus a bonus for reporting a recent rain/irrigation event |
|
|
289
|
+
| energy costs | − | 0.1 per joule spent on sensing and on transmission |
|
|
290
|
+
| staleness | − | grows with the age of the application's information, weighted 1/2/4 by priority, saturating after 6 h |
|
|
291
|
+
| battery risk | − | quadratic penalty below 30 % SoC; 2.0 per step of brown-out |
|
|
292
|
+
| rejected actions, wasted harvest | − | 0.2 per rejected operation; 0.02 per joule of harvest that did not fit in the battery |
|
|
293
|
+
|
|
294
|
+
Orders of magnitude for a 7-day episode with the defaults: utility 100–140, energy costs
|
|
295
|
+
15–30, a policy that never reports loses ~130 to staleness, an always-on policy loses several
|
|
296
|
+
hundred to battery penalties. Formulas and constants: `docs/modeling.md` §3.
|
|
297
|
+
|
|
298
|
+
**Metrics** — `env.metrics` (an `EpisodeMetrics`) accumulates per episode: harvested /
|
|
299
|
+
consumed / always-on / sensing / communication / wasted energy [J]; number of readings,
|
|
300
|
+
accurate readings, transmissions, deliveries (also per radio mode), rejected operations;
|
|
301
|
+
average and minimum SoC, fraction of steps below 30 % SoC, brown-out events; average and
|
|
302
|
+
maximum *age of information* (AoI); total utility and reward. `env.metrics.summary()` prints
|
|
303
|
+
them, `info["metrics"]` carries the same dictionary at every step, and `env.log` (an
|
|
304
|
+
`EpisodeLog`) keeps the per-step history used by the plots.
|
|
305
|
+
|
|
306
|
+
**Other `info` keys.** `ground_truth` (hidden state, for evaluation only — never feed it to
|
|
307
|
+
a policy), `rejected` (list of reasons), `tx_mode`, `tx_success`, `app_priority` (what the
|
|
308
|
+
application wants now) vs `node_priority` (what the node has been told), `utility_breakdown`,
|
|
309
|
+
`observation_names`.
|
|
310
|
+
|
|
311
|
+
## 7. Configuration, scenarios and domain randomisation
|
|
312
|
+
|
|
313
|
+
Everything is a plain Python dataclass; the environment code never needs to change.
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
import edgeengine_aware as ea
|
|
317
|
+
|
|
318
|
+
cfg = ea.default_config()
|
|
319
|
+
cfg.time.timestep_s = 600 # 10-minute decisions
|
|
320
|
+
cfg.time.episode_days = 14
|
|
321
|
+
cfg.storage.capacity_j = 800
|
|
322
|
+
cfg.storage.initial_soc_range = (0.3, 0.9) # random initial charge at every reset
|
|
323
|
+
cfg.mcu.baseline_power_w = 120e-6
|
|
324
|
+
cfg.harvesting.max_power_w = 0.008 # panel peak power [W] at clear-sky noon
|
|
325
|
+
cfg.observation.harvest_ref_power_w = 0.005 # keep the harvest normalisation consistent with the panel
|
|
326
|
+
cfg.harvesting.clearness_mean = 0.5 # cloudier climate
|
|
327
|
+
cfg.communication = ea.config.single_mode_radio(energy_j=0.9) # a radio with one setting -> binary transmit action
|
|
328
|
+
cfg.communication.priority_update_mode = "on_uplink" # priority only arrives with an ACK (LoRaWAN class A)
|
|
329
|
+
env = ea.EdgeEngineAwareEnv(cfg)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Two things to know: `cfg.validate()` is called by the environment and rejects inconsistent
|
|
333
|
+
settings with a clear message; and the observation normalisation constants
|
|
334
|
+
(`cfg.observation`) are part of the hardware profile — if you change the panel or the
|
|
335
|
+
battery, check that `harvest_ref_power_w` (the power that maps to 1.0) still makes sense.
|
|
336
|
+
|
|
337
|
+
**Scenarios** (`edgeengine_aware.scenarios`) are named configurations used as the benchmark
|
|
338
|
+
protocol. Each stresses one aspect:
|
|
339
|
+
|
|
340
|
+
| name | what changes with respect to `default` | what it tests |
|
|
341
|
+
|---|---|---|
|
|
342
|
+
| `default` | — (mixed weather, clearness 0.7 ± 0.2) | the nominal regime |
|
|
343
|
+
| `cloudy_week` | clearness 0.35 ± 0.15, more cloud variability, battery starts at 40 % → ~30 J/day (55 % of default) | rationing energy over several dark days |
|
|
344
|
+
| `tiny_battery` | 150 J instead of 300 J | smoothing consumption with a small buffer |
|
|
345
|
+
| `lossy_link` | +6 dB mean path loss, slower fading → standard mode delivers ~40 %, robust ~85 % | choosing the radio mode from the link estimate |
|
|
346
|
+
| `drought` | evapotranspiration ×2, almost no rain, irrigation delayed 18 h, drier start | tracking the approach to the stress thresholds |
|
|
347
|
+
| `demanding_application` | 1.5 external campaigns/day of 2–8 h, half of them urgent, "elevated" after 4 h of silence | following the priority, saving in between |
|
|
348
|
+
|
|
349
|
+
`ea.get_scenario(name, randomize=False)` returns the configuration; `rl.make_env(name)` the
|
|
350
|
+
environment; `rl.make_env("mixed")` (or a list of names) an environment that draws a scenario
|
|
351
|
+
at every reset (`MixedScenarioEnv`) — the recommended training distribution.
|
|
352
|
+
|
|
353
|
+
**Domain randomisation** (`cfg.randomization`, off by default) perturbs the physical
|
|
354
|
+
parameters at every `reset()` so that a trained policy does not over-fit one exact device:
|
|
355
|
+
sensor noise ×[0.7, 1.5], sensing energy ×[0.8, 1.3], radio energies ×[0.8, 1.3], mean path
|
|
356
|
+
loss ±3 dB, solar intensity ×[0.6, 1.2], cloud variability ×[0.5, 1.5], battery capacity
|
|
357
|
+
×[0.8, 1.2], always-on power ×[0.7, 1.5] (all uniform); in the domains with a weekly schedule
|
|
358
|
+
(§10) the weekday on which the episode starts is drawn at random too. The randomised energy costs are what
|
|
359
|
+
the node reports in its observation, exactly as a real device would report its own measured
|
|
360
|
+
profile. Enable with `cfg.randomization.enabled = True` or `get_scenario(name, randomize=True)`.
|
|
361
|
+
|
|
362
|
+
## 8. Built-in policies
|
|
363
|
+
|
|
364
|
+
All policies implement the tiny `Policy` protocol — `act(observation) -> action` and
|
|
365
|
+
`reset()` — and only ever see the 18-number observation, so they run identically in the
|
|
366
|
+
simulator, in the deployment controller and (for the rule-based one) in C.
|
|
367
|
+
|
|
368
|
+
| policy | what it does |
|
|
369
|
+
|---|---|
|
|
370
|
+
| `RuleBasedPolicy(params, profile)` | the interpretable reference controller, see below |
|
|
371
|
+
| `PeriodicPolicy(period_steps, sensing_level, tx)` | the classic duty-cycled firmware: sense and transmit every *n* steps, regardless of energy or application |
|
|
372
|
+
| `RandomPolicy(seed)` | uniform random actions — a lower bound |
|
|
373
|
+
| `AlwaysOnPolicy()` | accurate reading and transmission at every step — an upper bound on information, a disaster for the battery |
|
|
374
|
+
|
|
375
|
+
**The rule-based controller** (`policies.RuleBasedPolicy`, parameters in `RuleBasedParams`)
|
|
376
|
+
applies these rules in order, all on the normalised observation:
|
|
377
|
+
|
|
378
|
+
1. *Deep economy* — battery below 20 %: one accurate report every 8 h (every 30 min if the
|
|
379
|
+
application is urgent), nothing else.
|
|
380
|
+
2. *Retry* — a fresh accurate reading (younger than 18 min) that was not acknowledged is
|
|
381
|
+
re-sent, unless the link looks down (link quality < 0.5).
|
|
382
|
+
3. *Scheduled report* — when the estimated age of the application's information exceeds the
|
|
383
|
+
report interval: 2 h / 1 h / 30 min for routine / elevated / urgent priority. In *economy
|
|
384
|
+
mode* (battery below 50 %, unless the priority is urgent) the interval is doubled; at
|
|
385
|
+
routine priority it is shortened to 75 % when the battery is above 70 % or the sun is
|
|
386
|
+
strong. A report is an accurate reading plus a transmission.
|
|
387
|
+
4. *Event report* — the stored reading differs from the last delivered one by more than 0.08,
|
|
388
|
+
or is near a stress threshold and differs by more than 0.03.
|
|
389
|
+
5. *Check* — outside economy mode, a cheap reading every hour (no transmission).
|
|
390
|
+
6. Otherwise sleep.
|
|
391
|
+
|
|
392
|
+
Radio mode: the cheapest mode whose expected link margin, computed from the node's path-loss
|
|
393
|
+
estimate and the link-budget table in the profile, is at least 4 dB; one mode more robust
|
|
394
|
+
when recent uplinks failed (link quality < 0.7); the standard mode before the first estimate.
|
|
395
|
+
|
|
396
|
+
## 9. Training and comparing RL agents
|
|
397
|
+
|
|
398
|
+
The environment follows the Gymnasium API, so any library works. The project uses
|
|
399
|
+
[Stable-Baselines3](https://stable-baselines3.readthedocs.io/) (SB3): PPO and A2C accept the
|
|
400
|
+
`MultiDiscrete` action space directly; DQN needs `rl.FlatActionWrapper`. No RL algorithm is
|
|
401
|
+
implemented inside the package on purpose — the package is the benchmark, the notebook is
|
|
402
|
+
the experiment.
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
import edgeengine_aware as ea
|
|
406
|
+
from stable_baselines3 import PPO
|
|
407
|
+
from stable_baselines3.common.vec_env import DummyVecEnv
|
|
408
|
+
from edgeengine_aware.rl import make_env_fn, SB3Policy, evaluate, summarize
|
|
409
|
+
from edgeengine_aware.scenarios import SCENARIOS
|
|
410
|
+
|
|
411
|
+
# train on a mixture of all scenarios with domain randomisation, 8 parallel copies
|
|
412
|
+
vec_env = DummyVecEnv([make_env_fn("mixed", randomize=True, seed=i) for i in range(8)])
|
|
413
|
+
model = PPO("MlpPolicy", vec_env, policy_kwargs=dict(net_arch=[64, 64]), device="cpu").learn(2_000_000)
|
|
414
|
+
|
|
415
|
+
# evaluate on every scenario, 20 held-out seeds, against the rule-based controller
|
|
416
|
+
profile = ea.NodeProfile.from_config(ea.default_config())
|
|
417
|
+
rows = evaluate(
|
|
418
|
+
{"PPO": lambda: SB3Policy(model), "rule-based": lambda: ea.RuleBasedPolicy(profile=profile)},
|
|
419
|
+
scenarios=SCENARIOS, seeds=range(1000, 1020),
|
|
420
|
+
)
|
|
421
|
+
print(summarize(rows, "reward")) # {scenario: {policy: (mean, std)}}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Evaluation protocol** (`rl.evaluate`): deterministic policies, 7-day episodes, the six
|
|
425
|
+
scenarios with nominal (non-randomised) physics, seeds ≥ 1000 that training never uses; one
|
|
426
|
+
`EvalRow` per episode with reward, utility, energy, counts, min SoC, brown-outs, AoI and the
|
|
427
|
+
reward components. `evaluate(..., envs={label: env})` evaluates on ready-made environments
|
|
428
|
+
instead (e.g. trace-driven ones, §11, or other domains, §10).
|
|
429
|
+
|
|
430
|
+
**What we found** (`examples/train_rl.ipynb`, details in the notebook): with 1–2 M training
|
|
431
|
+
steps PPO learns a sensible policy — including using the robust radio mode when the link is
|
|
432
|
+
poor and never the fast one — but the link-aware rule-based controller is as good or better
|
|
433
|
+
on the nominal, lossy-link and small-battery scenarios; PPO wins on the energy-limited
|
|
434
|
+
`cloudy_week`. With **5 M steps** (three seeds, `examples/run_long_training.sh`) PPO reaches
|
|
435
|
+
parity with the rule-based controller on the nominal, small-battery, lossy-link and
|
|
436
|
+
demanding-application scenarios and is clearly ahead on `cloudy_week` (+6) and `drought`
|
|
437
|
+
(+8): 82.0 ± 1.8 vs 81.1 averaged over the six scenarios. DQN is clearly below PPO at every
|
|
438
|
+
budget tried, with a higher variance across seeds, and wastes uplinks in the fast mode. Adding
|
|
439
|
+
memory (4-frame stacking or a recurrent PPO) does not help. Repeating training over several
|
|
440
|
+
seeds (`examples/train_seeds.py`) gives the mean ± std that a paper should quote.
|
|
441
|
+
|
|
442
|
+
## 10. Three application domains
|
|
443
|
+
|
|
444
|
+
The agricultural node is one instance of a general problem: an embedded node with a small
|
|
445
|
+
energy store, a harvester and a duty to report. `edgeengine_aware.domains` makes the hidden
|
|
446
|
+
world pluggable and ships three domains. Whatever the domain, the node sees the **same
|
|
447
|
+
18-number observation** and returns the **same action**: the monitored quantity is normalised
|
|
448
|
+
to [0, 1], and the only new profile constant is on which side the danger lies
|
|
449
|
+
(`critical_is_upper`). The rule-based controller, the RL tooling, the exported bundles and the
|
|
450
|
+
C runtime therefore work unchanged in every domain — and a policy trained in one domain can be
|
|
451
|
+
evaluated in another.
|
|
452
|
+
|
|
453
|
+
| domain | monitored quantity (danger) | energy source | radio | storage / always-on | what drives the week |
|
|
454
|
+
|---|---|---|---|---|---|
|
|
455
|
+
| `agriculture` | soil moisture (low) | solar cell, weather | LoRa-like, 3 spreading factors | 300 J / 200 µW | sun, clouds, rain, irrigation |
|
|
456
|
+
| `indoor_air` | CO₂ of a classroom (high: 1000 / 1500 ppm) | indoor PV under the ceiling lights, plus a window | BLE-like, 3 PHY modes, sub-millijoule uplinks | 60 J / 20 µW | occupancy 08–18 on weekdays: people raise the CO₂ *and* switch the lights on; nights and weekends bring neither energy nor relevance; the CO₂ sensor (20 / 150 mJ per reading) is the energy hog |
|
|
457
|
+
| `industrial` | bearing temperature of a motor (high: 70 / 90 °C) | thermoelectric generator on the warm casing | LoRa-like | 120 J / 200 µW | two shifts 06–22 on weekdays: the machine's heat is both the energy source and the monitored variable; faults make it run hotter until maintenance; weekends are cold and dark |
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
import edgeengine_aware as ea
|
|
461
|
+
from edgeengine_aware.rl import make_env
|
|
462
|
+
|
|
463
|
+
env = ea.EdgeEngineAwareEnv(ea.domain_config("industrial")) # a domain's default configuration
|
|
464
|
+
env = make_env("indoor_air:no_window") # a scenario of another domain ("domain:scenario")
|
|
465
|
+
env = make_env("all") # training mixture over every scenario of every domain
|
|
466
|
+
print(ea.scenario_names("all")) # 6 agricultural + 5 + 5 scenarios
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Each domain has its own stress scenarios (`indoor_air`: `no_window`, `weak_ventilation`,
|
|
470
|
+
`long_hours`, `dim_lights`; `industrial`: `single_shift`, `degrading`, `continuous`,
|
|
471
|
+
`weak_link`). `examples/domains.ipynb` puts the three side by side and asks the cross-domain
|
|
472
|
+
question. First answer: the 5 M-step PPO trained on the agricultural scenarios is at parity
|
|
473
|
+
with the rule-based controller in its own domain but **loses 21 points on the industrial and
|
|
474
|
+
33 on the indoor domain** — it learned the solar day, not only the trade-off.
|
|
475
|
+
`examples/run_cross_domain.sh` trains one policy per domain and a *universal* one on the
|
|
476
|
+
mixture of all three (PPO, 5 M steps, two seeds); the notebook builds the train-domain ×
|
|
477
|
+
test-domain matrix from those runs. Advantage over the rule-based controller, averaged over the
|
|
478
|
+
test domain's scenarios:
|
|
479
|
+
|
|
480
|
+
| trained on ↓ / tested on → | agriculture | indoor_air | industrial |
|
|
481
|
+
|---|---|---|---|
|
|
482
|
+
| agriculture | **+4** | −24 | −14 |
|
|
483
|
+
| indoor_air | −668 | **+51** | −526 |
|
|
484
|
+
| industrial | −9 | +13 | **−3** |
|
|
485
|
+
| all three (universal) | −6 | +46 | −11 |
|
|
486
|
+
|
|
487
|
+
Three things to read in it. There is **no transfer without training** — the indoor policy,
|
|
488
|
+
which learned that uplinks are free (1 mJ over BLE), drains a LoRa node's battery in a day.
|
|
489
|
+
**In its own domain PPO is at parity with the rule on the two physical domains and far ahead
|
|
490
|
+
on the indoor one** (+51; a rule re-tuned for the domain recovers only part of it), where
|
|
491
|
+
relevance is bursty and uplinks cheap, so the learned policy reports every 15 minutes while
|
|
492
|
+
the room is occupied and sleeps otherwise. **The universal policy** keeps most of the indoor
|
|
493
|
+
gain and stays within ~10 points of the rule elsewhere: one network can serve three products
|
|
494
|
+
at a modest cost, with two seeds and 5 M steps. The exported policies are shipped in
|
|
495
|
+
`examples/bundles/` (`ppo_indoor_air.json`, `ppo_industrial.json`, `ppo_universal.json`).
|
|
496
|
+
Models and constants of the two new domains: `docs/modeling.md` §6.
|
|
497
|
+
|
|
498
|
+
## 11. Recorded traces instead of models
|
|
499
|
+
|
|
500
|
+
The weather and soil dynamics of the simulator are *stochastic models*. To check a policy
|
|
501
|
+
against data those models never produced, `edgeengine_aware.traces` replays recorded hourly
|
|
502
|
+
series through the same environment:
|
|
503
|
+
|
|
504
|
+
* `Trace` — a time-indexed table loaded from CSV. Each column is either *instant* (soil
|
|
505
|
+
moisture, temperature: linearly interpolated) or a *preceding-interval mean* (irradiance
|
|
506
|
+
in W/m², precipitation in mm — the convention of weather archives, held constant over the
|
|
507
|
+
hour that ends at the timestamp). Time is in seconds since the first local midnight.
|
|
508
|
+
* `TraceSolarEnergySource` — panel power = `max_power_w × efficiency × irradiance / 1000 W/m²`.
|
|
509
|
+
* `TraceFieldEnvironment` — replays soil moisture (volumetric m³/m³ divided by the field
|
|
510
|
+
capacity, 0.40 by default, to get the simulator's "fraction of field capacity"),
|
|
511
|
+
temperature, humidity and rain as the hidden ground truth.
|
|
512
|
+
* `TraceDrivenEnv(trace, config, start_day=None, harvesting=True, field=True, ...)` — an
|
|
513
|
+
`EdgeEngineAwareEnv` whose harvesting and/or field come from the trace. Each `reset()`
|
|
514
|
+
replays a 7-day window starting at a random admissible day (or at `start_day`, or at
|
|
515
|
+
`reset(options={"start_day": k})`); `info["trace_start_day"]` reports it. Spaces,
|
|
516
|
+
observation and reward are unchanged: **the policy cannot tell the two worlds apart.**
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
import edgeengine_aware as ea
|
|
520
|
+
from edgeengine_aware.traces import Trace, TraceDrivenEnv
|
|
521
|
+
|
|
522
|
+
trace = Trace.from_csv("data/traces/demo_liguria_2023_hourly.csv")
|
|
523
|
+
print(trace.describe())
|
|
524
|
+
summer = trace.slice_days(151, 92) # days 151-243 of the year
|
|
525
|
+
env = TraceDrivenEnv(summer, ea.default_config()) # random summer weeks at every reset
|
|
526
|
+
obs, info = env.reset(seed=0)
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Data.** `data/traces/albenga_2023_hourly.csv` is the real ERA5 / ERA5-Land reanalysis for
|
|
530
|
+
a coastal site in Liguria (year 2023, hourly), downloaded with `tools/fetch_open_meteo.py`
|
|
531
|
+
(Open-Meteo archive, licence CC BY 4.0; the script needs only the standard library and works
|
|
532
|
+
for any site and year). `data/traces/demo_liguria_2023_hourly.csv` is a clearly labelled
|
|
533
|
+
**synthetic** stand-in with a darker winter, generated by `tools/make_demo_trace.py`. Your own
|
|
534
|
+
logger data works with the same CSV format (`data/traces/README.md`).
|
|
535
|
+
`examples/trace_driven.ipynb` compares the trace statistics with the synthetic scenarios and
|
|
536
|
+
evaluates the baselines and the exported PPO policies season by season: on the real trace the
|
|
537
|
+
PPO policies trained on the synthetic mixture behave as on the synthetic scenarios — the
|
|
538
|
+
2 M-step policy about 3 points below the rule-based controller, the 5 M-step policy at parity
|
|
539
|
+
while sending fewer packets — with no brown-outs in winter; on the darker synthetic trace the
|
|
540
|
+
2 M policy fails in winter, the regime the training scenarios do not contain.
|
|
541
|
+
|
|
542
|
+
## 12. From simulation to a microcontroller
|
|
543
|
+
|
|
544
|
+
Three pieces make a trained policy deployable:
|
|
545
|
+
|
|
546
|
+
1. **`deployment.PolicyBundle`** — one JSON file that freezes everything the device must
|
|
547
|
+
reproduce: the observation names in order and their normalisation formulas, the node
|
|
548
|
+
profile (all constants), the action encoding, the model parameters (thresholds for the
|
|
549
|
+
rule-based policy, weights for a neural network) and metadata. `export_policy(...)` creates
|
|
550
|
+
it; `rl.export_sb3_mlp(model)` extracts the weights of an SB3 network as plain lists.
|
|
551
|
+
Six bundles are shipped in `examples/bundles/`: `rule_based_default.json`,
|
|
552
|
+
`ppo_default.json` (PPO, 2 M steps, network 18→64→64→7), `ppo_long.json` (PPO, 5 M steps,
|
|
553
|
+
agriculture), `ppo_indoor_air.json`, `ppo_industrial.json` and `ppo_universal.json` (PPO,
|
|
554
|
+
5 M steps, trained on that domain or on all three).
|
|
555
|
+
2. **`deployment.NodeController`** — the firmware main loop written in Python against the
|
|
556
|
+
hardware protocols of `interfaces.py` (clock, energy storage, energy source, sensor,
|
|
557
|
+
radio, application). It reuses the simulator's tracker, observation builder and
|
|
558
|
+
feasibility rule. `deployment.make_mock_backend(profile)` provides fake drivers with no
|
|
559
|
+
ground truth so the loop can be exercised on a laptop; `tests/test_deployment.py` replays
|
|
560
|
+
a simulated episode's driver readings through it and checks that the observations match
|
|
561
|
+
the simulator's bit for bit.
|
|
562
|
+
3. **`firmware/`** — a dependency-free C99 translation of the node-side logic: tracker,
|
|
563
|
+
observation builder, feasibility rule, rule-based controller and the dense-network forward
|
|
564
|
+
pass (about 4 kB of code). `tools/export_c.py` turns a bundle into a header with the
|
|
565
|
+
constants and weights; `firmware/main_example.c` is the decision loop with stub drivers to
|
|
566
|
+
replace; `tests/test_firmware.py` compiles it and checks against Python: observations
|
|
567
|
+
**bit-exact** (float32), rule-based and feasibility decisions identical, network outputs
|
|
568
|
+
equal to ~1e-6 and actions identical except at exact numerical ties.
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
python tools/export_c.py examples/bundles/ppo_default.json -o firmware/eea_policy_data.h
|
|
572
|
+
cc -std=c99 -O2 -I firmware firmware/eea_node.c firmware/main_example.c -lm -o node_example && ./node_example
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Details, the field-evaluation workflow and the alternatives (TensorFlow Lite Micro, CMSIS-NN):
|
|
576
|
+
`docs/deployment.md`, `firmware/README.md`.
|
|
577
|
+
|
|
578
|
+
## 13. Notebooks, scripts and tools
|
|
579
|
+
|
|
580
|
+
The notebooks are **generated** by the `examples/build_*.py` scripts (edit the script, not the
|
|
581
|
+
notebook, then run it and execute the notebook).
|
|
582
|
+
|
|
583
|
+
| file | purpose | runtime |
|
|
584
|
+
|---|---|---|
|
|
585
|
+
| `examples/baseline_policy.ipynb` | guided tour: environment, one episode step by step, dashboard, metrics, baseline comparison over scenarios | < 1 min |
|
|
586
|
+
| `examples/train_rl.ipynb` | the RL experiment: protocol, baselines, PPO and DQN training, learning curves, comparison, behaviour analysis, export check, multi-seed study, memory study | `EEA_BUDGET=quick` ~3 min (smoke test), `default` ~30–40 min + ~1.5 h for the seed study, `paper` longer |
|
|
587
|
+
| `examples/trace_driven.ipynb` | the simulator on recorded traces: seasons, model-vs-trace statistics, policies per season, replayed weeks | ~1 min |
|
|
588
|
+
| `examples/domains.ipynb` | the three domains side by side: energy budgets, one week each, baselines on all 16 scenarios, cross-domain transfer of PPO policies | ~3 min |
|
|
589
|
+
| `examples/compare_policies.py` | headless baseline comparison (`--seeds`, `--randomize`, `--days`) | seconds |
|
|
590
|
+
| `examples/train_seeds.py` | train and evaluate one (algorithm, seed) pair: `--algo ppo|dqn|ppo_stack|rppo --seed N --steps ... --domain agriculture|indoor_air|industrial|all --eval-domain ... --out DIR`; writes a JSON with the evaluation rows and learning curve, the checkpoints and (PPO/DQN) the exported bundle | 20–60 min per run |
|
|
591
|
+
| `examples/run_long_training.sh` | chains the long runs (PPO 5 M steps × 3 seeds, DQN 2 M × 3) into `examples/rl_runs/seeds_long/`, creating `.venv` with the RL extras if needed; resumable, keeps a macOS laptop awake | 1–5 h |
|
|
592
|
+
| `examples/run_cross_domain.sh` | PPO 5 M × 2 seeds trained on each domain and on the mixture of all, evaluated on every domain → `examples/rl_runs/seeds_domains/` (read by `domains.ipynb` §4b) | ~2 h |
|
|
593
|
+
| `tools/fetch_open_meteo.py` | download an hourly ERA5/ERA5-Land trace (`--lat --lon --start --end --utc-offset --site --out`) | seconds |
|
|
594
|
+
| `tools/make_demo_trace.py` | regenerate the synthetic demo trace (`--year --seed --out`) | seconds |
|
|
595
|
+
| `tools/export_c.py` | `PolicyBundle` JSON → `eea_policy_data.h` (`-o`) | instant |
|
|
596
|
+
|
|
597
|
+
Trained models and evaluation caches go to `examples/rl_runs/` (ignored by git; set
|
|
598
|
+
`EEA_RETRAIN=1` to force retraining).
|
|
599
|
+
|
|
600
|
+
## 14. Repository layout
|
|
601
|
+
|
|
602
|
+
```
|
|
603
|
+
edgeengine_aware/ the package
|
|
604
|
+
config.py every parameter as a dataclass field, with units, defaults and validation; scenarios build on it
|
|
605
|
+
interfaces.py the six hardware protocols (Clock, EnergyStorage, EnergySource, Sensor, Radio, RemoteApplication),
|
|
606
|
+
the Measurement / Packet / TxResult records and the Policy protocol
|
|
607
|
+
observation.py NodeProfile (constants), NodeState (measurable state), NodeStateTracker (bookkeeping),
|
|
608
|
+
ObservationBuilder (the 18-vector) - the sim-to-real contract
|
|
609
|
+
actions.py action encoding, flat index, plan_execution (feasibility rule)
|
|
610
|
+
energy.py SimulatedClock, SimulatedEnergyStorage, SolarEnergySource (stochastic solar model)
|
|
611
|
+
agriculture.py FieldEnvironment: hidden soil / weather ground truth of the agriculture domain
|
|
612
|
+
sensing.py SimulatedSoilMoistureSensor
|
|
613
|
+
communication.py SimulatedLoRaRadio: radio modes, link-budget channel with slow and fast fading, ACK + margin
|
|
614
|
+
application.py RemoteMonitoringApplication: utility of the delivered information, priority requests
|
|
615
|
+
reward.py RewardCalculator with separately reported components
|
|
616
|
+
metrics.py EpisodeMetrics and EpisodeLog
|
|
617
|
+
env.py EdgeEngineAwareEnv (gymnasium.Env)
|
|
618
|
+
rendering.py Matplotlib dashboard (human / rgb_array) and text dashboard (ansi)
|
|
619
|
+
policies.py RuleBasedPolicy, PeriodicPolicy, RandomPolicy, AlwaysOnPolicy, run_episode
|
|
620
|
+
scenarios.py the six agricultural benchmark scenarios; lookup of every domain's scenarios by "domain:name"
|
|
621
|
+
domains.py the three domains: default configurations, their scenarios, the world factory (build_world)
|
|
622
|
+
process.py ProcessState (what every monitored process reports) and the weekly ActivitySchedule
|
|
623
|
+
indoor.py indoor_air domain: CO2 mass balance of a room, indoor-light PV source
|
|
624
|
+
industrial.py industrial domain: bearing thermal model with wear/faults/maintenance, thermoelectric source
|
|
625
|
+
rl.py FlatActionWrapper, MixedScenarioEnv, make_env, SB3Policy, evaluate / summarize,
|
|
626
|
+
export_sb3_mlp, NumpyMLPPolicy, FrameStacker / StackedPolicy
|
|
627
|
+
traces.py Trace, TraceSolarEnergySource, TraceFieldEnvironment, TraceDrivenEnv
|
|
628
|
+
deployment.py NodeController, mock hardware backend, PolicyBundle, export_policy
|
|
629
|
+
firmware/ C99 runtime (eea_node.h/.c), main_example.c, test harness, README
|
|
630
|
+
tools/ export_c.py, fetch_open_meteo.py, make_demo_trace.py
|
|
631
|
+
data/traces/ real Albenga 2023 trace (ERA5-Land), synthetic demo trace, CSV format description
|
|
632
|
+
examples/ notebooks, their generator scripts, compare_policies.py, train_seeds.py, run_long_training.sh, bundles/
|
|
633
|
+
tests/ pytest suite (see below)
|
|
634
|
+
docs/ detailed documentation (see below)
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## 15. Tests
|
|
638
|
+
|
|
639
|
+
`pytest` runs 114 tests:
|
|
640
|
+
|
|
641
|
+
| file | what it checks |
|
|
642
|
+
|---|---|
|
|
643
|
+
| `test_env.py` | Gymnasium API and spaces, `check_env`, truncation, feasibility/rejection, no leakage of hidden state into the observation, rendering, single-mode radio |
|
|
644
|
+
| `test_energy.py` | energy bounds and conservation, units, solar model, storage bookkeeping |
|
|
645
|
+
| `test_reproducibility.py` | same seed → same episode; domain randomisation changes the physics but not the spaces |
|
|
646
|
+
| `test_deployment.py` | protocols, mock backend, controller vs simulator observation equality, bundle export |
|
|
647
|
+
| `test_rl.py` | scenarios, wrappers, evaluation protocol, numpy MLP runtime vs SB3, rule-based radio-mode choice |
|
|
648
|
+
| `test_traces.py` | trace loader semantics (interpolation, interval means, CSV round trip), trace-driven backends and environment |
|
|
649
|
+
| `test_firmware.py` | C runtime vs Python for the three domain profiles (compiles `firmware/` with the system C compiler; skipped if none) |
|
|
650
|
+
| `test_domains.py` | threshold direction, weekly schedule, CO₂ and bearing models, TEG coupling, shared spaces across domains, qualified scenario names and mixtures, agriculture defaults unchanged |
|
|
651
|
+
|
|
652
|
+
## 16. Documentation
|
|
653
|
+
|
|
654
|
+
| document | content |
|
|
655
|
+
|---|---|
|
|
656
|
+
| `docs/observation.md` | the 18 observation components: meaning, normalisation, hardware source; why the problem is partially observable and what is done about it |
|
|
657
|
+
| `docs/actions.md` | action encoding, radio modes and their link budget, the feasibility rule, flat encoding |
|
|
658
|
+
| `docs/modeling.md` | all physical models with formulas and default constants, utility and reward, the MDP/POMDP formulation |
|
|
659
|
+
| `docs/sim_to_real.md` | the architecture that keeps simulation and hardware on the same contract, the known gaps and their mitigations, domain randomisation, how to add backends |
|
|
660
|
+
| `docs/deployment.md` | PolicyBundle, the controller loop, the C runtime, TinyML alternatives, field-evaluation workflow |
|
|
661
|
+
| `firmware/README.md` | the C runtime: API, generated data header, equivalence tests, porting notes |
|
|
662
|
+
| `data/traces/README.md` | the trace CSV format and the shipped/obtainable data |
|
|
663
|
+
|
|
664
|
+
## 17. What is not modelled
|
|
665
|
+
|
|
666
|
+
Being explicit about the simplifications matters more than the list of features:
|
|
667
|
+
|
|
668
|
+
* **Battery**: an ideal energy buffer with a single charge-efficiency knob; no voltage
|
|
669
|
+
curve, temperature effect, ageing or self-discharge. The node reads its energy exactly.
|
|
670
|
+
* **Radio**: delivery depends only on the link margin; no duty-cycle limits, collisions,
|
|
671
|
+
gateway congestion or multiple gateways; energy per uplink is constant per mode.
|
|
672
|
+
* **Soil**: a single bucket with evapotranspiration, rain and external irrigation; no crop
|
|
673
|
+
model, no spatial variability. Irrigation is *not* a decision of the node.
|
|
674
|
+
* **Application**: a rule-based priority (thresholds, information age, random campaigns), not
|
|
675
|
+
a human operator.
|
|
676
|
+
* **Clock**: exact 15-minute steps, no drift or jitter.
|
|
677
|
+
* **Indoor air**: a single well-mixed room with one occupancy schedule; the CO₂ sensor's own
|
|
678
|
+
warm-up and drift are folded into the two energy/noise levels. **Industrial**: a first-order
|
|
679
|
+
thermal model with a scalar "health"; vibration is not simulated, the accurate sensing level
|
|
680
|
+
stands for a vibration burst only through its energy and precision.
|
|
681
|
+
|
|
682
|
+
`docs/sim_to_real.md` lists, for each of these, what a real device will do differently and
|
|
683
|
+
which mitigation (domain randomisation, configuration, recorded traces) is available.
|
|
684
|
+
|
|
685
|
+
## Glossary
|
|
686
|
+
|
|
687
|
+
* **ACK (acknowledgement)** — a short reply from the gateway confirming that an uplink was
|
|
688
|
+
received. It also lets the node measure how much margin the link had.
|
|
689
|
+
* **AoI (age of information)** — how old the freshest value known to the application is:
|
|
690
|
+
time since it was received plus the age the reading already had when sent.
|
|
691
|
+
* **Brown-out** — the battery cannot even supply the always-on load; the node stops
|
|
692
|
+
working until energy is harvested again.
|
|
693
|
+
* **Domain randomisation** — perturbing the simulator's physical parameters at every
|
|
694
|
+
episode so that a trained policy is robust to devices and conditions that differ from the
|
|
695
|
+
nominal ones.
|
|
696
|
+
* **Duty cycle** — a fixed schedule ("sense and transmit every hour"), the traditional way
|
|
697
|
+
low-power nodes are programmed.
|
|
698
|
+
* **EWMA (exponentially weighted moving average)** — a running average that gives weight
|
|
699
|
+
α to the newest value and (1−α) to the previous average; a cheap memory of the recent past.
|
|
700
|
+
* **Field capacity** — the amount of water a soil holds after excess water has drained;
|
|
701
|
+
soil moisture in this project is expressed as a fraction of it (1.0 = field capacity).
|
|
702
|
+
* **Held-out seeds** — random seeds used only for evaluation, never during training, so that
|
|
703
|
+
results are not flattered by memorised episodes.
|
|
704
|
+
* **Link margin / link budget** — transmit power minus path loss minus receiver sensitivity,
|
|
705
|
+
in dB. Positive margin: the message is likely to be received. Each radio mode has a
|
|
706
|
+
different sensitivity, hence a different margin for the same channel.
|
|
707
|
+
* **Path loss** — attenuation of the radio signal between node and gateway, in dB; it varies
|
|
708
|
+
slowly (vegetation, humidity, obstacles: *slow fading*) and from message to message
|
|
709
|
+
(*fast fading*).
|
|
710
|
+
* **POMDP** — a decision problem in which the agent does not observe the full state (here:
|
|
711
|
+
the true soil moisture, the weather regime, the channel state are hidden). Standard remedies
|
|
712
|
+
are summary statistics of the past, explicit timers and, if needed, memory in the policy.
|
|
713
|
+
* **Priority** — the application's declared level of interest: routine (0), elevated (1),
|
|
714
|
+
urgent (2).
|
|
715
|
+
* **SF (spreading factor)** — a LoRa modulation setting; higher SF means longer, slower
|
|
716
|
+
transmissions that can be received at weaker signal (better sensitivity) but cost more
|
|
717
|
+
energy. The project's *fast / standard / robust* modes are SF7 / SF9 / SF12-like.
|
|
718
|
+
* **SoC (state of charge)** — stored energy divided by capacity, from 0 to 1.
|
|
719
|
+
* **`MultiDiscrete([3, 4])`** — Gymnasium's action space for a vector of independent
|
|
720
|
+
integers, here one in {0,1,2} and one in {0,1,2,3}.
|
|
721
|
+
|
|
722
|
+
## Citation
|
|
723
|
+
|
|
724
|
+
If you use EdgeEngine AWARE in academic work, please cite it. The repository carries a
|
|
725
|
+
`CITATION.cff` file (GitHub shows it under *Cite this repository*); in BibTeX:
|
|
726
|
+
|
|
727
|
+
```bibtex
|
|
728
|
+
@software{elios_edgeengine_aware_2026,
|
|
729
|
+
author = {{ELIOS Lab, DITEN, University of Genoa}},
|
|
730
|
+
title = {EdgeEngine AWARE: an application- and energy-aware simulation environment for
|
|
731
|
+
reinforcement learning in energy-harvesting Edge IoT systems},
|
|
732
|
+
year = {2026},
|
|
733
|
+
version = {0.4.0},
|
|
734
|
+
url = {https://github.com/measurify/EdgeEngine-AWARE},
|
|
735
|
+
note = {\url{https://www.elios.unige.it/}}
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
EdgeEngine AWARE is released under the MIT License (see `LICENSE`).
|