torch-fis 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. torch_fis-0.2.0/LICENSE +21 -0
  2. torch_fis-0.2.0/PKG-INFO +426 -0
  3. torch_fis-0.2.0/README.md +385 -0
  4. torch_fis-0.2.0/__init__.py +29 -0
  5. torch_fis-0.2.0/config.py +344 -0
  6. torch_fis-0.2.0/consequent.py +202 -0
  7. torch_fis-0.2.0/defuzz.py +108 -0
  8. torch_fis-0.2.0/envs/__init__.py +42 -0
  9. torch_fis-0.2.0/envs/assets/__init__.py +167 -0
  10. torch_fis-0.2.0/envs/controllers/__init__.py +7 -0
  11. torch_fis-0.2.0/envs/controllers/base.py +24 -0
  12. torch_fis-0.2.0/envs/controllers/pid.py +107 -0
  13. torch_fis-0.2.0/envs/controllers/racing.py +140 -0
  14. torch_fis-0.2.0/envs/core/__init__.py +0 -0
  15. torch_fis-0.2.0/envs/core/gym_env.py +164 -0
  16. torch_fis-0.2.0/envs/core/integrate.py +78 -0
  17. torch_fis-0.2.0/envs/core/metrics.py +131 -0
  18. torch_fis-0.2.0/envs/core/modes.py +37 -0
  19. torch_fis-0.2.0/envs/core/plant.py +165 -0
  20. torch_fis-0.2.0/envs/core/render.py +372 -0
  21. torch_fis-0.2.0/envs/core/scenarios.py +86 -0
  22. torch_fis-0.2.0/envs/core/video.py +189 -0
  23. torch_fis-0.2.0/envs/core/wrappers.py +80 -0
  24. torch_fis-0.2.0/envs/plants/__init__.py +5 -0
  25. torch_fis-0.2.0/envs/plants/crane.py +203 -0
  26. torch_fis-0.2.0/envs/plants/drone.py +166 -0
  27. torch_fis-0.2.0/envs/plants/robot.py +242 -0
  28. torch_fis-0.2.0/envs/racing.py +513 -0
  29. torch_fis-0.2.0/membership.py +115 -0
  30. torch_fis-0.2.0/norm.py +41 -0
  31. torch_fis-0.2.0/pyproject.toml +79 -0
  32. torch_fis-0.2.0/rule.py +130 -0
  33. torch_fis-0.2.0/setup.cfg +4 -0
  34. torch_fis-0.2.0/system.py +387 -0
  35. torch_fis-0.2.0/tests/test_config.py +122 -0
  36. torch_fis-0.2.0/tests/test_defuzz.py +93 -0
  37. torch_fis-0.2.0/tests/test_envs_controllers.py +166 -0
  38. torch_fis-0.2.0/tests/test_envs_crane.py +172 -0
  39. torch_fis-0.2.0/tests/test_envs_crossvalidation.py +126 -0
  40. torch_fis-0.2.0/tests/test_envs_drone.py +56 -0
  41. torch_fis-0.2.0/tests/test_envs_gym.py +314 -0
  42. torch_fis-0.2.0/tests/test_envs_integrate.py +115 -0
  43. torch_fis-0.2.0/tests/test_envs_racing.py +121 -0
  44. torch_fis-0.2.0/tests/test_envs_video.py +68 -0
  45. torch_fis-0.2.0/tests/test_grad.py +126 -0
  46. torch_fis-0.2.0/tests/test_mamdani.py +164 -0
  47. torch_fis-0.2.0/tests/test_membership.py +134 -0
  48. torch_fis-0.2.0/tests/test_shapes.py +126 -0
  49. torch_fis-0.2.0/tests/test_tsk.py +267 -0
  50. torch_fis-0.2.0/tests/test_viz.py +130 -0
  51. torch_fis-0.2.0/torch_fis.egg-info/PKG-INFO +426 -0
  52. torch_fis-0.2.0/torch_fis.egg-info/SOURCES.txt +86 -0
  53. torch_fis-0.2.0/torch_fis.egg-info/dependency_links.txt +1 -0
  54. torch_fis-0.2.0/torch_fis.egg-info/requires.txt +23 -0
  55. torch_fis-0.2.0/torch_fis.egg-info/top_level.txt +1 -0
  56. torch_fis-0.2.0/variable.py +43 -0
  57. torch_fis-0.2.0/viz.py +356 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Thiago Medeiros
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,426 @@
1
+ Metadata-Version: 2.4
2
+ Name: torch-fis
3
+ Version: 0.2.0
4
+ Summary: A differentiable Fuzzy Inference System (FIS) library built on PyTorch
5
+ Author-email: Thiago Medeiros <mdrs.thiago@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mdrs-thiago/fuzzy_torch
8
+ Project-URL: Issues, https://github.com/mdrs-thiago/fuzzy_torch/issues
9
+ Keywords: fuzzy,inference,pytorch,mamdani,sugeno,tsk,anfis,membership-functions
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: torch>=1.12
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest>=7; extra == "test"
25
+ Requires-Dist: matplotlib>=3.7; extra == "test"
26
+ Provides-Extra: envs
27
+ Requires-Dist: gymnasium>=1.0; extra == "envs"
28
+ Requires-Dist: numpy>=1.21; extra == "envs"
29
+ Requires-Dist: pygame-ce>=2.5; extra == "envs"
30
+ Requires-Dist: pillow>=9; extra == "envs"
31
+ Provides-Extra: plot
32
+ Requires-Dist: matplotlib>=3.7; extra == "plot"
33
+ Provides-Extra: viz
34
+ Requires-Dist: torch-fis[envs]; extra == "viz"
35
+ Requires-Dist: mujoco>=3.2; extra == "viz"
36
+ Requires-Dist: matplotlib>=3.7; extra == "viz"
37
+ Provides-Extra: rl
38
+ Requires-Dist: torch-fis[envs]; extra == "rl"
39
+ Requires-Dist: stable-baselines3>=2.9; extra == "rl"
40
+ Dynamic: license-file
41
+
42
+ # fuzzy_torch
43
+
44
+ A differentiable Fuzzy Inference System (FIS) library built on **PyTorch**.
45
+
46
+ Design and evaluate **Mamdani** and **Takagi-Sugeno-Kang (TSK)** fuzzy systems with batched
47
+ tensor operations, differentiable membership functions, and composable rules. Every parameter
48
+ is an `nn.Parameter`, so a fuzzy system trains with any `torch.optim` optimizer and moves to
49
+ the GPU with `.to(device)` like any other `nn.Module`.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install torch-fis # core library
55
+ pip install "torch-fis[envs]" # + Gymnasium control environments
56
+ pip install "torch-fis[viz]" # + MuJoCo rendering and matplotlib
57
+ ```
58
+
59
+ The distribution is called `torch-fis`; the import name is `fuzzy_torch`:
60
+
61
+ ```python
62
+ import fuzzy_torch
63
+ ```
64
+
65
+ The latest development version installs straight from GitHub:
66
+
67
+ ```bash
68
+ pip install git+https://github.com/mdrs-thiago/fuzzy_torch.git
69
+ ```
70
+
71
+ Or for local development:
72
+
73
+ ```bash
74
+ git clone https://github.com/mdrs-thiago/fuzzy_torch.git
75
+ cd fuzzy_torch
76
+ pip install -e .
77
+ ```
78
+
79
+ ## Quick Start - Mamdani
80
+
81
+ ```python
82
+ import torch
83
+ from fuzzy_torch import (
84
+ FuzzyVariable, FuzzyRule, FuzzyModule, Centroid,
85
+ TriangularMF, TrapezoidalMF,
86
+ )
87
+
88
+ # 1. Variables carry a name, a range, and named linguistic terms.
89
+ service = FuzzyVariable('Service', 0, 10)
90
+ service.add_term('Poor', TriangularMF(0, 0, 5))
91
+ service.add_term('Good', TriangularMF(0, 5, 10))
92
+ service.add_term('Excellent', TriangularMF(5, 10, 10))
93
+
94
+ food = FuzzyVariable('Food', 0, 10)
95
+ food.add_term('Rancid', TrapezoidalMF(0, 0, 1, 3))
96
+ food.add_term('Delicious', TrapezoidalMF(7, 9, 10, 10))
97
+
98
+ tip = FuzzyVariable('Tip', 0, 30)
99
+ tip.add_term('Low', TriangularMF(0, 0, 15))
100
+ tip.add_term('Medium', TriangularMF(0, 15, 30))
101
+ tip.add_term('High', TriangularMF(15, 30, 30))
102
+
103
+ # 2. Rules: an antecedent (or list of them, AND-connected) and a consequent.
104
+ rules = [
105
+ FuzzyRule([(service, 'Poor'), (food, 'Rancid')], (tip, 'Low')),
106
+ FuzzyRule((service, 'Good'), (tip, 'Medium')),
107
+ FuzzyRule([(service, 'Excellent'), (food, 'Delicious')], (tip, 'High')),
108
+ ]
109
+
110
+ # 3. Build and run.
111
+ fis = FuzzyModule(inputs=[service, food], output=tip, rules=rules,
112
+ defuzz_method=Centroid(0, 30))
113
+
114
+ out = fis({'Service': torch.tensor([6.5]), 'Food': torch.tensor([9.8])})
115
+ print(f"Tip: {out.item():.2f}")
116
+
117
+ fis.explain({'Service': torch.tensor(6.5), 'Food': torch.tensor(9.8)})
118
+ ```
119
+
120
+ Inputs may be 0-dim scalars, `(batch,)` or `(batch, 1)` tensors, or plain Python numbers.
121
+ A 0-dim input gives a 0-dim output; everything else gives `(batch,)`.
122
+
123
+ ## Quick Start - TSK (Sugeno) of any order
124
+
125
+ Swap the consequent fuzzy set for a `TSKConsequent` and the system switches to TSK inference:
126
+ firing strengths weight per-rule crisp polynomials, `sum(w_i * f_i) / sum(w_i)`. No
127
+ defuzzification is involved, and the output variable needs no terms.
128
+
129
+ ```python
130
+ import torch
131
+ from fuzzy_torch import (
132
+ FuzzyVariable, FuzzyRule, TSKSystem, TSKConsequent, GaussianMF,
133
+ )
134
+
135
+ x = FuzzyVariable('X', 0, 10)
136
+ x.add_term('Low', GaussianMF(2.0, 2.0))
137
+ x.add_term('High', GaussianMF(8.0, 2.0))
138
+
139
+ y = FuzzyVariable('Y', 0, 100) # range only; no terms needed
140
+
141
+ rules = [
142
+ FuzzyRule((x, 'Low'), TSKConsequent(order=1)), # f(x) = c0 + c1*x
143
+ FuzzyRule((x, 'High'), TSKConsequent(order=1)),
144
+ ]
145
+ fis = TSKSystem(inputs=[x], output=y, rules=rules)
146
+
147
+ # The coefficients are nn.Parameters -- train them like anything else.
148
+ inputs = torch.linspace(0, 10, 200)
149
+ targets = 3.0 + 2.0 * inputs
150
+
151
+ opt = torch.optim.Adam(fis.parameters(), lr=0.05)
152
+ for _ in range(1000):
153
+ opt.zero_grad()
154
+ loss = ((fis({'X': inputs}) - targets) ** 2).mean()
155
+ loss.backward()
156
+ opt.step()
157
+
158
+ fis.print_rules()
159
+ ```
160
+
161
+ `order` is the total polynomial degree, and the basis is the **full multivariate polynomial**:
162
+ every monomial of degree `<= order`, cross terms included. For `n` inputs at order `p` that is
163
+ `C(n + p, p)` coefficients per rule.
164
+
165
+ | inputs | order | monomials |
166
+ |--------|-------|-----------|
167
+ | 2 | 0 | `1` |
168
+ | 2 | 1 | `1, a, b` |
169
+ | 2 | 2 | `1, a, b, a², a·b, b²` |
170
+ | 2 | 3 | the above plus `a³, a²b, ab², b³` (10 terms) |
171
+
172
+ By default `TSKConsequent(normalize=True)` rescales each input from its variable range onto
173
+ `[-1, 1]` before raising powers, which keeps high orders conditioned -- a raw cubic term on a
174
+ `[0, 30]` universe reaches 27000 and would swamp the constant. The printed coefficients then
175
+ apply to the rescaled inputs; pass `normalize=False` to work in raw units.
176
+
177
+ For a consequent that is not a polynomial at all, subclass `Consequent` and implement
178
+ `forward(X: (batch, n_inputs)) -> (batch,)`.
179
+
180
+ ## Visualisation
181
+
182
+ Sets, variables and systems can draw themselves with matplotlib
183
+ (`pip install "torch-fis[plot]"`). Each method returns the matplotlib axes (or figure),
184
+ and accepts `ax=` to draw into an existing one.
185
+
186
+ ```python
187
+ TriangularMF(0, 5, 10).plot() # one membership function
188
+ service.plot() # every term of a variable
189
+ service.plot(value=6.5) # ... with the degrees reached at 6.5
190
+
191
+ fis.plot_inference({'Service': 6.5, 'Food': 9.8}) # rule viewer, Mamdani or TSK
192
+ fis.plot_surface() # output over the first two inputs
193
+ fis.plot_surface('Service', fixed={'Food': 9.0}) # a 1-D slice
194
+ ```
195
+
196
+ `plot_inference` is the graphical counterpart of `explain()`: one row per rule
197
+ showing each antecedent set cut at the degree the input reaches, the firing strength,
198
+ and the implied output set (or, for TSK, the rule's crisp value); the last row shows the
199
+ aggregated set and the crisp output. Rule bases larger than 12 rules show only the rules
200
+ that fire, unless you pass `active_only=False`.
201
+
202
+ ## Systems as JSON
203
+
204
+ A system can also be described as plain data and built with `build_system`; the
205
+ inverse, `system_to_config`, exports a (possibly trained) system back to it:
206
+
207
+ ```python
208
+ from fuzzy_torch import build_system, system_to_config
209
+
210
+ fis = build_system({
211
+ "inputs": {"error": {"range": [-1, 1], "terms": {
212
+ "neg": ["trap", -1, -1, -0.5, 0], "zero": ["tri", -0.5, 0, 0.5], "pos": ["trap", 0, 0.5, 1, 1]}}},
213
+ "output": {"name": "u", "range": [-1, 1], "terms": {
214
+ "down": ["tri", -1, -1, 0], "hold": ["tri", -1, 0, 1], "up": ["tri", 0, 1, 1]}},
215
+ "rules": [{"if": {"error": "neg"}, "then": "up"},
216
+ {"if": {"error": "zero"}, "then": "hold"},
217
+ {"if": {"error": "pos"}, "then": "down"}],
218
+ })
219
+ ```
220
+
221
+ A rule whose `then` is a number or a coefficient list makes a TSK system instead.
222
+ Malformed specs raise `ConfigError` naming the field, e.g. `rules[1].if.error: unknown term`.
223
+ The full format is documented in `config.py`.
224
+
225
+ ## Choosing the mode
226
+
227
+ The mode is inferred from the rules, so `FuzzyModule` handles both. `MamdaniSystem` and
228
+ `TSKSystem` are the same class with an assertion, for when you want the intent stated in code.
229
+ A rule base may not mix the two kinds of consequent.
230
+
231
+ ## Configuration
232
+
233
+ ```python
234
+ FuzzyModule(
235
+ inputs, output, rules,
236
+ defuzz_method=None, # Mamdani: Centroid (default), Bisector, MeanOfMaximum
237
+ implication=None, # Mamdani: Min (default, clipping) or Product (scaling)
238
+ aggregation=None, # Mamdani: Max (default) or ProbabilisticSum
239
+ tsk_normalize=True, # TSK: weighted average (True) or weighted sum (False)
240
+ no_rule_value=float('nan'),
241
+ chunk_rules=None,
242
+ )
243
+ ```
244
+
245
+ **When no rule fires**, there is no meaningful crisp answer, so the output is `NaN` rather
246
+ than a plausible-looking number. Set `no_rule_value` (e.g. to the midpoint of the output
247
+ range) if you would rather have a fallback. Gradients of the batch items that *did* fire are
248
+ unaffected either way.
249
+
250
+ **Only `Centroid` is differentiable.** `Bisector` and `MeanOfMaximum` rely on hard index
251
+ selection, so no gradient reaches the membership functions; constructing a system with one
252
+ emits a warning. Each class declares this as `.differentiable`.
253
+
254
+ ## Performance notes
255
+
256
+ Mamdani inference is `O(batch × rules × resolution)` however it is arranged, so the wins come
257
+ from removing redundant work rather than from wider tensors. Against the previous
258
+ implementation on the same rule bases (CPU, 4 threads, three runs, min-of-repeats):
259
+
260
+ | rules × batch × resolution | speedup |
261
+ |---|---|
262
+ | 25 × 64 × 201 | 2.8-3.1× |
263
+ | 25 × 1024 × 201 | 1.9-2.1× |
264
+ | 120 × 1024 × 201 | 1.8-1.9× |
265
+ | 400 × 4096 × 201 | 1.2-1.6× |
266
+ | 120 × 8192 × 501 | 1.0-1.1× |
267
+
268
+ Ratios rather than absolute times: the absolute numbers moved by 4× between runs on a loaded
269
+ machine, while the ratios stayed put. The gain is largest on small-to-medium rule bases, where
270
+ the removed per-rule overhead dominates; at the top end the work is memory-bound and the two
271
+ implementations converge.
272
+
273
+ Rules are folded one at a time by default. Batching them into a single
274
+ `(batch, rules, resolution)` tensor was measurably *slower* at every size tried on CPU -- it
275
+ is memory-bound, and 400 rules over a batch of 4096 materialises 1.3 GB. `chunk_rules` sets
276
+ the block width if you want to try the wide form on a GPU.
277
+
278
+ The consequent grid is cached in `eval()` mode and recomputed in `train()`, where the output
279
+ membership parameters are still moving.
280
+
281
+ ## Features
282
+
283
+ - **Membership functions**: Triangular, Trapezoidal, Gaussian, Bell, Sigmoid --
284
+ correct at degenerate shoulders (`TriangularMF(0, 0, 5)` is 1.0 at `x = 0`) and finite
285
+ under autograd everywhere.
286
+ - **T-Norms / S-Norms**: Min, Max, Product, Probabilistic Sum, Łukasiewicz.
287
+ - **Inference**: Mamdani (configurable implication and aggregation) and TSK of any order.
288
+ - **Defuzzification**: Centroid, Bisector, Mean of Maximum.
289
+ - **Differentiable**: end to end, over membership parameters and TSK coefficients alike.
290
+ - **GPU-ready**: `nn.Module` throughout; `.to(device)` moves the whole system.
291
+ - **Visualisation**: `.plot()` on sets and variables; `plot_inference` and `plot_surface` on systems.
292
+
293
+ ## Watching simulations in a notebook
294
+
295
+ `record` runs one episode headlessly and returns a video that plays inline in Jupyter,
296
+ VS Code or Colab; `compare` shows several side by side. It renders with MuJoCo (3-D)
297
+ when an OpenGL context is available and falls back to pygame (2-D) otherwise.
298
+
299
+ ```python
300
+ from fuzzy_torch.envs import RacingEnv, record, compare
301
+ from fuzzy_torch.envs.controllers import FuzzyRaceDriver
302
+
303
+ video = record(RacingEnv('chicane', render_mode='rgb_array'), FuzzyRaceDriver())
304
+ video # last line of a cell -> animated playback
305
+ video.info['lap_time'] # every info dict, observation and raw state is kept
306
+ video.save('lap.gif') # or .webp
307
+ ```
308
+
309
+ Make the environment with `render_mode='human'` instead and the same call opens a live
310
+ window. `examples/notebooks/02_fuzzy_control_envs.ipynb` uses this throughout.
311
+
312
+ ## Quadrotor environment
313
+
314
+ The optional ``envs`` extra also includes a differentiable 3-D quadrotor. Its
315
+ 13-state rigid-body model uses world position and velocity, a scalar-first unit
316
+ quaternion, and body angular rates. The default action is four physical rotor
317
+ thrusts; ``ControlMode.RATE_THRUST`` supplies a practical higher-level interface
318
+ of collective thrust plus roll, pitch, and yaw-rate requests.
319
+
320
+ ```python
321
+ from fuzzy_torch.envs import ControlMode, QuadrotorPlant
322
+ from fuzzy_torch.envs.core.gym_env import FuzzyControlEnv
323
+
324
+ drone = QuadrotorPlant(mode=ControlMode.MOTOR_THRUST)
325
+ env = FuzzyControlEnv(drone, render_mode='human')
326
+ observation, info = env.reset(seed=0)
327
+ hover = [float(drone.m * drone.g / 4)] * 4
328
+ observation, reward, terminated, truncated, info = env.step(hover)
329
+ ```
330
+
331
+ MuJoCo draws an asset-free 3-D quadrotor when available; the pygame fallback is a
332
+ top-down flight view with altitude and target readouts.
333
+
334
+ Run the baseline demo to generate a tracking plot, or use the live 3-D viewer:
335
+
336
+ ```bash
337
+ .venv/bin/python examples/drone_demo.py --out out/
338
+ .venv/bin/python examples/drone_demo.py --live --backend mujoco
339
+ ```
340
+
341
+ ## Fuzzy Grand Prix environment
342
+
343
+ The optional ``envs`` extra includes a closed-circuit Gymnasium time-trial for the
344
+ dynamic ``DiffDriveRobot`` plant. It has three comparable lecture circuits:
345
+ ``oval``, ``chicane`` and ``technical``. A lap ends only at the finish line;
346
+ leaving the track is a DNF, so the reported ``lap_time`` cannot be improved by
347
+ cutting corners.
348
+
349
+ ```python
350
+ from fuzzy_torch.envs import RacingEnv
351
+ from fuzzy_torch.envs.controllers import FuzzyRaceDriver
352
+
353
+ env = RacingEnv('chicane')
354
+ driver = FuzzyRaceDriver()
355
+ obs, info = env.reset(seed=0)
356
+ while True:
357
+ obs, reward, terminated, truncated, info = env.step(driver(obs))
358
+ if terminated or truncated:
359
+ break
360
+ print(info['lap_time'], info['off_track']) # seconds, or None for a DNF
361
+ ```
362
+
363
+ The observation is deliberately track-relative rather than a raw world pose:
364
+ ``[cross_track_error, heading_error, speed, yaw_rate, lookahead_curvature, progress]``.
365
+ This gives a small, explainable fuzzy-controller input space and lets the same rule
366
+ base be evaluated on every circuit. See ``examples/racing_demo.py`` for a complete
367
+ three-circuit run and plot. Add ``--live`` to open the animated circuit window:
368
+
369
+ ```bash
370
+ pip install -e '.[viz]'
371
+ python examples/racing_demo.py --live --backend mujoco
372
+ ```
373
+
374
+ The MuJoCo view is a stylised 3-D kart, full road surface, alternating kerbs and a
375
+ follow camera. The underlying vehicle dynamics remain the differentiable torch model,
376
+ so this is a visualisation of the same Gymnasium state that the controller receives.
377
+
378
+ For a lecture, [`examples/race_control/`](examples/race_control/README.md) runs a live
379
+ leaderboard: students submit a name plus a JSON driver from the browser, and every
380
+ entry is raced on all three circuits, ranked, and replayable.
381
+
382
+ ## Student fuzzy-control workspace
383
+
384
+ The three-case-study starter workspace lives in
385
+ [`examples/student/`](examples/student/README.md). It fixes the simulation and
386
+ evaluation contracts while supplying neutral, trainable TSK fuzzy controllers for
387
+ the crane, kart, and quadrotor. Students can therefore design rules and fine-tune
388
+ membership/consequent parameters without accidentally changing the benchmark.
389
+
390
+ ## Upgrading from 0.1
391
+
392
+ **Existing systems will produce different numbers**, because 0.1 computed several things
393
+ incorrectly. The changes that move results:
394
+
395
+ - **Membership functions with a degenerate shoulder** (`a == b` or `b == c`) returned `0` at
396
+ their flat endpoint instead of `1`. `TriangularMF(0, 0, 5)` was 0.0 at `x = 0`, and
397
+ `TrapezoidalMF(7, 9, 10, 10)` was 0.0 at `x = 10`. Any rule base using shoulder sets at the
398
+ ends of a range -- the common case -- was firing weakly or not at all there.
399
+ - A `+1e-6` guard in the denominators biased ordinary interior values as well
400
+ (`TriangularMF(0, 0, 5)` gave `0.49999991` at the midpoint).
401
+ - **When no rule fires**, 0.1 returned `0.0`, which is not even inside a `[0, 30]` output
402
+ range. It is now `NaN` by default -- pass `no_rule_value=` for the old-style fallback.
403
+ - `Bisector` was one grid step low on symmetric sets.
404
+ - `GaussianMF` guarded its division with `sigma + 1e-6`, which only protects sigma
405
+ approaching zero from above and puts a singularity at `sigma = -1e-6` (NaN value *and*
406
+ NaN gradient). It now uses `|sigma|` clamped off zero, so sigma crossing zero during
407
+ training stays finite.
408
+
409
+ Also worth knowing:
410
+
411
+ - `pip install` from 0.1 shipped **no Python modules at all** (the package discovery config
412
+ matched nothing), so any working 0.1 install was a source checkout.
413
+ - `FuzzyRule` now registers its variables as submodules, so `rule.parameters()`,
414
+ `rule.to(device)` and `rule.state_dict()` work on a standalone rule.
415
+ - Scalar (0-dim) inputs used to raise `IndexError`; they now work and return a 0-dim output.
416
+
417
+ ## Tests
418
+
419
+ ```bash
420
+ pip install -e ".[test]"
421
+ pytest
422
+ ```
423
+
424
+ ## License
425
+
426
+ MIT