fuzzytool 0.4.0__tar.gz → 0.5.1__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.
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/CHANGELOG.md +34 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/CITATION.cff +2 -2
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/PKG-INFO +8 -1
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/README.md +7 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/ROADMAP.md +14 -4
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/api/python.md +4 -0
- fuzzytool-0.5.1/docs/changelog.md +1 -0
- fuzzytool-0.5.1/docs/desktop.md +59 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/membership.md +6 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/type2.md +46 -0
- fuzzytool-0.5.1/docs/images/desktop_icon.png +0 -0
- fuzzytool-0.5.1/docs/images/gt2_zslices.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/__init__.py +9 -1
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/defuzz.py +10 -5
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/inference/mamdani.py +27 -10
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/membership.py +9 -1
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/type2/__init__.py +10 -0
- fuzzytool-0.5.1/fuzzytool/type2/general.py +211 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/mkdocs.yml +5 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/pyproject.toml +5 -1
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_inference.py +26 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_membership.py +13 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_type2.py +56 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.github/workflows/ci.yml +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.github/workflows/docs.yml +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.github/workflows/release-pypi.yml +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.gitignore +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.zenodo.json +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/CLAUDE.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/CONTRIBUTING.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/LICENSE +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/citing.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/extending.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/getting-started.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/anfis.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/batch-and-io.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/clustering.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/defuzzification.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/fraud-alert.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/ftransform.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/mamdani.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/mcdm.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/rule-learning.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/rules.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/tsk.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/visualization.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/clusters.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/credit_control_surface.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/fraud_anomaly_terms.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/fraud_control_surface.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/gs_score_terms.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/it2_score.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/premium_terms.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/tut_advisor_surface.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/tut_advisor_volatility.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/tut_anfis.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/tut_clusters.png +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/index.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/installation.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/integrations.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/javascripts/mathjax.js +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/tutorials/anfis-learning.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/tutorials/clustering-segmentation.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/tutorials/risk-advisor.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/tutorials/topsis-decision.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/anfis.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/clustering.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/credit_risk.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/credit_risk_it2.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/fraud_alert.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/ftransform.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/mcdm.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/anfis.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/cluster.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/datasets.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/ftransform.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/fuzzynum.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/inference/__init__.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/inference/tsk.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/inference/tsukamoto.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/__init__.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/_util.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/agents.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/optuna.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/pandas.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/parallel.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/scipy.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/sklearn.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/torch.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/turboswarm.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/learn.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/mcdm.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/norms.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/rules.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/serialize.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/sets.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/type2/inference.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/type2/reduction.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/type2/sets.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/viz.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/01_quickstart.ipynb +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/02_type2.ipynb +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/03_clustering.ipynb +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/04_learning.ipynb +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/README.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/paper.bib +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/paper.md +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_cluster.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_engineering.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_integrations.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_learn.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_learning.py +0 -0
- {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_mcdm.py +0 -0
|
@@ -6,6 +6,40 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.1] - 2026-07-07
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`sigmoid` membership function** is now numerically stable at extreme
|
|
14
|
+
arguments: `exp` is only ever applied to non-positive values, so large inputs
|
|
15
|
+
no longer trigger an overflow `RuntimeWarning` (they saturate cleanly to 0/1).
|
|
16
|
+
Output is unchanged in the numerically safe range.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`Mamdani` caches consequent set shapes** across inference calls. A
|
|
21
|
+
consequent term's membership over its output universe depends only on
|
|
22
|
+
`(term, universe)`, not on the inputs, so it is now memoized (keyed by the
|
|
23
|
+
membership-function identity, which auto-invalidates when a term is replaced).
|
|
24
|
+
Repeated `__call__`/`predict` runs are faster; results are identical.
|
|
25
|
+
- Internal cleanup: shared implication logic between `Mamdani.__call__` and
|
|
26
|
+
`Mamdani.predict`, and a single mid-universe fallback helper across the
|
|
27
|
+
defuzzifiers.
|
|
28
|
+
|
|
29
|
+
## [0.5.0] - 2026-06-28
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **General type-2 (GT2) fuzzy sets** via the zSlices / alpha-plane
|
|
34
|
+
representation (`fuzzytool.type2.general`):
|
|
35
|
+
- `GeneralType2MF` — a GT2 set as a stack of IT2 z-slices; also exposes the
|
|
36
|
+
overall FOU (`lower`/`upper`), so it can stand in for its IT2 footprint.
|
|
37
|
+
- Constructors `gt2_from_it2`, `gt2_gauss_uncertain_mean`, `gt2_scale`
|
|
38
|
+
(triangular secondary membership peaking at the principal MF).
|
|
39
|
+
- `GeneralType2Mamdani` — inference as a z-weighted stack of IT2 Mamdani
|
|
40
|
+
runs, reusing the Karnik-Mendel machinery.
|
|
41
|
+
- `centroid_gt2` — zSlices type reduction of a single GT2 set.
|
|
42
|
+
|
|
9
43
|
## [0.4.0] - 2026-06-28
|
|
10
44
|
|
|
11
45
|
### Added
|
|
@@ -5,8 +5,8 @@ type: software
|
|
|
5
5
|
authors:
|
|
6
6
|
- family-names: Salmeron
|
|
7
7
|
given-names: Jose L.
|
|
8
|
-
version: 0.
|
|
9
|
-
date-released: 2026-
|
|
8
|
+
version: 0.5.1
|
|
9
|
+
date-released: 2026-07-07
|
|
10
10
|
doi: 10.5281/zenodo.20836712
|
|
11
11
|
license: MIT
|
|
12
12
|
repository-code: "https://github.com/fuzzytool/fuzzytool.github.io"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fuzzytool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: A clean, extensible fuzzy-logic toolkit in pure Python + NumPy
|
|
5
5
|
Project-URL: Homepage, https://fuzzytool.github.io
|
|
6
6
|
Project-URL: Documentation, https://fuzzytool.github.io
|
|
@@ -118,6 +118,13 @@ s-norm, `~` the complement.
|
|
|
118
118
|
| 6 | Notebooks, JOSS `paper.md`, Zenodo DOI, PyPI release | ✅ |
|
|
119
119
|
| 7 | **Ecosystem integrations**: pandas, scikit-learn, PyTorch, SciPy, turboswarm, Optuna, Joblib/Dask, LLM agents | ✅ |
|
|
120
120
|
|
|
121
|
+
### v0.5.0
|
|
122
|
+
|
|
123
|
+
- **General type-2 (GT2) fuzzy sets** via the zSlices / alpha-plane
|
|
124
|
+
representation: `GeneralType2MF`, the `gt2_*` constructors,
|
|
125
|
+
`GeneralType2Mamdani` inference and `centroid_gt2` type reduction — reusing
|
|
126
|
+
the existing interval type-2 / Karnik-Mendel machinery.
|
|
127
|
+
|
|
121
128
|
### v0.4.0
|
|
122
129
|
|
|
123
130
|
- **turboswarm integration:** gradient-free, global tuning of a fuzzy system's
|
|
@@ -57,6 +57,13 @@ s-norm, `~` the complement.
|
|
|
57
57
|
| 6 | Notebooks, JOSS `paper.md`, Zenodo DOI, PyPI release | ✅ |
|
|
58
58
|
| 7 | **Ecosystem integrations**: pandas, scikit-learn, PyTorch, SciPy, turboswarm, Optuna, Joblib/Dask, LLM agents | ✅ |
|
|
59
59
|
|
|
60
|
+
### v0.5.0
|
|
61
|
+
|
|
62
|
+
- **General type-2 (GT2) fuzzy sets** via the zSlices / alpha-plane
|
|
63
|
+
representation: `GeneralType2MF`, the `gt2_*` constructors,
|
|
64
|
+
`GeneralType2Mamdani` inference and `centroid_gt2` type reduction — reusing
|
|
65
|
+
the existing interval type-2 / Karnik-Mendel machinery.
|
|
66
|
+
|
|
60
67
|
### v0.4.0
|
|
61
68
|
|
|
62
69
|
- **turboswarm integration:** gradient-free, global tuning of a fuzzy system's
|
|
@@ -72,16 +72,26 @@ Each pulls its dependency in only on import, behind its own extra.
|
|
|
72
72
|
- [x] **Agents (LangChain/LangGraph)** (`[agents]`): `explain` plus an
|
|
73
73
|
`inference_tool` LangChain tool that reports which rules fired.
|
|
74
74
|
|
|
75
|
+
## ✅ General type-2 (zSlices)
|
|
76
|
+
|
|
77
|
+
- [x] `GeneralType2MF` via the zSlices / alpha-plane representation (a stack of
|
|
78
|
+
IT2 slices; also exposes the overall FOU, so it degrades to IT2).
|
|
79
|
+
- [x] Constructors `gt2_from_it2`, `gt2_gauss_uncertain_mean`, `gt2_scale`
|
|
80
|
+
(triangular secondary MF).
|
|
81
|
+
- [x] `GeneralType2Mamdani` (z-weighted stack of IT2 Mamdani inferences) and
|
|
82
|
+
`centroid_gt2` zSlices type reduction.
|
|
83
|
+
|
|
75
84
|
## Ideas for later
|
|
76
85
|
|
|
77
86
|
- Fuzzy cognitive maps (+ grey FCM).
|
|
78
|
-
-
|
|
87
|
+
- More defuzzifiers; non-triangular GT2 secondary MFs.
|
|
79
88
|
|
|
80
89
|
## ⏳ Phase 6 — Release (v0.1.0)
|
|
81
90
|
|
|
82
91
|
- [x] Example notebooks (`notebooks/`) and a comparison page vs scikit-fuzzy.
|
|
83
92
|
- [x] JOSS `paper.md`; Zenodo metadata (`.zenodo.json`) and a citing/releasing page.
|
|
84
93
|
- [x] GitHub Pages live (`https://fuzzytool.github.io`).
|
|
85
|
-
- [
|
|
86
|
-
|
|
87
|
-
- [
|
|
94
|
+
- [x] PyPI trusted publisher registered; tagging a version runs `release-pypi.yml`
|
|
95
|
+
(published 0.1.0 → 0.4.0 via OIDC, no stored token).
|
|
96
|
+
- [x] Zenodo GitHub integration; each GitHub release is archived with a DOI
|
|
97
|
+
(`10.5281/zenodo.20836712`).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--8<-- "CHANGELOG.md"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Desktop app (prototype)
|
|
2
|
+
|
|
3
|
+
<img src="../images/desktop_icon.png" alt="fuzzytool studio icon" width="96" align="right" />
|
|
4
|
+
|
|
5
|
+
**fuzzytool studio** is a small desktop app — a [Tauri](https://tauri.app)
|
|
6
|
+
shell (Rust + system webview) driving the `fuzzytool` library through a **Python
|
|
7
|
+
sidecar**. It turns the library into an interactive workbench: pick or **build**
|
|
8
|
+
a fuzzy system, move the inputs, and watch the output, the fired rules and the
|
|
9
|
+
control surface update live.
|
|
10
|
+
|
|
11
|
+
It lives in [`desktop/`](https://github.com/fuzzytool/fuzzytool.github.io/tree/main/desktop)
|
|
12
|
+
in the repository. It is a prototype: not published as a binary, but fully
|
|
13
|
+
runnable from source.
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
- **Several built-in systems** — credit-risk premium, card-fraud alert, and an
|
|
18
|
+
investment-risk advisor — selectable from a dropdown.
|
|
19
|
+
- **Live inference** — sliders for each input; a gauge shows the crisp output and
|
|
20
|
+
a list shows which rules fired and how strongly (via
|
|
21
|
+
[`integrations.agents.explain`](integrations.md#llm-agents)).
|
|
22
|
+
- **Membership-function plots** for every variable and a **control-surface
|
|
23
|
+
heatmap** with X/Y variable pickers and a colour bar.
|
|
24
|
+
- **Configurable defuzzifier** (centroid, bisector, MOM/SOM/LOM), applied live.
|
|
25
|
+
- **A built-in editor** — create or edit variables, terms (`tri`/`trap`/`gauss`)
|
|
26
|
+
and rules (clauses combined with AND/OR, optional NOT, a consequent and a
|
|
27
|
+
weight), then **Apply** to compile and run the system instantly.
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
webview (sliders, plots, editor)
|
|
33
|
+
│ invoke("fuzzy_call", json)
|
|
34
|
+
▼
|
|
35
|
+
Rust (Tauri) ──stdin/stdout, line-delimited JSON──▶ Python sidecar (fuzzytool)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Systems are described by a plain-JSON **spec** (variables + typed terms +
|
|
39
|
+
clause-based rules) that the sidecar compiles into a `Mamdani`. The built-in
|
|
40
|
+
demos are predefined specs, and the editor sends the same shape — so demos and
|
|
41
|
+
hand-built systems share one engine. The frozen-sidecar build path (PyInstaller)
|
|
42
|
+
lets a packaged app run with no Python installed.
|
|
43
|
+
|
|
44
|
+
## Run it
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd desktop
|
|
48
|
+
npm install
|
|
49
|
+
|
|
50
|
+
# Point the sidecar at a Python that has fuzzytool installed:
|
|
51
|
+
export FUZZY_SIDECAR_PYTHON=/path/to/venv/bin/python
|
|
52
|
+
|
|
53
|
+
npm run tauri dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires Node, Rust with the [Tauri v2 prerequisites](https://tauri.app/start/prerequisites/),
|
|
57
|
+
and `fuzzytool` installed in the target Python. See the
|
|
58
|
+
[`desktop/README.md`](https://github.com/fuzzytool/fuzzytool.github.io/blob/main/desktop/README.md)
|
|
59
|
+
for the production build (frozen sidecar + `tauri build`).
|
|
@@ -19,6 +19,12 @@ m(5) # 1.0
|
|
|
19
19
|
m(np.array([3, 5, 7])) # vectorized
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
!!! note "Numerical stability"
|
|
23
|
+
`fz.sigmoid` uses an overflow-safe logistic: the exponential is only ever
|
|
24
|
+
evaluated on non-positive arguments, so even very large inputs saturate
|
|
25
|
+
cleanly to `0` or `1` without a `RuntimeWarning`. Output is unchanged within
|
|
26
|
+
the numerically safe range.
|
|
27
|
+
|
|
22
28
|
## Custom shapes
|
|
23
29
|
|
|
24
30
|
Any callable works — no registration needed:
|
|
@@ -82,3 +82,49 @@ plt.show()
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|

|
|
85
|
+
|
|
86
|
+
## General type-2 (zSlices)
|
|
87
|
+
|
|
88
|
+
An IT2 set treats every point inside its FOU as equally possible. A **general
|
|
89
|
+
type-2** set adds a *secondary* membership that weights those possibilities —
|
|
90
|
+
the third dimension IT2 discards. fuzzytool represents a GT2 set with the
|
|
91
|
+
**zSlices / alpha-plane** decomposition: slicing the secondary domain at levels
|
|
92
|
+
`z` turns the GT2 set into a stack of ordinary IT2 sets, so inference and type
|
|
93
|
+
reduction reuse the IT2 machinery and combine the slices weighted by `z`.
|
|
94
|
+
|
|
95
|
+
Each constructor builds a GT2 set from an IT2 footprint plus a **triangular
|
|
96
|
+
secondary** peaking at the principal (mid) MF — narrow at `z = 1`, the full FOU
|
|
97
|
+
at `z → 0`:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import fuzzytool as fz
|
|
101
|
+
|
|
102
|
+
score = fz.Variable("score", (300, 850))
|
|
103
|
+
score["good"] = fz.gt2_gauss_uncertain_mean(740, 780, 50, n_slices=5)
|
|
104
|
+
score["poor"] = fz.gt2_gauss_uncertain_mean(420, 480, 70, n_slices=5)
|
|
105
|
+
premium = fz.Variable("premium", (0, 12))
|
|
106
|
+
premium["low"] = fz.gt2_gauss_uncertain_mean(1.5, 2.5, 1.5)
|
|
107
|
+
premium["high"] = fz.gt2_gauss_uncertain_mean(9.5, 10.5, 1.5)
|
|
108
|
+
|
|
109
|
+
sys = fz.GeneralType2Mamdani()
|
|
110
|
+
sys.rule(score["good"], premium["low"])
|
|
111
|
+
sys.rule(score["poor"], premium["high"])
|
|
112
|
+
sys(score=780) # -> 2.31 (z-weighted average of per-slice IT2 results)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+

|
|
116
|
+
|
|
117
|
+
A GT2 term also exposes `lower`/`upper` (its overall FOU), so it can stand in for
|
|
118
|
+
its IT2 footprint inside an `IT2Mamdani` if you want the cheaper approximation.
|
|
119
|
+
Build a GT2 set from *any* IT2 footprint with
|
|
120
|
+
[`gt2_from_it2`][fuzzytool.type2.general.gt2_from_it2], and type-reduce a single
|
|
121
|
+
GT2 set with `centroid_gt2`:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import numpy as np
|
|
125
|
+
from fuzzytool.type2 import centroid_gt2
|
|
126
|
+
|
|
127
|
+
good = fz.gt2_gauss_uncertain_mean(740, 780, 50, n_slices=6)
|
|
128
|
+
universe = np.linspace(300, 850, 400)
|
|
129
|
+
centroid_gt2(good, universe) # -> ~754.7 (z-weighted centroid)
|
|
130
|
+
```
|
|
Binary file
|
|
Binary file
|
|
@@ -29,14 +29,19 @@ from .serialize import load, save
|
|
|
29
29
|
from .sets import Variable
|
|
30
30
|
from .type2 import (
|
|
31
31
|
IT2TSK,
|
|
32
|
+
GeneralType2Mamdani,
|
|
33
|
+
GeneralType2MF,
|
|
32
34
|
IT2Mamdani,
|
|
35
|
+
gt2_from_it2,
|
|
36
|
+
gt2_gauss_uncertain_mean,
|
|
37
|
+
gt2_scale,
|
|
33
38
|
it2,
|
|
34
39
|
it2_gauss_uncertain_mean,
|
|
35
40
|
it2_gauss_uncertain_std,
|
|
36
41
|
it2_scale,
|
|
37
42
|
)
|
|
38
43
|
|
|
39
|
-
__version__ = "0.
|
|
44
|
+
__version__ = "0.5.1"
|
|
40
45
|
|
|
41
46
|
__all__ = [
|
|
42
47
|
"__version__",
|
|
@@ -51,6 +56,9 @@ __all__ = [
|
|
|
51
56
|
# interval type-2
|
|
52
57
|
"IT2Mamdani", "IT2TSK",
|
|
53
58
|
"it2", "it2_scale", "it2_gauss_uncertain_mean", "it2_gauss_uncertain_std",
|
|
59
|
+
# general type-2
|
|
60
|
+
"GeneralType2Mamdani", "GeneralType2MF",
|
|
61
|
+
"gt2_from_it2", "gt2_gauss_uncertain_mean", "gt2_scale",
|
|
54
62
|
# fuzzy clustering
|
|
55
63
|
"fuzzy_cmeans", "gustafson_kessel", "possibilistic_cmeans",
|
|
56
64
|
# learning & approximation
|
|
@@ -15,11 +15,16 @@ import numpy as np
|
|
|
15
15
|
_trapezoid = np.trapezoid if hasattr(np, "trapezoid") else np.trapz
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _mid(x: np.ndarray) -> float:
|
|
19
|
+
"""Fallback crisp value when no rule fired (empty output set): mid-universe."""
|
|
20
|
+
return float(x[len(x) // 2])
|
|
21
|
+
|
|
22
|
+
|
|
18
23
|
def centroid(x: np.ndarray, y: np.ndarray) -> float:
|
|
19
24
|
"""Center of gravity of the area under ``y`` (the most common choice)."""
|
|
20
25
|
total = _trapezoid(y, x)
|
|
21
26
|
if total == 0:
|
|
22
|
-
return
|
|
27
|
+
return _mid(x)
|
|
23
28
|
return float(_trapezoid(x * y, x) / total)
|
|
24
29
|
|
|
25
30
|
|
|
@@ -30,7 +35,7 @@ def bisector(x: np.ndarray, y: np.ndarray) -> float:
|
|
|
30
35
|
seg = (y[:-1] + y[1:]) / 2.0 * dx
|
|
31
36
|
total = seg.sum()
|
|
32
37
|
if total == 0:
|
|
33
|
-
return
|
|
38
|
+
return _mid(x)
|
|
34
39
|
cum = np.concatenate([[0.0], np.cumsum(seg)])
|
|
35
40
|
idx = int(np.searchsorted(cum, total / 2.0))
|
|
36
41
|
return float(x[min(idx, len(x) - 1)])
|
|
@@ -40,7 +45,7 @@ def mom(x: np.ndarray, y: np.ndarray) -> float:
|
|
|
40
45
|
"""Mean of maxima."""
|
|
41
46
|
peak = y.max()
|
|
42
47
|
if peak == 0:
|
|
43
|
-
return
|
|
48
|
+
return _mid(x)
|
|
44
49
|
return float(x[np.isclose(y, peak)].mean())
|
|
45
50
|
|
|
46
51
|
|
|
@@ -48,7 +53,7 @@ def som(x: np.ndarray, y: np.ndarray) -> float:
|
|
|
48
53
|
"""Smallest of maxima."""
|
|
49
54
|
peak = y.max()
|
|
50
55
|
if peak == 0:
|
|
51
|
-
return
|
|
56
|
+
return _mid(x)
|
|
52
57
|
return float(x[np.isclose(y, peak)].min())
|
|
53
58
|
|
|
54
59
|
|
|
@@ -56,7 +61,7 @@ def lom(x: np.ndarray, y: np.ndarray) -> float:
|
|
|
56
61
|
"""Largest of maxima."""
|
|
57
62
|
peak = y.max()
|
|
58
63
|
if peak == 0:
|
|
59
|
-
return
|
|
64
|
+
return _mid(x)
|
|
60
65
|
return float(x[np.isclose(y, peak)].max())
|
|
61
66
|
|
|
62
67
|
|
|
@@ -52,6 +52,13 @@ class Mamdani:
|
|
|
52
52
|
self.defuzz = get_defuzzifier(defuzz)
|
|
53
53
|
self.rules: list[Rule] = []
|
|
54
54
|
self._outputs: dict[str, Variable] = {}
|
|
55
|
+
# Cache of consequent term shapes over their output universe, keyed by
|
|
56
|
+
# id(membership function). The shape depends only on (term, universe),
|
|
57
|
+
# not on the inputs, so it is stable across calls. Keying by id(mf)
|
|
58
|
+
# auto-invalidates when a term is replaced via ``Variable.__setitem__``
|
|
59
|
+
# (a new callable gets a new id); mutating an MF object in place is not
|
|
60
|
+
# part of the API and would leave a stale entry.
|
|
61
|
+
self._shape_cache: dict[int, np.ndarray] = {}
|
|
55
62
|
# Original (string) spec, kept for serialization.
|
|
56
63
|
self._spec = {"tnorm": tnorm, "snorm": snorm, "implication": implication,
|
|
57
64
|
"aggregation": aggregation, "defuzz": defuzz}
|
|
@@ -65,6 +72,22 @@ class Mamdani:
|
|
|
65
72
|
self._outputs[consequent.variable.name] = consequent.variable
|
|
66
73
|
return self
|
|
67
74
|
|
|
75
|
+
def _consequent_shape(self, var: Variable, term: str) -> np.ndarray:
|
|
76
|
+
"""Membership of ``term`` over ``var``'s universe, memoized by id(mf)."""
|
|
77
|
+
membership = var.terms[term]
|
|
78
|
+
key = id(membership)
|
|
79
|
+
shape = self._shape_cache.get(key)
|
|
80
|
+
if shape is None:
|
|
81
|
+
shape = np.asarray(membership(var.universe), dtype=float)
|
|
82
|
+
self._shape_cache[key] = shape
|
|
83
|
+
return shape
|
|
84
|
+
|
|
85
|
+
def _imply(self, firing, shape):
|
|
86
|
+
"""Shape a consequent set by a firing strength (clip or scale)."""
|
|
87
|
+
if self.implication == "min":
|
|
88
|
+
return np.minimum(firing, shape)
|
|
89
|
+
return firing * shape # prod
|
|
90
|
+
|
|
68
91
|
def __call__(self, **inputs: float):
|
|
69
92
|
"""Run inference. Returns a float for one output, else a dict by name."""
|
|
70
93
|
if not self.rules:
|
|
@@ -78,11 +101,8 @@ class Mamdani:
|
|
|
78
101
|
if firing <= 0.0:
|
|
79
102
|
continue
|
|
80
103
|
var = r.consequent.variable
|
|
81
|
-
shape = var
|
|
82
|
-
|
|
83
|
-
shaped = np.minimum(firing, shape)
|
|
84
|
-
else: # prod
|
|
85
|
-
shaped = firing * shape
|
|
104
|
+
shape = self._consequent_shape(var, r.consequent.term)
|
|
105
|
+
shaped = self._imply(firing, shape)
|
|
86
106
|
agg[var.name] = self.aggregation(agg[var.name], shaped)
|
|
87
107
|
|
|
88
108
|
crisp = {name: self.defuzz(self._outputs[name].universe, y)
|
|
@@ -108,11 +128,8 @@ class Mamdani:
|
|
|
108
128
|
continue
|
|
109
129
|
firing = np.asarray(r.antecedent.eval(arrs, self.tnorm, self.snorm),
|
|
110
130
|
dtype=float) * r.weight
|
|
111
|
-
shape = var
|
|
112
|
-
|
|
113
|
-
shaped = np.minimum(firing[:, None], shape[None, :])
|
|
114
|
-
else:
|
|
115
|
-
shaped = firing[:, None] * shape[None, :]
|
|
131
|
+
shape = self._consequent_shape(var, r.consequent.term)
|
|
132
|
+
shaped = self._imply(firing[:, None], shape[None, :])
|
|
116
133
|
agg = self.aggregation(agg, shaped)
|
|
117
134
|
x = var.universe
|
|
118
135
|
out[name] = np.array([self.defuzz(x, agg[i]) for i in range(n)])
|
|
@@ -111,7 +111,15 @@ class Sigmoid:
|
|
|
111
111
|
|
|
112
112
|
def __call__(self, x):
|
|
113
113
|
x = np.asarray(x, dtype=float)
|
|
114
|
-
|
|
114
|
+
z = self.a * (x - self.c)
|
|
115
|
+
# Numerically stable logistic: exp is only ever applied to non-positive
|
|
116
|
+
# arguments, so it never overflows (exp(+large) -> inf is avoided).
|
|
117
|
+
pos = z >= 0
|
|
118
|
+
out = np.empty_like(z)
|
|
119
|
+
out[pos] = 1.0 / (1.0 + np.exp(-z[pos]))
|
|
120
|
+
ez = np.exp(z[~pos])
|
|
121
|
+
out[~pos] = ez / (1.0 + ez)
|
|
122
|
+
return out[()] if out.ndim == 0 else out
|
|
115
123
|
|
|
116
124
|
def inverse(self, y: float) -> float:
|
|
117
125
|
"""The ``x`` at which membership equals ``y`` (for Tsukamoto inference)."""
|
|
@@ -9,6 +9,14 @@ IT2 rules reuse the very same operator syntax as type-1 rules (``|``, ``&``,
|
|
|
9
9
|
tree and collapse the result with Karnik-Mendel type reduction.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
from .general import (
|
|
13
|
+
GeneralType2Mamdani,
|
|
14
|
+
GeneralType2MF,
|
|
15
|
+
centroid_gt2,
|
|
16
|
+
gt2_from_it2,
|
|
17
|
+
gt2_gauss_uncertain_mean,
|
|
18
|
+
gt2_scale,
|
|
19
|
+
)
|
|
12
20
|
from .inference import IT2TSK, IT2Mamdani
|
|
13
21
|
from .reduction import centroid_it2, karnik_mendel, km_endpoint
|
|
14
22
|
from .sets import (
|
|
@@ -24,4 +32,6 @@ __all__ = [
|
|
|
24
32
|
"it2", "it2_scale", "it2_gauss_uncertain_mean", "it2_gauss_uncertain_std",
|
|
25
33
|
"IT2Mamdani", "IT2TSK",
|
|
26
34
|
"karnik_mendel", "km_endpoint", "centroid_it2",
|
|
35
|
+
"GeneralType2MF", "GeneralType2Mamdani", "centroid_gt2",
|
|
36
|
+
"gt2_from_it2", "gt2_gauss_uncertain_mean", "gt2_scale",
|
|
27
37
|
]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""General type-2 (GT2) fuzzy sets via the zSlices / alpha-plane representation.
|
|
2
|
+
|
|
3
|
+
An interval type-2 set treats every point inside its footprint of uncertainty
|
|
4
|
+
(FOU) as equally possible. A **general** type-2 set adds a *secondary* membership
|
|
5
|
+
that weights those possibilities — the third dimension IT2 throws away.
|
|
6
|
+
|
|
7
|
+
A practical, exact way to handle that third dimension is the **zSlices** (alpha
|
|
8
|
+
-plane) decomposition (Wagner & Hagras): slice the secondary domain at levels
|
|
9
|
+
``z`` and, at each level, the GT2 set reduces to an ordinary IT2 set whose FOU is
|
|
10
|
+
the ``z``-cut. Inference and type reduction then run the existing IT2 machinery
|
|
11
|
+
on each slice and combine the results, weighted by ``z``.
|
|
12
|
+
|
|
13
|
+
Here a GT2 set is built from an IT2 footprint plus a **triangular secondary**
|
|
14
|
+
peaking at the FOU's principal (mid) MF: at ``z -> 0`` a slice is the full FOU,
|
|
15
|
+
at ``z = 1`` it collapses to the principal MF.
|
|
16
|
+
|
|
17
|
+
* :class:`GeneralType2MF` — a stack of IT2 z-slices (also exposes the overall
|
|
18
|
+
FOU via ``lower``/``upper``, so it degrades gracefully to IT2).
|
|
19
|
+
* :func:`gt2_from_it2`, :func:`gt2_gauss_uncertain_mean`, :func:`gt2_scale` —
|
|
20
|
+
constructors.
|
|
21
|
+
* :class:`GeneralType2Mamdani` — z-weighted stack of IT2 Mamdani inferences.
|
|
22
|
+
* :func:`centroid_gt2` — zSlices centroid of a single GT2 set.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
from ..sets import Variable
|
|
30
|
+
from .inference import IT2Mamdani
|
|
31
|
+
from .reduction import centroid_it2
|
|
32
|
+
from .sets import IntervalType2MF, it2
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GeneralType2MF:
|
|
36
|
+
"""A general type-2 set represented as a stack of IT2 ``z``-slices.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
slices: IT2 sets, one per ``z`` level, narrowing toward the principal MF
|
|
40
|
+
as ``z`` increases.
|
|
41
|
+
zlevels: the secondary levels (in ``(0, 1]``), ascending, aligned with
|
|
42
|
+
``slices``.
|
|
43
|
+
footprint: the overall FOU as an IT2 set (used for ``lower``/``upper`` so
|
|
44
|
+
the GT2 set can stand in for its IT2 footprint).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, slices: list[IntervalType2MF], zlevels: np.ndarray,
|
|
48
|
+
footprint: IntervalType2MF) -> None:
|
|
49
|
+
self.slices = list(slices)
|
|
50
|
+
self.zlevels = np.asarray(zlevels, dtype=float)
|
|
51
|
+
self._footprint = footprint
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def n_slices(self) -> int:
|
|
55
|
+
return len(self.slices)
|
|
56
|
+
|
|
57
|
+
def slice(self, i: int) -> IntervalType2MF:
|
|
58
|
+
"""The IT2 set at ``z``-level index ``i``."""
|
|
59
|
+
return self.slices[i]
|
|
60
|
+
|
|
61
|
+
def lower(self, x):
|
|
62
|
+
return self._footprint.lower(x)
|
|
63
|
+
|
|
64
|
+
def upper(self, x):
|
|
65
|
+
return self._footprint.upper(x)
|
|
66
|
+
|
|
67
|
+
def __call__(self, x):
|
|
68
|
+
return self._footprint(x)
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
return f"GeneralType2MF(n_slices={self.n_slices})"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def gt2_from_it2(footprint: IntervalType2MF, n_slices: int = 5) -> GeneralType2MF:
|
|
75
|
+
"""Build a GT2 set from an IT2 FOU with a triangular secondary MF.
|
|
76
|
+
|
|
77
|
+
The secondary membership peaks at the principal MF (the midline of the FOU)
|
|
78
|
+
and falls linearly to 0 at the LMF/UMF. The ``z``-slice at level ``z`` is the
|
|
79
|
+
IT2 set whose FOU is narrowed from the full footprint toward that midline by
|
|
80
|
+
a factor ``(1 - z)``.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
footprint: the IT2 set describing the full footprint of uncertainty.
|
|
84
|
+
n_slices: number of ``z`` levels (``z = 1/n .. 1``). More slices = a finer
|
|
85
|
+
secondary, at proportional cost.
|
|
86
|
+
"""
|
|
87
|
+
if n_slices < 1:
|
|
88
|
+
raise ValueError("n_slices must be >= 1")
|
|
89
|
+
zlevels = np.linspace(1.0 / n_slices, 1.0, n_slices)
|
|
90
|
+
|
|
91
|
+
def mid(x):
|
|
92
|
+
return (footprint.lower(x) + footprint.upper(x)) / 2.0
|
|
93
|
+
|
|
94
|
+
slices = []
|
|
95
|
+
for z in zlevels:
|
|
96
|
+
# Default-arg binding captures the current z in each closure.
|
|
97
|
+
def lower(x, z=z):
|
|
98
|
+
m = mid(x)
|
|
99
|
+
return m - (1.0 - z) * (m - footprint.lower(x))
|
|
100
|
+
|
|
101
|
+
def upper(x, z=z):
|
|
102
|
+
m = mid(x)
|
|
103
|
+
return m + (1.0 - z) * (footprint.upper(x) - m)
|
|
104
|
+
|
|
105
|
+
slices.append(it2(lower, upper))
|
|
106
|
+
return GeneralType2MF(slices, zlevels, footprint)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def gt2_gauss_uncertain_mean(c1: float, c2: float, sigma: float,
|
|
110
|
+
n_slices: int = 5) -> GeneralType2MF:
|
|
111
|
+
"""GT2 Gaussian with an uncertain mean in ``[c1, c2]`` (triangular secondary)."""
|
|
112
|
+
from .sets import it2_gauss_uncertain_mean
|
|
113
|
+
return gt2_from_it2(it2_gauss_uncertain_mean(c1, c2, sigma), n_slices)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def gt2_scale(mf, scale: float, n_slices: int = 5) -> GeneralType2MF:
|
|
117
|
+
"""GT2 set from a height-uncertainty FOU (UMF ``mf``, LMF ``scale * mf``)."""
|
|
118
|
+
from .sets import it2_scale
|
|
119
|
+
return gt2_from_it2(it2_scale(mf, scale), n_slices)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def centroid_gt2(gt2mf: GeneralType2MF, universe) -> float:
|
|
123
|
+
"""zSlices centroid of a GT2 set: ``z``-weighted mean of per-slice IT2 centroids."""
|
|
124
|
+
z = gt2mf.zlevels
|
|
125
|
+
mids = np.array([sum(centroid_it2(gt2mf.slice(i), universe)) / 2.0
|
|
126
|
+
for i in range(gt2mf.n_slices)])
|
|
127
|
+
return float(np.sum(z * mids) / np.sum(z))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _antecedent_variables(node, out: dict) -> None:
|
|
131
|
+
if hasattr(node, "variable"):
|
|
132
|
+
out.setdefault(id(node.variable), node.variable)
|
|
133
|
+
if hasattr(node, "left"):
|
|
134
|
+
_antecedent_variables(node.left, out)
|
|
135
|
+
_antecedent_variables(node.right, out)
|
|
136
|
+
if hasattr(node, "operand"):
|
|
137
|
+
_antecedent_variables(node.operand, out)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class GeneralType2Mamdani:
|
|
141
|
+
"""General type-2 Mamdani inference via the zSlices decomposition.
|
|
142
|
+
|
|
143
|
+
At each ``z`` level the GT2 terms collapse to their IT2 ``z``-slice and the
|
|
144
|
+
existing :class:`~fuzzytool.type2.inference.IT2Mamdani` engine
|
|
145
|
+
(center-of-sets type reduction) produces a crisp output ``y(z)``. The final
|
|
146
|
+
result is the ``z``-weighted average ``Σ z·y(z) / Σ z`` — the centroid of the
|
|
147
|
+
type-reduced general type-2 output.
|
|
148
|
+
|
|
149
|
+
Variables may mix GT2 and IT2/type-1 terms; only the GT2 terms are sliced.
|
|
150
|
+
All GT2 terms must share the same ``z`` levels (same ``n_slices``).
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, tnorm="min", snorm="max") -> None:
|
|
154
|
+
self._it2 = IT2Mamdani(tnorm, snorm)
|
|
155
|
+
|
|
156
|
+
def rule(self, antecedent, consequent, weight: float = 1.0) -> GeneralType2Mamdani:
|
|
157
|
+
"""Add ``IF antecedent THEN output is term`` and return ``self``."""
|
|
158
|
+
self._it2.rule(antecedent, consequent, weight)
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def rules(self):
|
|
163
|
+
return self._it2.rules
|
|
164
|
+
|
|
165
|
+
def _gt2_variables(self) -> list[Variable]:
|
|
166
|
+
found: dict[int, Variable] = {}
|
|
167
|
+
for r in self._it2.rules:
|
|
168
|
+
_antecedent_variables(r.antecedent, found)
|
|
169
|
+
found.setdefault(id(r.consequent.variable), r.consequent.variable)
|
|
170
|
+
return [v for v in found.values()
|
|
171
|
+
if any(isinstance(t, GeneralType2MF) for t in v.terms.values())]
|
|
172
|
+
|
|
173
|
+
def __call__(self, **inputs: float):
|
|
174
|
+
"""Run inference. Returns a float for one output, else a dict by name."""
|
|
175
|
+
if not self._it2.rules:
|
|
176
|
+
raise RuntimeError("no rules defined")
|
|
177
|
+
variables = self._gt2_variables()
|
|
178
|
+
if not variables:
|
|
179
|
+
raise RuntimeError("no general type-2 terms found; use IT2Mamdani instead")
|
|
180
|
+
zlevels = next(t.zlevels for v in variables for t in v.terms.values()
|
|
181
|
+
if isinstance(t, GeneralType2MF))
|
|
182
|
+
|
|
183
|
+
z_acc, y_acc = [], []
|
|
184
|
+
for i, z in enumerate(zlevels):
|
|
185
|
+
saved = {id(v): v.terms for v in variables}
|
|
186
|
+
try:
|
|
187
|
+
for v in variables:
|
|
188
|
+
v.terms = {name: (t.slice(i) if isinstance(t, GeneralType2MF) else t)
|
|
189
|
+
for name, t in v.terms.items()}
|
|
190
|
+
self._it2._centroids.clear()
|
|
191
|
+
y = self._it2(**inputs)
|
|
192
|
+
finally:
|
|
193
|
+
for v in variables:
|
|
194
|
+
v.terms = saved[id(v)]
|
|
195
|
+
z_acc.append(float(z))
|
|
196
|
+
y_acc.append(y)
|
|
197
|
+
|
|
198
|
+
z = np.asarray(z_acc)
|
|
199
|
+
if isinstance(y_acc[0], dict):
|
|
200
|
+
return {k: float(np.sum(z * np.array([y[k] for y in y_acc])) / np.sum(z))
|
|
201
|
+
for k in y_acc[0]}
|
|
202
|
+
return float(np.sum(z * np.asarray(y_acc)) / np.sum(z))
|
|
203
|
+
|
|
204
|
+
def __repr__(self) -> str:
|
|
205
|
+
return f"GeneralType2Mamdani(rules={len(self._it2.rules)})"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
__all__ = [
|
|
209
|
+
"GeneralType2MF", "gt2_from_it2", "gt2_gauss_uncertain_mean", "gt2_scale",
|
|
210
|
+
"centroid_gt2", "GeneralType2Mamdani",
|
|
211
|
+
]
|
|
@@ -56,9 +56,11 @@ nav:
|
|
|
56
56
|
- Defuzzification: guide/defuzzification.md
|
|
57
57
|
- Visualization: guide/visualization.md
|
|
58
58
|
- Integrations: integrations.md
|
|
59
|
+
- Desktop app: desktop.md
|
|
59
60
|
- Extending: extending.md
|
|
60
61
|
- Citing & releasing: citing.md
|
|
61
62
|
- API reference: api/python.md
|
|
63
|
+
- Changelog: changelog.md
|
|
62
64
|
|
|
63
65
|
markdown_extensions:
|
|
64
66
|
- admonition
|
|
@@ -73,6 +75,9 @@ markdown_extensions:
|
|
|
73
75
|
- pymdownx.superfences
|
|
74
76
|
- pymdownx.tabbed:
|
|
75
77
|
alternate_style: true
|
|
78
|
+
- pymdownx.snippets:
|
|
79
|
+
base_path: ["."]
|
|
80
|
+
check_paths: true
|
|
76
81
|
|
|
77
82
|
extra:
|
|
78
83
|
social:
|
|
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "fuzzytool"
|
|
11
|
-
version = "0.
|
|
11
|
+
version = "0.5.1"
|
|
12
12
|
description = "A clean, extensible fuzzy-logic toolkit in pure Python + NumPy"
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.9"
|
|
@@ -50,6 +50,10 @@ Issues = "https://github.com/fuzzytool/fuzzytool.github.io/issues"
|
|
|
50
50
|
[tool.hatch.build.targets.wheel]
|
|
51
51
|
packages = ["fuzzytool"]
|
|
52
52
|
|
|
53
|
+
[tool.hatch.build.targets.sdist]
|
|
54
|
+
# The Tauri desktop prototype is not part of the Python distribution.
|
|
55
|
+
exclude = ["desktop"]
|
|
56
|
+
|
|
53
57
|
[tool.pytest.ini_options]
|
|
54
58
|
addopts = "--doctest-modules --doctest-glob='*.md'"
|
|
55
59
|
testpaths = ["tests", "fuzzytool"]
|
|
@@ -47,3 +47,29 @@ def test_centroid_handles_no_rule_fired():
|
|
|
47
47
|
# An output universe sampled symmetrically falls back to its midpoint.
|
|
48
48
|
x = np.linspace(0, 12, 501)
|
|
49
49
|
assert fz.defuzz.centroid(x, np.zeros_like(x)) == pytest.approx(6.0, abs=0.1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_mamdani_caches_consequent_shapes_and_invalidates_on_replace():
|
|
53
|
+
sys, _, _, premium = datasets.credit_risk()
|
|
54
|
+
sys._shape_cache.clear()
|
|
55
|
+
|
|
56
|
+
# Calling twice reuses cached shapes (same array objects, never recomputed).
|
|
57
|
+
sys(score=650, dti=25)
|
|
58
|
+
cached = {k: id(v) for k, v in sys._shape_cache.items()}
|
|
59
|
+
assert cached # at least one consequent term was shaped
|
|
60
|
+
sys(score=650, dti=25)
|
|
61
|
+
for key, arr_id in cached.items():
|
|
62
|
+
assert id(sys._shape_cache[key]) == arr_id
|
|
63
|
+
|
|
64
|
+
# predict shares the cache and agrees with scalar calls.
|
|
65
|
+
pts = [(800, 10), (520, 42), (650, 25)]
|
|
66
|
+
batch = sys.predict(score=np.array([s for s, _ in pts]),
|
|
67
|
+
dti=np.array([d for _, d in pts]))
|
|
68
|
+
scalar = np.array([sys(score=s, dti=d) for s, d in pts])
|
|
69
|
+
assert np.allclose(batch, scalar)
|
|
70
|
+
|
|
71
|
+
# Replacing the consequent term that fires (new MF id) invalidates the
|
|
72
|
+
# cache: for these inputs only the "medium" rule fires.
|
|
73
|
+
before = sys(score=650, dti=25)
|
|
74
|
+
premium["medium"] = fz.tri(7, 9, 11)
|
|
75
|
+
assert sys(score=650, dti=25) != before
|
|
@@ -38,3 +38,16 @@ def test_invalid_params_raise():
|
|
|
38
38
|
fz.tri(5, 0, 10)
|
|
39
39
|
with pytest.raises(ValueError):
|
|
40
40
|
fz.gauss(0, 0)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_sigmoid_is_stable_at_extreme_arguments():
|
|
44
|
+
s = fz.sigmoid(2.0, 5.0)
|
|
45
|
+
with np.errstate(over="raise"): # overflow in exp would raise here
|
|
46
|
+
y = s(np.array([-1e6, 0.0, 1e6]))
|
|
47
|
+
assert np.all(np.isfinite(y)) and np.all((y >= 0) & (y <= 1))
|
|
48
|
+
assert y[0] == 0.0 and y[-1] == 1.0
|
|
49
|
+
# Still matches the plain logistic in the numerically safe range.
|
|
50
|
+
x = np.linspace(-20, 30, 500)
|
|
51
|
+
assert np.allclose(s(x), 1.0 / (1.0 + np.exp(-2.0 * (x - 5.0))), atol=1e-12)
|
|
52
|
+
# Scalar input yields a scalar-like (0-d) result.
|
|
53
|
+
assert np.ndim(s(5.0)) == 0
|
|
@@ -75,3 +75,59 @@ def test_type1_and_it2_terms_mix_in_one_antecedent():
|
|
|
75
75
|
lo, hi = (a["hi"] & b["hi"]).eval_interval({"a": 0.8, "b": 1.0},
|
|
76
76
|
fz.norms.t_min, fz.norms.s_max)
|
|
77
77
|
assert lo <= hi
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# --- general type-2 (zSlices) -----------------------------------------------
|
|
81
|
+
|
|
82
|
+
def test_gt2_exposes_footprint_and_slices():
|
|
83
|
+
g = fz.gt2_gauss_uncertain_mean(740, 780, 50, n_slices=5)
|
|
84
|
+
x = np.linspace(300, 850, 200)
|
|
85
|
+
# degrades to its IT2 footprint via lower/upper
|
|
86
|
+
assert np.all(g.lower(x) <= g.upper(x) + 1e-12)
|
|
87
|
+
assert g.n_slices == 5 and len(g.zlevels) == 5
|
|
88
|
+
# the z=1 slice (last) collapses toward the principal MF: near-zero FOU
|
|
89
|
+
top = g.slice(g.n_slices - 1)
|
|
90
|
+
assert np.max(top.upper(x) - top.lower(x)) < 1e-9
|
|
91
|
+
# the lowest slice spans (almost) the full footprint
|
|
92
|
+
bottom = g.slice(0)
|
|
93
|
+
assert np.max(bottom.upper(x) - bottom.lower(x)) > 0.0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_gt2_mamdani_runs_and_is_bounded():
|
|
97
|
+
score = fz.Variable("score", (300, 850))
|
|
98
|
+
score["good"] = fz.gt2_gauss_uncertain_mean(740, 780, 50)
|
|
99
|
+
score["poor"] = fz.gt2_gauss_uncertain_mean(420, 480, 70)
|
|
100
|
+
premium = fz.Variable("premium", (0, 12))
|
|
101
|
+
premium["low"] = fz.gt2_gauss_uncertain_mean(1.5, 2.5, 1.5)
|
|
102
|
+
premium["high"] = fz.gt2_gauss_uncertain_mean(9.5, 10.5, 1.5)
|
|
103
|
+
|
|
104
|
+
sys = fz.GeneralType2Mamdani()
|
|
105
|
+
sys.rule(score["good"], premium["low"])
|
|
106
|
+
sys.rule(score["poor"], premium["high"])
|
|
107
|
+
# a good score gives a low premium, a poor score a high one
|
|
108
|
+
assert sys(score=780) < sys(score=450)
|
|
109
|
+
assert 0.0 <= sys(score=780) <= 12.0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_gt2_does_not_mutate_terms_after_call():
|
|
113
|
+
score = fz.Variable("score", (300, 850))
|
|
114
|
+
score["good"] = fz.gt2_gauss_uncertain_mean(740, 780, 50)
|
|
115
|
+
premium = fz.Variable("premium", (0, 12))
|
|
116
|
+
premium["low"] = fz.gt2_gauss_uncertain_mean(1.5, 2.5, 1.5)
|
|
117
|
+
sys = fz.GeneralType2Mamdani()
|
|
118
|
+
sys.rule(score["good"], premium["low"])
|
|
119
|
+
sys(score=760)
|
|
120
|
+
# terms are restored to the original GT2 objects (not left as a slice)
|
|
121
|
+
from fuzzytool.type2 import GeneralType2MF
|
|
122
|
+
assert isinstance(score.terms["good"], GeneralType2MF)
|
|
123
|
+
assert isinstance(premium.terms["low"], GeneralType2MF)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_centroid_gt2_matches_it2_order():
|
|
127
|
+
from fuzzytool.type2 import centroid_gt2
|
|
128
|
+
g = fz.gt2_gauss_uncertain_mean(740, 780, 50, n_slices=6)
|
|
129
|
+
uni = np.linspace(300, 850, 400)
|
|
130
|
+
c_gt2 = centroid_gt2(g, uni)
|
|
131
|
+
c_it2 = sum(centroid_it2(g, uni)) / 2.0
|
|
132
|
+
assert 700 < c_gt2 < 820
|
|
133
|
+
assert abs(c_gt2 - c_it2) < 20 # close, but GT2 weights the principal MF
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|