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.
Files changed (113) hide show
  1. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/CHANGELOG.md +34 -0
  2. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/CITATION.cff +2 -2
  3. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/PKG-INFO +8 -1
  4. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/README.md +7 -0
  5. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/ROADMAP.md +14 -4
  6. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/api/python.md +4 -0
  7. fuzzytool-0.5.1/docs/changelog.md +1 -0
  8. fuzzytool-0.5.1/docs/desktop.md +59 -0
  9. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/membership.md +6 -0
  10. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/type2.md +46 -0
  11. fuzzytool-0.5.1/docs/images/desktop_icon.png +0 -0
  12. fuzzytool-0.5.1/docs/images/gt2_zslices.png +0 -0
  13. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/__init__.py +9 -1
  14. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/defuzz.py +10 -5
  15. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/inference/mamdani.py +27 -10
  16. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/membership.py +9 -1
  17. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/type2/__init__.py +10 -0
  18. fuzzytool-0.5.1/fuzzytool/type2/general.py +211 -0
  19. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/mkdocs.yml +5 -0
  20. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/pyproject.toml +5 -1
  21. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_inference.py +26 -0
  22. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_membership.py +13 -0
  23. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_type2.py +56 -0
  24. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.github/workflows/ci.yml +0 -0
  25. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.github/workflows/docs.yml +0 -0
  26. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.github/workflows/release-pypi.yml +0 -0
  27. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.gitignore +0 -0
  28. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/.zenodo.json +0 -0
  29. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/CLAUDE.md +0 -0
  30. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/CONTRIBUTING.md +0 -0
  31. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/LICENSE +0 -0
  32. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/citing.md +0 -0
  33. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/extending.md +0 -0
  34. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/getting-started.md +0 -0
  35. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/anfis.md +0 -0
  36. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/batch-and-io.md +0 -0
  37. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/clustering.md +0 -0
  38. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/defuzzification.md +0 -0
  39. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/fraud-alert.md +0 -0
  40. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/ftransform.md +0 -0
  41. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/mamdani.md +0 -0
  42. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/mcdm.md +0 -0
  43. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/rule-learning.md +0 -0
  44. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/rules.md +0 -0
  45. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/tsk.md +0 -0
  46. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/guide/visualization.md +0 -0
  47. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/clusters.png +0 -0
  48. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/credit_control_surface.png +0 -0
  49. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/fraud_anomaly_terms.png +0 -0
  50. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/fraud_control_surface.png +0 -0
  51. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/gs_score_terms.png +0 -0
  52. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/it2_score.png +0 -0
  53. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/premium_terms.png +0 -0
  54. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/tut_advisor_surface.png +0 -0
  55. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/tut_advisor_volatility.png +0 -0
  56. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/tut_anfis.png +0 -0
  57. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/images/tut_clusters.png +0 -0
  58. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/index.md +0 -0
  59. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/installation.md +0 -0
  60. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/integrations.md +0 -0
  61. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/javascripts/mathjax.js +0 -0
  62. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/tutorials/anfis-learning.md +0 -0
  63. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/tutorials/clustering-segmentation.md +0 -0
  64. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/tutorials/risk-advisor.md +0 -0
  65. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/docs/tutorials/topsis-decision.md +0 -0
  66. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/anfis.py +0 -0
  67. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/clustering.py +0 -0
  68. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/credit_risk.py +0 -0
  69. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/credit_risk_it2.py +0 -0
  70. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/fraud_alert.py +0 -0
  71. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/ftransform.py +0 -0
  72. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/examples/mcdm.py +0 -0
  73. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/anfis.py +0 -0
  74. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/cluster.py +0 -0
  75. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/datasets.py +0 -0
  76. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/ftransform.py +0 -0
  77. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/fuzzynum.py +0 -0
  78. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/inference/__init__.py +0 -0
  79. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/inference/tsk.py +0 -0
  80. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/inference/tsukamoto.py +0 -0
  81. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/__init__.py +0 -0
  82. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/_util.py +0 -0
  83. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/agents.py +0 -0
  84. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/optuna.py +0 -0
  85. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/pandas.py +0 -0
  86. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/parallel.py +0 -0
  87. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/scipy.py +0 -0
  88. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/sklearn.py +0 -0
  89. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/torch.py +0 -0
  90. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/integrations/turboswarm.py +0 -0
  91. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/learn.py +0 -0
  92. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/mcdm.py +0 -0
  93. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/norms.py +0 -0
  94. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/rules.py +0 -0
  95. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/serialize.py +0 -0
  96. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/sets.py +0 -0
  97. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/type2/inference.py +0 -0
  98. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/type2/reduction.py +0 -0
  99. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/type2/sets.py +0 -0
  100. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/fuzzytool/viz.py +0 -0
  101. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/01_quickstart.ipynb +0 -0
  102. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/02_type2.ipynb +0 -0
  103. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/03_clustering.ipynb +0 -0
  104. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/04_learning.ipynb +0 -0
  105. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/notebooks/README.md +0 -0
  106. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/paper.bib +0 -0
  107. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/paper.md +0 -0
  108. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_cluster.py +0 -0
  109. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_engineering.py +0 -0
  110. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_integrations.py +0 -0
  111. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_learn.py +0 -0
  112. {fuzzytool-0.4.0 → fuzzytool-0.5.1}/tests/test_learning.py +0 -0
  113. {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.4.0
9
- date-released: 2026-06-28
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.4.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
- - General (non-interval) type-2; more defuzzifiers.
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
- - [ ] Register the PyPI trusted publisher, then tag `v0.1.0` to publish (the
86
- `release-pypi.yml` workflow is ready).
87
- - [ ] Enable the Zenodo GitHub integration and cut a release to mint a DOI.
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`).
@@ -62,6 +62,10 @@ Generated from docstrings with `mkdocstrings`.
62
62
 
63
63
  ::: fuzzytool.type2.reduction
64
64
 
65
+ ## General type-2 (zSlices)
66
+
67
+ ::: fuzzytool.type2.general
68
+
65
69
  ## Fuzzy clustering
66
70
 
67
71
  ::: fuzzytool.cluster
@@ -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
  ![IT2 score variable: each term's LMF and UMF with a shaded footprint of uncertainty](../images/it2_score.png)
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
+ ![A general type-2 set as nested z-slices (alpha-planes) over its footprint of uncertainty](../images/gt2_zslices.png)
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
+ ```
@@ -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.4.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 float(x[len(x) // 2]) # no rule fired: fall back to mid-universe
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 float(x[len(x) // 2])
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 float(x[len(x) // 2])
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 float(x[len(x) // 2])
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 float(x[len(x) // 2])
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.terms[r.consequent.term](var.universe)
82
- if self.implication == "min":
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.terms[r.consequent.term](var.universe)
112
- if self.implication == "min":
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
- return 1.0 / (1.0 + np.exp(-self.a * (x - self.c)))
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.4.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