Skip to content

Commit f654e1c

Browse files
author
Pieter Cawood
committed
Update readme
1 parent b1bef82 commit f654e1c

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

README.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,155 @@ You can inspect all loss terms and constraints in `controllers.py` (classes `GRU
116116
Everything is implemented to be **stable‑by‑construction**: we never bypass slew/clamp and we bias to
117117
baseline allocations when signals are missing or become non‑finite.
118118

119+
---
120+
121+
## How to add a new *Problem* (plant)
122+
123+
Problems live in `deeppid/envs/problems.py` and are registered in `AVAILABLE_PROBLEMS` so the GUI can discover them.
124+
125+
**1) Implement a class** with the following minimal API (feel free to copy an existing one and tweak):
126+
127+
```python
128+
# deeppid/envs/problems.py
129+
130+
import torch
131+
132+
class MyCustomProblem:
133+
def __init__(self, Ts: float):
134+
# Dimensions / labels
135+
self.N = 3
136+
self.labels = [f"Source {i+1}" for i in range(self.N)]
137+
138+
# Metadata (optional; affects GUI labels)
139+
self.output_name = "Flow"
140+
self.output_unit = "L/min"
141+
self.entity_title = "Material"
142+
143+
# Constraints and nominal model parameters
144+
self.k_coeff = torch.tensor([0.9, 1.1, 0.8], dtype=torch.float64)
145+
self.speed_min = torch.tensor([0.0, 0.0, 0.0], dtype=torch.float64)
146+
self.speed_max = torch.tensor([100.0, 100.0, 100.0], dtype=torch.float64)
147+
self.slew_rate = torch.tensor([5.0, 5.0, 5.0], dtype=torch.float64) # % per step
148+
self.Ts = Ts
149+
self.alpha = 0.4 # single-pole plant step factor for filtering
150+
151+
# Defaults
152+
self.default_total_flow = 60.0
153+
self.default_target_ratio = torch.ones(self.N, dtype=torch.float64) / self.N
154+
155+
# Internal filtered measurement
156+
self._y = torch.zeros(self.N, dtype=torch.float64)
157+
158+
def baseline_allocation(self, ratio: torch.Tensor, F_total: torch.Tensor) -> torch.Tensor:
159+
\"\"\"Feedforward speeds (simple inverse of k within bounds).\"\"\"
160+
s = (ratio * F_total) / (self.k_coeff + 1e-12)
161+
return torch.clamp(s, self.speed_min, self.speed_max)
162+
163+
def step(self, speeds_cmd: torch.Tensor) -> torch.Tensor:
164+
\"\"\"One simulation step. Update and return filtered measured outputs.\"\"\"
165+
y_raw = self.k_coeff * speeds_cmd
166+
self._y = self._y + self.alpha * (y_raw - self._y)
167+
return self._y.clone()
168+
169+
def comp_from_speeds(self, speeds: torch.Tensor) -> torch.Tensor:
170+
\"\"\"Return composition (fractions) implied by nominal model for display/metrics.\"\"\"
171+
flow = self.k_coeff * speeds
172+
tot = flow.sum() + 1e-12
173+
return flow / tot
174+
```
175+
176+
**2) Register it** at the bottom of `problems.py`:
177+
178+
```python
179+
AVAILABLE_PROBLEMS = {
180+
# existing ones...
181+
"MyCustomProblem": MyCustomProblem,
182+
}
183+
```
184+
185+
Your new problem will now appear in the GUI's *Problem* dropdown.
186+
187+
---
188+
189+
## How to add a new *Controller*
190+
191+
Controllers live in `deeppid/controllers/controllers.py`. The GUI expects them to be discoverable via the
192+
package registry `deeppid.AVAILABLE` (set up in `deeppid/__init__.py`). The easiest path is to implement
193+
a class with a **PID-like interface** and wrap it with `CtrlAdapter` automatically:
194+
195+
**Minimum contract (any of these works):**
196+
- Provide `step(flows_meas_filt, target_ratio, F_total, speeds_direct)` → returns speeds (Tensor of length N); or
197+
- Provide `forward(target_ratio, F_total, flows_meas_filt)` → returns speeds; and optionally
198+
- Provide `train_step(...)` if your controller learns online; and
199+
- Optionally `sync_to(speeds_now, flows_now)` to initialize internal state when the problem changes.
200+
201+
**Constructor signature** should accept (at least) the common parameters so the GUI can instantiate it:
202+
`(N, k, speed_min, speed_max, Ts, slew_rate)` — extra arguments are fine.
203+
204+
**Skeleton:**
205+
206+
```python
207+
# deeppid/controllers/controllers.py
208+
import torch
209+
210+
class MyFancyController(torch.nn.Module):
211+
def __init__(self, N, k, speed_min, speed_max, Ts, slew_rate, **kwargs):
212+
super().__init__()
213+
self.N, self.k = N, k
214+
self.speed_min, self.speed_max = speed_min, speed_max
215+
self.Ts, self.slew = Ts, slew_rate
216+
self.register_buffer("prev_speeds", torch.ones(N, dtype=torch.float64) * 25.0)
217+
# your nets/params here...
218+
self.net = torch.nn.Sequential(
219+
torch.nn.Linear(N + 1 + N + N, 128), torch.nn.ReLU(),
220+
torch.nn.Linear(128, N)
221+
)
222+
223+
def _state(self, tr, Ft, y):
224+
Ft_n = (Ft / (torch.sum(self.k * self.speed_max).clamp_min(1.0))).clamp(0, 2.0)
225+
prev = ((self.prev_speeds - self.speed_min) / (self.speed_max - self.speed_min).clamp_min(1e-6)).clamp(0, 1)
226+
flows_n = (y / (Ft + 1e-6)).clamp(0, 2.0)
227+
return torch.cat([tr, Ft_n.view(1), flows_n, prev], dim=0)
228+
229+
def forward(self, target_ratio, F_total, flows_meas_filt):
230+
x = self._state(target_ratio, F_total, flows_meas_filt)
231+
raw = self.net(x)
232+
span = (self.speed_max - self.speed_min).clamp_min(1e-6)
233+
s = self.speed_min + span * torch.sigmoid(raw)
234+
s = torch.clamp(
235+
self.prev_speeds + torch.clamp(s - self.prev_speeds, -self.slew, self.slew),
236+
self.speed_min, self.speed_max
237+
)
238+
self.prev_speeds = s.clone()
239+
return s
240+
241+
# Optional, for adaptive controllers
242+
def train_step(self, target_ratio, F_total, flows_meas_filt, **_):
243+
# do an update; return the new speeds (or None)
244+
return self.forward(target_ratio, F_total, flows_meas_filt)
245+
```
246+
247+
**Register the controller** name in `deeppid/__init__.py` (registry `AVAILABLE`):
248+
249+
```python
250+
from .controllers.controllers import MyFancyController
251+
252+
AVAILABLE["MyFancy"] = MyFancyController
253+
```
254+
255+
It will then show up in the GUI *Driver* combo-box automatically.
256+
257+
> 🧩 Tip: If your controller already conforms to the `step(...)` signature, the GUI will call it directly.
258+
> Otherwise it will fall back to `forward(...)`. `CtrlAdapter` normalizes those differences for you.
259+
260+
---
261+
262+
## Testing
263+
264+
- Quick import smoke test: `python -c "import deeppid; print('OK')"`
265+
- Run GUI: `python examples/test.py`
266+
- Add lightweight unit tests in `tests/` (e.g., `pytest -q`).
267+
119268
## License
120269

121270
MIT

0 commit comments

Comments
 (0)