@@ -116,6 +116,155 @@ You can inspect all loss terms and constraints in `controllers.py` (classes `GRU
116116Everything is implemented to be ** stable‑by‑construction** : we never bypass slew/clamp and we bias to
117117baseline 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
121270MIT
0 commit comments