fuzzytool 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/CHANGELOG.md +16 -0
  2. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/CITATION.cff +2 -2
  3. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/PKG-INFO +10 -2
  4. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/README.md +7 -1
  5. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/ROADMAP.md +2 -1
  6. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/api/python.md +4 -0
  7. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/integrations.md +33 -0
  8. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/__init__.py +1 -1
  9. fuzzytool-0.4.0/fuzzytool/integrations/_util.py +96 -0
  10. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/integrations/scipy.py +9 -50
  11. fuzzytool-0.4.0/fuzzytool/integrations/turboswarm.py +120 -0
  12. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/pyproject.toml +2 -1
  13. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/tests/test_integrations.py +20 -0
  14. fuzzytool-0.3.0/fuzzytool/integrations/_util.py +0 -39
  15. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/.github/workflows/ci.yml +0 -0
  16. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/.github/workflows/docs.yml +0 -0
  17. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/.github/workflows/release-pypi.yml +0 -0
  18. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/.gitignore +0 -0
  19. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/.zenodo.json +0 -0
  20. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/CLAUDE.md +0 -0
  21. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/CONTRIBUTING.md +0 -0
  22. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/LICENSE +0 -0
  23. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/citing.md +0 -0
  24. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/extending.md +0 -0
  25. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/getting-started.md +0 -0
  26. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/anfis.md +0 -0
  27. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/batch-and-io.md +0 -0
  28. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/clustering.md +0 -0
  29. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/defuzzification.md +0 -0
  30. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/fraud-alert.md +0 -0
  31. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/ftransform.md +0 -0
  32. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/mamdani.md +0 -0
  33. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/mcdm.md +0 -0
  34. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/membership.md +0 -0
  35. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/rule-learning.md +0 -0
  36. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/rules.md +0 -0
  37. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/tsk.md +0 -0
  38. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/type2.md +0 -0
  39. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/guide/visualization.md +0 -0
  40. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/clusters.png +0 -0
  41. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/credit_control_surface.png +0 -0
  42. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/fraud_anomaly_terms.png +0 -0
  43. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/fraud_control_surface.png +0 -0
  44. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/gs_score_terms.png +0 -0
  45. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/it2_score.png +0 -0
  46. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/premium_terms.png +0 -0
  47. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/tut_advisor_surface.png +0 -0
  48. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/tut_advisor_volatility.png +0 -0
  49. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/tut_anfis.png +0 -0
  50. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/images/tut_clusters.png +0 -0
  51. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/index.md +0 -0
  52. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/installation.md +0 -0
  53. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/javascripts/mathjax.js +0 -0
  54. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/tutorials/anfis-learning.md +0 -0
  55. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/tutorials/clustering-segmentation.md +0 -0
  56. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/tutorials/risk-advisor.md +0 -0
  57. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/docs/tutorials/topsis-decision.md +0 -0
  58. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/examples/anfis.py +0 -0
  59. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/examples/clustering.py +0 -0
  60. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/examples/credit_risk.py +0 -0
  61. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/examples/credit_risk_it2.py +0 -0
  62. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/examples/fraud_alert.py +0 -0
  63. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/examples/ftransform.py +0 -0
  64. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/examples/mcdm.py +0 -0
  65. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/anfis.py +0 -0
  66. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/cluster.py +0 -0
  67. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/datasets.py +0 -0
  68. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/defuzz.py +0 -0
  69. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/ftransform.py +0 -0
  70. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/fuzzynum.py +0 -0
  71. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/inference/__init__.py +0 -0
  72. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/inference/mamdani.py +0 -0
  73. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/inference/tsk.py +0 -0
  74. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/inference/tsukamoto.py +0 -0
  75. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/integrations/__init__.py +0 -0
  76. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/integrations/agents.py +0 -0
  77. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/integrations/optuna.py +0 -0
  78. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/integrations/pandas.py +0 -0
  79. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/integrations/parallel.py +0 -0
  80. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/integrations/sklearn.py +0 -0
  81. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/integrations/torch.py +0 -0
  82. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/learn.py +0 -0
  83. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/mcdm.py +0 -0
  84. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/membership.py +0 -0
  85. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/norms.py +0 -0
  86. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/rules.py +0 -0
  87. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/serialize.py +0 -0
  88. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/sets.py +0 -0
  89. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/type2/__init__.py +0 -0
  90. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/type2/inference.py +0 -0
  91. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/type2/reduction.py +0 -0
  92. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/type2/sets.py +0 -0
  93. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/fuzzytool/viz.py +0 -0
  94. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/mkdocs.yml +0 -0
  95. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/notebooks/01_quickstart.ipynb +0 -0
  96. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/notebooks/02_type2.ipynb +0 -0
  97. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/notebooks/03_clustering.ipynb +0 -0
  98. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/notebooks/04_learning.ipynb +0 -0
  99. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/notebooks/README.md +0 -0
  100. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/paper.bib +0 -0
  101. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/paper.md +0 -0
  102. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/tests/test_cluster.py +0 -0
  103. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/tests/test_engineering.py +0 -0
  104. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/tests/test_inference.py +0 -0
  105. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/tests/test_learn.py +0 -0
  106. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/tests/test_learning.py +0 -0
  107. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/tests/test_mcdm.py +0 -0
  108. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/tests/test_membership.py +0 -0
  109. {fuzzytool-0.3.0 → fuzzytool-0.4.0}/tests/test_type2.py +0 -0
@@ -6,6 +6,22 @@ All notable changes to this project are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0] - 2026-06-28
10
+
11
+ ### Added
12
+
13
+ - **turboswarm integration** (`fuzzytool.integrations.turboswarm`, extra
14
+ `[turboswarm]`): `tune` fits a system's built-in membership-function
15
+ parameters to data with the sibling library's gradient-free, global Particle
16
+ Swarm Optimization — the metaheuristic counterpart of the SciPy least-squares
17
+ tuner. Returns turboswarm's `PsoResult`.
18
+
19
+ ### Changed
20
+
21
+ - Shared membership-function tuning helpers (`sanitize_mf_params`,
22
+ `tunable_terms`, `MF_PENALTY`) moved to `fuzzytool.integrations._util` and
23
+ reused by both the SciPy and turboswarm tuners.
24
+
9
25
  ## [0.3.0] - 2026-06-27
10
26
 
11
27
  ### Added
@@ -5,8 +5,8 @@ type: software
5
5
  authors:
6
6
  - family-names: Salmeron
7
7
  given-names: Jose L.
8
- version: 0.3.0
9
- date-released: 2026-06-27
8
+ version: 0.4.0
9
+ date-released: 2026-06-28
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.0
3
+ Version: 0.4.0
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
@@ -53,6 +53,8 @@ Provides-Extra: sklearn
53
53
  Requires-Dist: scikit-learn>=1.0; extra == 'sklearn'
54
54
  Provides-Extra: torch
55
55
  Requires-Dist: torch>=1.12; extra == 'torch'
56
+ Provides-Extra: turboswarm
57
+ Requires-Dist: turboswarm>=0.7; extra == 'turboswarm'
56
58
  Provides-Extra: viz
57
59
  Requires-Dist: matplotlib>=3.4; extra == 'viz'
58
60
  Description-Content-Type: text/markdown
@@ -114,7 +116,13 @@ s-norm, `~` the complement.
114
116
  | 4 | **Fuzzy clustering**: fuzzy c-means, Gustafson-Kessel, possibilistic | ✅ |
115
117
  | 5 | **ANFIS** (trainable TSK) + **F-transform** (direct/inverse) | ✅ |
116
118
  | 6 | Notebooks, JOSS `paper.md`, Zenodo DOI, PyPI release | ✅ |
117
- | 7 | **Ecosystem integrations**: pandas, scikit-learn, PyTorch, SciPy, Optuna, Joblib/Dask, LLM agents | ✅ |
119
+ | 7 | **Ecosystem integrations**: pandas, scikit-learn, PyTorch, SciPy, turboswarm, Optuna, Joblib/Dask, LLM agents | ✅ |
120
+
121
+ ### v0.4.0
122
+
123
+ - **turboswarm integration:** gradient-free, global tuning of a fuzzy system's
124
+ membership functions with Particle Swarm Optimization
125
+ (`integrations.turboswarm.tune`) — the metaheuristic sibling of the SciPy tuner.
118
126
 
119
127
  ### v0.3.0
120
128
 
@@ -55,7 +55,13 @@ s-norm, `~` the complement.
55
55
  | 4 | **Fuzzy clustering**: fuzzy c-means, Gustafson-Kessel, possibilistic | ✅ |
56
56
  | 5 | **ANFIS** (trainable TSK) + **F-transform** (direct/inverse) | ✅ |
57
57
  | 6 | Notebooks, JOSS `paper.md`, Zenodo DOI, PyPI release | ✅ |
58
- | 7 | **Ecosystem integrations**: pandas, scikit-learn, PyTorch, SciPy, Optuna, Joblib/Dask, LLM agents | ✅ |
58
+ | 7 | **Ecosystem integrations**: pandas, scikit-learn, PyTorch, SciPy, turboswarm, Optuna, Joblib/Dask, LLM agents | ✅ |
59
+
60
+ ### v0.4.0
61
+
62
+ - **turboswarm integration:** gradient-free, global tuning of a fuzzy system's
63
+ membership functions with Particle Swarm Optimization
64
+ (`integrations.turboswarm.tune`) — the metaheuristic sibling of the SciPy tuner.
59
65
 
60
66
  ### v0.3.0
61
67
 
@@ -63,6 +63,8 @@ Each pulls its dependency in only on import, behind its own extra.
63
63
  `nn.Module` trainable by autograd and composable into a network.
64
64
  - [x] **SciPy** (`[scipy]`): `tune` MF parameters to data via
65
65
  `scipy.optimize.least_squares` (returns the `OptimizeResult`).
66
+ - [x] **turboswarm** (`[turboswarm]`): `tune` MF parameters with gradient-free
67
+ Particle Swarm Optimization (the sibling PSO library; returns `PsoResult`).
66
68
  - [x] **Optuna** (`[optuna]`): `suggest_inference_spec`, `suggest_anfis` and a
67
69
  ready-made `tune_anfis` study.
68
70
  - [x] **Joblib / Dask** (`[parallel]`, `[dask]`): `parallel_predict`,
@@ -74,7 +76,6 @@ Each pulls its dependency in only on import, behind its own extra.
74
76
 
75
77
  - Fuzzy cognitive maps (+ grey FCM).
76
78
  - General (non-interval) type-2; more defuzzifiers.
77
- - FIS tuning via metaheuristics (e.g. PSO).
78
79
 
79
80
  ## ⏳ Phase 6 — Release (v0.1.0)
80
81
 
@@ -90,6 +90,10 @@ Generated from docstrings with `mkdocstrings`.
90
90
 
91
91
  ::: fuzzytool.integrations.scipy
92
92
 
93
+ ## Integrations — turboswarm (PSO)
94
+
95
+ ::: fuzzytool.integrations.turboswarm
96
+
93
97
  ## Integrations — Optuna
94
98
 
95
99
  ::: fuzzytool.integrations.optuna
@@ -10,6 +10,7 @@ third-party dependency **only when imported**, so the base install stays light.
10
10
  | scikit-learn | `fuzzytool.integrations.sklearn` | `pip install fuzzytool[sklearn]` |
11
11
  | PyTorch | `fuzzytool.integrations.torch` | `pip install fuzzytool[torch]` |
12
12
  | SciPy | `fuzzytool.integrations.scipy` | `pip install fuzzytool[scipy]` |
13
+ | turboswarm | `fuzzytool.integrations.turboswarm` | `pip install fuzzytool[turboswarm]` |
13
14
  | Optuna | `fuzzytool.integrations.optuna` | `pip install fuzzytool[optuna]` |
14
15
  | Joblib / Dask | `fuzzytool.integrations.parallel` | `pip install fuzzytool[parallel]` / `[dask]` |
15
16
  | LLM agents | `fuzzytool.integrations.agents` | `pip install fuzzytool[agents]` |
@@ -199,6 +200,38 @@ Only built-in shapes (`tri`, `trap`, `gauss`, `gbell`, `sigmoid`, `ramp_up`,
199
200
  `tune_outputs=False` to keep a Mamdani's output sets fixed, and forward any
200
201
  `least_squares` keyword (e.g. `max_nfev=200`).
201
202
 
203
+ ## turboswarm
204
+
205
+ [turboswarm](https://pypi.org/project/turboswarm/) is fuzzytool's sibling
206
+ Particle Swarm Optimization library. Its `tune` is the **gradient-free, global**
207
+ counterpart of the SciPy one: slower, but it escapes poor local optima and needs
208
+ no derivatives — a good fit for the rugged error surfaces fuzzy rule bases
209
+ produce. Same call shape, returns turboswarm's `PsoResult`:
210
+
211
+ ```python
212
+ import numpy as np
213
+ import fuzzytool as fz
214
+ from fuzzytool.integrations.turboswarm import tune
215
+
216
+ rng = np.random.default_rng(0)
217
+ X = rng.uniform(0, 10, size=(150, 1))
218
+ y = np.sin(X[:, 0]) + 0.1 * X[:, 0]
219
+
220
+ x = fz.Variable("x", (0, 10), terms=["lo", "mid", "hi"])
221
+ sys = fz.TSK()
222
+ sys.rule(x["lo"], 0.0)
223
+ sys.rule(x["mid"], 0.5)
224
+ sys.rule(x["hi"], 1.0)
225
+
226
+ result = tune(sys, X, y, columns=["x"], n_particles=20, max_iter=40, seed=0)
227
+ result.best_value # PsoResult; the system now fits the data better
228
+ ```
229
+
230
+ Each parameter is searched within `±margin × universe` around its current value
231
+ (`margin=0.5` by default); shape validity is enforced every evaluation. Any
232
+ `turboswarm.minimize` keyword (`topology`, `patience`, `max_time`, …) is
233
+ forwarded.
234
+
202
235
  ## Optuna
203
236
 
204
237
  Fuzzy systems have many discrete/continuous knobs. These helpers turn an Optuna
@@ -36,7 +36,7 @@ from .type2 import (
36
36
  it2_scale,
37
37
  )
38
38
 
39
- __version__ = "0.3.0"
39
+ __version__ = "0.4.0"
40
40
 
41
41
  __all__ = [
42
42
  "__version__",
@@ -0,0 +1,96 @@
1
+ """Shared helpers for the integration submodules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .. import membership as mf
6
+
7
+ # Returned in place of a non-finite residual so tuners avoid degenerate regions.
8
+ MF_PENALTY = 1e6
9
+
10
+
11
+ def input_variable_names(system) -> list[str]:
12
+ """Ordered, de-duplicated names of the input variables a system reads.
13
+
14
+ Walks every rule's antecedent tree of a :class:`~fuzzytool.inference.Mamdani`
15
+ or :class:`~fuzzytool.inference.TSK` system and collects the variables its
16
+ propositions reference, in first-seen order. This is the natural column
17
+ order for feeding a 2-D array or a DataFrame to the system.
18
+ """
19
+ names: list[str] = []
20
+ seen: set[str] = set()
21
+
22
+ def walk(node) -> None:
23
+ if hasattr(node, "variable"): # Proposition (atom)
24
+ name = node.variable.name
25
+ if name not in seen:
26
+ seen.add(name)
27
+ names.append(name)
28
+ if hasattr(node, "left"): # And / Or
29
+ walk(node.left)
30
+ walk(node.right)
31
+ if hasattr(node, "operand"): # Not
32
+ walk(node.operand)
33
+
34
+ for rule in getattr(system, "rules", []):
35
+ walk(rule.antecedent)
36
+ return names
37
+
38
+
39
+ def output_name(system, default: str = "output") -> str:
40
+ """Best-effort name for a single-output system (Mamdani tracks it; TSK does not)."""
41
+ outputs = getattr(system, "_outputs", None)
42
+ if outputs:
43
+ return next(iter(outputs))
44
+ return default
45
+
46
+
47
+ def sanitize_mf_params(kind: str, params) -> list[float]:
48
+ """Repair a membership-function parameter slice so it is always a valid shape.
49
+
50
+ Keeps breakpoints ordered and widths positive, so a tuner (SciPy least-squares
51
+ or turboswarm PSO) can explore freely without producing degenerate sets.
52
+ """
53
+ p = [float(v) for v in params]
54
+ if kind in ("tri", "trap", "ramp_up", "ramp_down"):
55
+ return sorted(p) # keep breakpoints ordered
56
+ if kind == "gauss":
57
+ return [p[0], abs(p[1]) or 1e-6] # sigma > 0
58
+ if kind == "gbell":
59
+ return [abs(p[0]) or 1e-6, abs(p[1]) or 1e-6, p[2]] # width, slope > 0
60
+ return p
61
+
62
+
63
+ def tunable_terms(system, tune_outputs: bool = True):
64
+ """Collect ``(variable, term_name, kind, params)`` for every tunable MF.
65
+
66
+ Walks the rules' propositions (and, if ``tune_outputs``, the Mamdani output
67
+ variables), keeping only terms backed by a built-in membership shape; custom
68
+ callables are skipped.
69
+ """
70
+ seen: dict[int, object] = {}
71
+
72
+ def walk(node):
73
+ if hasattr(node, "variable"):
74
+ seen.setdefault(id(node.variable), node.variable)
75
+ if hasattr(node, "left"):
76
+ walk(node.left)
77
+ walk(node.right)
78
+ if hasattr(node, "operand"):
79
+ walk(node.operand)
80
+
81
+ for rule in getattr(system, "rules", []):
82
+ walk(rule.antecedent)
83
+ variables = dict(seen)
84
+ if tune_outputs:
85
+ for out in getattr(system, "_outputs", {}).values():
86
+ variables.setdefault(id(out), out)
87
+
88
+ terms = []
89
+ for var in variables.values():
90
+ for term_name, membership in var.terms.items():
91
+ try:
92
+ spec = mf.to_dict(membership)
93
+ except TypeError:
94
+ continue # custom callable: skip
95
+ terms.append((var, term_name, spec["type"], list(spec["params"])))
96
+ return terms
@@ -20,27 +20,18 @@ from typing import TYPE_CHECKING
20
20
  import numpy as np
21
21
 
22
22
  from .. import membership as mf
23
- from ._util import input_variable_names
23
+ from ._util import (
24
+ MF_PENALTY,
25
+ input_variable_names,
26
+ sanitize_mf_params,
27
+ tunable_terms,
28
+ )
24
29
 
25
30
  if TYPE_CHECKING:
26
31
  from scipy.optimize import OptimizeResult
27
32
 
28
33
  from ..inference import TSK, Mamdani
29
34
 
30
- # Per-shape repair applied before rebuilding, so every candidate is a valid MF.
31
- _PENALTY = 1e6
32
-
33
-
34
- def _sanitize(kind: str, params: list[float]) -> list[float]:
35
- p = [float(v) for v in params]
36
- if kind in ("tri", "trap", "ramp_up", "ramp_down"):
37
- return sorted(p) # keep breakpoints ordered
38
- if kind == "gauss":
39
- return [p[0], abs(p[1]) or 1e-6] # sigma > 0
40
- if kind == "gbell":
41
- return [abs(p[0]) or 1e-6, abs(p[1]) or 1e-6, p[2]] # width, slope > 0
42
- return p
43
-
44
35
 
45
36
  def _require_scipy():
46
37
  try:
@@ -53,38 +44,6 @@ def _require_scipy():
53
44
  return least_squares
54
45
 
55
46
 
56
- def _tunable_terms(system, tune_outputs: bool):
57
- """Variables and term names whose membership functions can be tuned."""
58
- # Collect the Variable instances behind the rules' propositions.
59
- seen: dict[int, object] = {}
60
-
61
- def walk(node):
62
- if hasattr(node, "variable"):
63
- seen.setdefault(id(node.variable), node.variable)
64
- if hasattr(node, "left"):
65
- walk(node.left)
66
- walk(node.right)
67
- if hasattr(node, "operand"):
68
- walk(node.operand)
69
-
70
- for rule in system.rules:
71
- walk(rule.antecedent)
72
- variables = dict(seen)
73
- if tune_outputs:
74
- for out in getattr(system, "_outputs", {}).values():
75
- variables.setdefault(id(out), out)
76
-
77
- terms = []
78
- for var in variables.values():
79
- for term_name, membership in var.terms.items():
80
- try:
81
- spec = mf.to_dict(membership)
82
- except TypeError:
83
- continue # custom callable: skip
84
- terms.append((var, term_name, spec["type"], list(spec["params"])))
85
- return terms
86
-
87
-
88
47
  def tune(
89
48
  system: Mamdani | TSK,
90
49
  X: np.ndarray,
@@ -115,7 +74,7 @@ def tune(
115
74
  names = list(columns) if columns is not None else input_variable_names(system)
116
75
  inputs = {name: X[:, j] for j, name in enumerate(names)}
117
76
 
118
- terms = _tunable_terms(system, tune_outputs)
77
+ terms = tunable_terms(system, tune_outputs)
119
78
  if not terms:
120
79
  raise ValueError("the system has no tunable built-in membership functions")
121
80
 
@@ -130,13 +89,13 @@ def tune(
130
89
  def apply(p):
131
90
  for (var, term_name, kind, _params), (start, size) in zip(terms, slices):
132
91
  var[term_name] = mf.from_dict(
133
- {"type": kind, "params": _sanitize(kind, p[start:start + size])})
92
+ {"type": kind, "params": sanitize_mf_params(kind, p[start:start + size])})
134
93
 
135
94
  def residual(p):
136
95
  apply(p)
137
96
  pred = np.asarray(system.predict(**inputs), dtype=float)
138
97
  r = pred - y
139
- return np.where(np.isfinite(r), r, _PENALTY)
98
+ return np.where(np.isfinite(r), r, MF_PENALTY)
140
99
 
141
100
  result = least_squares(residual, p0, **least_squares_kwargs)
142
101
  apply(result.x) # leave the system at the best fit
@@ -0,0 +1,120 @@
1
+ """turboswarm integration: tune a fuzzy system with Particle Swarm Optimization.
2
+
3
+ Install with ``pip install fuzzytool[turboswarm]``. :func:`tune` fits the
4
+ parameters of a system's built-in membership functions to data with
5
+ `turboswarm <https://pypi.org/project/turboswarm/>`_'s PSO — a **gradient-free,
6
+ global** optimizer. It is the metaheuristic sibling of
7
+ :func:`fuzzytool.integrations.scipy.tune` (local least-squares): slower, but it
8
+ escapes poor local optima and needs no derivatives, which suits the rugged,
9
+ non-smooth error surfaces fuzzy rule bases often produce.
10
+
11
+ Each parameter is searched within a box of ``±margin × universe`` around its
12
+ current value; shape validity (ordered breakpoints, positive widths) is enforced
13
+ every evaluation, so the swarm explores freely. The system is mutated in place
14
+ with the best parameters found and the turboswarm ``PsoResult`` is returned.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING
20
+
21
+ import numpy as np
22
+
23
+ from .. import membership as mf
24
+ from ._util import (
25
+ MF_PENALTY,
26
+ input_variable_names,
27
+ sanitize_mf_params,
28
+ tunable_terms,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from turboswarm import PsoResult
33
+
34
+ from ..inference import TSK, Mamdani
35
+
36
+
37
+ def _require_turboswarm():
38
+ try:
39
+ from turboswarm import minimize
40
+ except ImportError as exc: # pragma: no cover
41
+ raise ImportError(
42
+ "the turboswarm integration needs turboswarm; install with "
43
+ "`pip install fuzzytool[turboswarm]`"
44
+ ) from exc
45
+ return minimize
46
+
47
+
48
+ def tune(
49
+ system: Mamdani | TSK,
50
+ X: np.ndarray,
51
+ y: np.ndarray,
52
+ columns: list[str] | None = None,
53
+ tune_outputs: bool = True,
54
+ margin: float = 0.5,
55
+ n_particles: int = 30,
56
+ max_iter: int = 100,
57
+ seed: int | None = None,
58
+ **minimize_kwargs,
59
+ ) -> PsoResult:
60
+ """Fit a system's membership-function parameters to data with PSO.
61
+
62
+ Extra keyword arguments are forwarded to :func:`turboswarm.minimize`
63
+ (e.g. ``topology``, ``patience``, ``max_time``).
64
+
65
+ Args:
66
+ system: a Mamdani or TSK system (mutated in place with the best params).
67
+ X: inputs, shape ``(n_samples, n_inputs)``.
68
+ y: targets, shape ``(n_samples,)``.
69
+ columns: input variable names matching ``X``'s columns, in order.
70
+ Defaults to the variables the rules reference.
71
+ tune_outputs: also tune the output variables' MFs (Mamdani only).
72
+ margin: search half-width per parameter as a fraction of its variable's
73
+ universe span (``0.5`` searches ±50% of the span around each value).
74
+ n_particles: swarm size.
75
+ max_iter: PSO iterations.
76
+ seed: PSO seed for reproducibility.
77
+
78
+ Returns:
79
+ The turboswarm ``PsoResult`` (``.best_position``, ``.best_value``, …).
80
+ """
81
+ minimize = _require_turboswarm()
82
+ X = np.asarray(X, dtype=float)
83
+ y = np.asarray(y, dtype=float)
84
+ names = list(columns) if columns is not None else input_variable_names(system)
85
+ inputs = {name: X[:, j] for j, name in enumerate(names)}
86
+
87
+ terms = tunable_terms(system, tune_outputs)
88
+ if not terms:
89
+ raise ValueError("the system has no tunable built-in membership functions")
90
+
91
+ # Flatten parameters; each gets a search box of ±(margin * span) around p0.
92
+ slices, p0, bounds = [], [], []
93
+ for var, _term, _kind, params in terms:
94
+ delta = margin * (var.high - var.low)
95
+ start = len(p0)
96
+ for value in params:
97
+ p0.append(value)
98
+ bounds.append((value - delta, value + delta))
99
+ slices.append((start, len(params)))
100
+
101
+ def apply(position):
102
+ for (var, term_name, kind, _params), (start, size) in zip(terms, slices):
103
+ var[term_name] = mf.from_dict(
104
+ {"type": kind,
105
+ "params": sanitize_mf_params(kind, position[start:start + size])})
106
+
107
+ def objective(position):
108
+ apply(position)
109
+ pred = np.asarray(system.predict(**inputs), dtype=float)
110
+ r = pred - y
111
+ r = np.where(np.isfinite(r), r, MF_PENALTY)
112
+ return float(np.mean(r * r)) # mean squared error
113
+
114
+ result = minimize(objective, bounds=bounds, n_particles=n_particles,
115
+ max_iter=max_iter, seed=seed, **minimize_kwargs)
116
+ apply(result.best_position) # leave the system at the best fit
117
+ return result
118
+
119
+
120
+ __all__ = ["tune"]
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
 
9
9
  [project]
10
10
  name = "fuzzytool"
11
- version = "0.3.0"
11
+ version = "0.4.0"
12
12
  description = "A clean, extensible fuzzy-logic toolkit in pure Python + NumPy"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.9"
@@ -35,6 +35,7 @@ optuna = ["optuna>=3.0"]
35
35
  parallel = ["joblib>=1.0"]
36
36
  dask = ["dask>=2022.0"]
37
37
  agents = ["langchain-core>=0.1"]
38
+ turboswarm = ["turboswarm>=0.7"]
38
39
  dev = ["pytest>=7", "pytest-cov", "ruff", "pandas>=1.3", "scikit-learn>=1.0",
39
40
  "scipy>=1.7", "optuna>=3.0", "joblib>=1.0", "langchain-core>=0.1"]
40
41
  docs = ["mkdocs-material", "mkdocstrings[python]"]
@@ -177,6 +177,26 @@ def test_scipy_tune_reduces_error():
177
177
  assert hasattr(res, "x") and hasattr(res, "cost")
178
178
 
179
179
 
180
+ def test_turboswarm_tune_reduces_error():
181
+ pytest.importorskip("turboswarm")
182
+ from fuzzytool.integrations.turboswarm import tune
183
+
184
+ rng = np.random.default_rng(0)
185
+ X = rng.uniform(0, 10, size=(150, 1))
186
+ y = (np.sin(X[:, 0]) + 0.1 * X[:, 0]).ravel()
187
+ x = fz.Variable("x", (0, 10), terms=["lo", "mid", "hi"])
188
+ sys = fz.TSK()
189
+ sys.rule(x["lo"], 0.0)
190
+ sys.rule(x["mid"], 0.5)
191
+ sys.rule(x["hi"], 1.0)
192
+
193
+ before = np.sqrt(np.nanmean((sys.predict(x=X[:, 0]) - y) ** 2))
194
+ res = tune(sys, X, y, columns=["x"], n_particles=20, max_iter=40, seed=0)
195
+ after = np.sqrt(np.nanmean((sys.predict(x=X[:, 0]) - y) ** 2))
196
+ assert after < before
197
+ assert hasattr(res, "best_position") and hasattr(res, "best_value")
198
+
199
+
180
200
  def test_scipy_tune_keeps_membership_valid():
181
201
  pytest.importorskip("scipy")
182
202
  from fuzzytool import membership as mf
@@ -1,39 +0,0 @@
1
- """Shared helpers for the integration submodules."""
2
-
3
- from __future__ import annotations
4
-
5
-
6
- def input_variable_names(system) -> list[str]:
7
- """Ordered, de-duplicated names of the input variables a system reads.
8
-
9
- Walks every rule's antecedent tree of a :class:`~fuzzytool.inference.Mamdani`
10
- or :class:`~fuzzytool.inference.TSK` system and collects the variables its
11
- propositions reference, in first-seen order. This is the natural column
12
- order for feeding a 2-D array or a DataFrame to the system.
13
- """
14
- names: list[str] = []
15
- seen: set[str] = set()
16
-
17
- def walk(node) -> None:
18
- if hasattr(node, "variable"): # Proposition (atom)
19
- name = node.variable.name
20
- if name not in seen:
21
- seen.add(name)
22
- names.append(name)
23
- if hasattr(node, "left"): # And / Or
24
- walk(node.left)
25
- walk(node.right)
26
- if hasattr(node, "operand"): # Not
27
- walk(node.operand)
28
-
29
- for rule in getattr(system, "rules", []):
30
- walk(rule.antecedent)
31
- return names
32
-
33
-
34
- def output_name(system, default: str = "output") -> str:
35
- """Best-effort name for a single-output system (Mamdani tracks it; TSK does not)."""
36
- outputs = getattr(system, "_outputs", None)
37
- if outputs:
38
- return next(iter(outputs))
39
- return default
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