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.
- torch_fis-0.2.0/LICENSE +21 -0
- torch_fis-0.2.0/PKG-INFO +426 -0
- torch_fis-0.2.0/README.md +385 -0
- torch_fis-0.2.0/__init__.py +29 -0
- torch_fis-0.2.0/config.py +344 -0
- torch_fis-0.2.0/consequent.py +202 -0
- torch_fis-0.2.0/defuzz.py +108 -0
- torch_fis-0.2.0/envs/__init__.py +42 -0
- torch_fis-0.2.0/envs/assets/__init__.py +167 -0
- torch_fis-0.2.0/envs/controllers/__init__.py +7 -0
- torch_fis-0.2.0/envs/controllers/base.py +24 -0
- torch_fis-0.2.0/envs/controllers/pid.py +107 -0
- torch_fis-0.2.0/envs/controllers/racing.py +140 -0
- torch_fis-0.2.0/envs/core/__init__.py +0 -0
- torch_fis-0.2.0/envs/core/gym_env.py +164 -0
- torch_fis-0.2.0/envs/core/integrate.py +78 -0
- torch_fis-0.2.0/envs/core/metrics.py +131 -0
- torch_fis-0.2.0/envs/core/modes.py +37 -0
- torch_fis-0.2.0/envs/core/plant.py +165 -0
- torch_fis-0.2.0/envs/core/render.py +372 -0
- torch_fis-0.2.0/envs/core/scenarios.py +86 -0
- torch_fis-0.2.0/envs/core/video.py +189 -0
- torch_fis-0.2.0/envs/core/wrappers.py +80 -0
- torch_fis-0.2.0/envs/plants/__init__.py +5 -0
- torch_fis-0.2.0/envs/plants/crane.py +203 -0
- torch_fis-0.2.0/envs/plants/drone.py +166 -0
- torch_fis-0.2.0/envs/plants/robot.py +242 -0
- torch_fis-0.2.0/envs/racing.py +513 -0
- torch_fis-0.2.0/membership.py +115 -0
- torch_fis-0.2.0/norm.py +41 -0
- torch_fis-0.2.0/pyproject.toml +79 -0
- torch_fis-0.2.0/rule.py +130 -0
- torch_fis-0.2.0/setup.cfg +4 -0
- torch_fis-0.2.0/system.py +387 -0
- torch_fis-0.2.0/tests/test_config.py +122 -0
- torch_fis-0.2.0/tests/test_defuzz.py +93 -0
- torch_fis-0.2.0/tests/test_envs_controllers.py +166 -0
- torch_fis-0.2.0/tests/test_envs_crane.py +172 -0
- torch_fis-0.2.0/tests/test_envs_crossvalidation.py +126 -0
- torch_fis-0.2.0/tests/test_envs_drone.py +56 -0
- torch_fis-0.2.0/tests/test_envs_gym.py +314 -0
- torch_fis-0.2.0/tests/test_envs_integrate.py +115 -0
- torch_fis-0.2.0/tests/test_envs_racing.py +121 -0
- torch_fis-0.2.0/tests/test_envs_video.py +68 -0
- torch_fis-0.2.0/tests/test_grad.py +126 -0
- torch_fis-0.2.0/tests/test_mamdani.py +164 -0
- torch_fis-0.2.0/tests/test_membership.py +134 -0
- torch_fis-0.2.0/tests/test_shapes.py +126 -0
- torch_fis-0.2.0/tests/test_tsk.py +267 -0
- torch_fis-0.2.0/tests/test_viz.py +130 -0
- torch_fis-0.2.0/torch_fis.egg-info/PKG-INFO +426 -0
- torch_fis-0.2.0/torch_fis.egg-info/SOURCES.txt +86 -0
- torch_fis-0.2.0/torch_fis.egg-info/dependency_links.txt +1 -0
- torch_fis-0.2.0/torch_fis.egg-info/requires.txt +23 -0
- torch_fis-0.2.0/torch_fis.egg-info/top_level.txt +1 -0
- torch_fis-0.2.0/variable.py +43 -0
- torch_fis-0.2.0/viz.py +356 -0
torch_fis-0.2.0/LICENSE
ADDED
|
@@ -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.
|
torch_fis-0.2.0/PKG-INFO
ADDED
|
@@ -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
|