Coverage for anfis_toolbox / builders.py: 100%
451 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-05 18:47 -0300
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-05 18:47 -0300
1"""Builder classes for easy ANFIS model construction."""
3from __future__ import annotations
5from collections.abc import Callable, Sequence
6from typing import TypeAlias, cast
8import numpy as np
9from numpy.random import Generator
10from numpy.typing import ArrayLike, NDArray
12from .clustering import FuzzyCMeans
13from .membership import (
14 BellMF,
15 DiffSigmoidalMF,
16 Gaussian2MF,
17 GaussianMF,
18 LinSShapedMF,
19 LinZShapedMF,
20 MembershipFunction,
21 PiMF,
22 ProdSigmoidalMF,
23 SigmoidalMF,
24 SShapedMF,
25 TrapezoidalMF,
26 TriangularMF,
27 ZShapedMF,
28)
29from .model import TSKANFIS
31MembershipFactory = Callable[[float, float, int, float], list[MembershipFunction]]
32RandomStateLike: TypeAlias = int | Generator | None
33FloatArray1D: TypeAlias = NDArray[np.float64]
34FloatArray2D: TypeAlias = NDArray[np.float64]
37class ANFISBuilder:
38 """Builder class for creating ANFIS models with intuitive API."""
40 def __init__(self) -> None:
41 """Initialize the ANFIS builder."""
42 self.input_mfs: dict[str, list[MembershipFunction]] = {}
43 self.input_ranges: dict[str, tuple[float, float]] = {}
44 self._rules: list[tuple[int, ...]] | None = None
45 # Centralized dispatch for MF creators (supports aliases)
46 dispatch: dict[str, MembershipFactory] = {
47 # Canonical
48 "gaussian": cast(MembershipFactory, self._create_gaussian_mfs),
49 "gaussian2": cast(MembershipFactory, self._create_gaussian2_mfs),
50 "triangular": cast(MembershipFactory, self._create_triangular_mfs),
51 "trapezoidal": cast(MembershipFactory, self._create_trapezoidal_mfs),
52 "bell": cast(MembershipFactory, self._create_bell_mfs),
53 "sigmoidal": cast(MembershipFactory, self._create_sigmoidal_mfs),
54 "sshape": cast(MembershipFactory, self._create_sshape_mfs),
55 "zshape": cast(MembershipFactory, self._create_zshape_mfs),
56 "pi": cast(MembershipFactory, self._create_pi_mfs),
57 "linsshape": cast(MembershipFactory, self._create_linsshape_mfs),
58 "linzshape": cast(MembershipFactory, self._create_linzshape_mfs),
59 "diffsigmoidal": cast(MembershipFactory, self._create_diff_sigmoidal_mfs),
60 "prodsigmoidal": cast(MembershipFactory, self._create_prod_sigmoidal_mfs),
61 # Aliases
62 "gbell": cast(MembershipFactory, self._create_bell_mfs),
63 "sigmoid": cast(MembershipFactory, self._create_sigmoidal_mfs),
64 "s": cast(MembershipFactory, self._create_sshape_mfs),
65 "z": cast(MembershipFactory, self._create_zshape_mfs),
66 "pimf": cast(MembershipFactory, self._create_pi_mfs),
67 "ls": cast(MembershipFactory, self._create_linsshape_mfs),
68 "lz": cast(MembershipFactory, self._create_linzshape_mfs),
69 "diffsigmoid": cast(MembershipFactory, self._create_diff_sigmoidal_mfs),
70 "prodsigmoid": cast(MembershipFactory, self._create_prod_sigmoidal_mfs),
71 }
72 self._dispatch: dict[str, MembershipFactory] = dispatch
74 def add_input(
75 self,
76 name: str,
77 range_min: float,
78 range_max: float,
79 n_mfs: int = 3,
80 mf_type: str = "gaussian",
81 overlap: float = 0.5,
82 ) -> ANFISBuilder:
83 """Add an input variable with automatic membership function generation.
85 Parameters:
86 name: Name of the input variable
87 range_min: Minimum value of the input range
88 range_max: Maximum value of the input range
89 n_mfs: Number of membership functions (default: 3)
90 mf_type: Type of membership functions. Supported:
91 'gaussian', 'gaussian2', 'triangular', 'trapezoidal',
92 'bell', 'sigmoidal', 'sshape', 'zshape', 'pi'
93 overlap: Overlap factor between adjacent MFs (0.0 to 1.0)
95 Returns:
96 Self for method chaining
97 """
98 self.input_ranges[name] = (range_min, range_max)
100 mf_key = mf_type.strip().lower()
101 factory = self._dispatch.get(mf_key)
102 if factory is None:
103 supported = ", ".join(sorted(set(self._dispatch.keys())))
104 raise ValueError(f"Unknown membership function type: {mf_type}. Supported: {supported}")
105 self.input_mfs[name] = factory(range_min, range_max, n_mfs, overlap)
107 return self
109 def add_input_from_data(
110 self,
111 name: str,
112 data: ArrayLike,
113 n_mfs: int = 3,
114 mf_type: str = "gaussian",
115 overlap: float = 0.5,
116 margin: float = 0.10,
117 init: str | None = "grid",
118 random_state: RandomStateLike = None,
119 ) -> ANFISBuilder:
120 """Add an input inferring range_min/range_max from data with a margin.
122 Parameters:
123 name: Input name
124 data: 1D array-like samples for this input
125 n_mfs: Number of membership functions
126 mf_type: Membership function type (see add_input)
127 overlap: Overlap factor between adjacent MFs
128 margin: Fraction of (max-min) to pad on each side
129 init: Initialization strategy: "grid" (default), "fcm", "random", or ``None``. When ``"fcm"``,
130 clusters from the data determine MF centers and widths (supports
131 'gaussian' and 'bell').
132 random_state: Optional seed for deterministic FCM initialization.
133 """
134 arr = cast(FloatArray1D, np.asarray(data, dtype=np.float64).reshape(-1))
136 if init is None:
137 strategy = "grid"
138 else:
139 strategy = str(init).strip().lower()
141 if strategy == "fcm":
142 self.input_mfs[name] = self._create_mfs_from_fcm(arr, n_mfs, mf_type, random_state)
143 self.input_ranges[name] = (float(np.min(arr)), float(np.max(arr)))
144 return self
145 if strategy == "random":
146 return self._add_input_random(
147 name=name,
148 data=arr,
149 n_mfs=n_mfs,
150 mf_type=mf_type,
151 overlap=overlap,
152 margin=margin,
153 random_state=random_state,
154 )
155 if strategy != "grid":
156 supported = "grid, fcm, random"
157 raise ValueError(f"Unknown init strategy '{init}'. Supported: {supported}")
159 rmin = float(np.min(arr))
160 rmax = float(np.max(arr))
161 pad = (rmax - rmin) * float(margin)
162 return self.add_input(name, rmin - pad, rmax + pad, n_mfs, mf_type, overlap)
164 # FCM-based MF creation for 1D inputs
165 def _create_mfs_from_fcm(
166 self,
167 data_1d: FloatArray1D,
168 n_mfs: int,
169 mf_type: str,
170 random_state: RandomStateLike,
171 ) -> list[MembershipFunction]:
172 """Create membership functions from 1D data via FCM."""
173 x = cast(FloatArray2D, np.asarray(data_1d, dtype=np.float64).reshape(-1, 1))
174 if x.shape[0] < n_mfs:
175 raise ValueError("n_samples must be >= n_mfs for FCM initialization")
177 if isinstance(random_state, Generator):
178 fcm_seed: int | None = int(random_state.integers(0, 2**32 - 1))
179 else:
180 fcm_seed = random_state
182 fcm = FuzzyCMeans(n_clusters=n_mfs, m=2.0, random_state=fcm_seed)
183 fcm.fit(x)
184 centers_arr = fcm.cluster_centers_
185 membership = fcm.membership_
186 if centers_arr is None or membership is None:
187 raise RuntimeError("FCM did not produce centers or membership values; ensure fit succeeded.")
189 centers = centers_arr.reshape(-1)
190 U = membership
191 m = fcm.m
193 diffs = x[:, 0][:, None] - centers[None, :]
194 num = np.sum((U**m) * (diffs * diffs), axis=0)
195 den = np.maximum(np.sum(U**m, axis=0), 1e-12)
196 sigmas = np.sqrt(num / den)
198 spacing = np.diff(np.sort(centers))
199 default_sigma = float(np.median(spacing)) if spacing.size else max(float(np.std(x)), 1e-3)
200 sigmas = np.where(sigmas > 1e-12, sigmas, max(default_sigma, 1e-3))
202 order = np.argsort(centers)
203 centers = centers[order]
204 sigmas = sigmas[order]
206 key = mf_type.strip().lower()
207 rmin = float(np.min(x))
208 rmax = float(np.max(x))
209 min_w = max(float(np.median(np.diff(np.sort(centers)))) if centers.size > 1 else float(np.std(x)), 1e-3)
210 widths = np.maximum(2.0 * sigmas, min_w)
212 return self._build_mfs_from_layout(key, centers, sigmas, widths, rmin, rmax)
214 def _add_input_random(
215 self,
216 name: str,
217 data: ArrayLike,
218 n_mfs: int,
219 mf_type: str,
220 overlap: float,
221 margin: float,
222 random_state: RandomStateLike,
223 ) -> ANFISBuilder:
224 x = cast(FloatArray1D, np.asarray(data, dtype=np.float64).reshape(-1))
225 if x.size == 0:
226 raise ValueError("Cannot initialize membership functions from empty data array")
228 rmin = float(np.min(x))
229 rmax = float(np.max(x))
230 pad = (rmax - rmin) * float(margin)
231 low = rmin - pad
232 high = rmax + pad
234 if isinstance(random_state, Generator):
235 rng = random_state
236 else:
237 rng = np.random.default_rng(random_state)
239 centers = np.sort(rng.uniform(low, high, size=max(int(n_mfs), 1)))
240 if centers.size == 1:
241 widths = np.array([max(high - low, 1e-3)])
242 else:
243 diffs = np.diff(centers)
244 left = np.concatenate(([diffs[0]], diffs))
245 right = np.concatenate((diffs, [diffs[-1]]))
246 widths = (left + right) / 2.0
248 overlap = float(overlap)
249 base_span = (high - low) / max(centers.size, 1)
250 floor = max(base_span * max(overlap, 0.1), 1e-3)
251 widths = np.maximum(widths, floor)
252 sigmas = np.maximum(widths / 2.0, 1e-3)
254 key = mf_type.strip().lower()
255 mfs = self._build_mfs_from_layout(key, centers, sigmas, widths, low, high)
257 self.input_ranges[name] = (low, high)
258 self.input_mfs[name] = mfs
259 return self
261 def set_rules(self, rules: Sequence[Sequence[int]] | None) -> ANFISBuilder:
262 """Define an explicit set of fuzzy rules to use when building the model.
264 Parameters:
265 rules: Iterable of rules where each rule lists the membership index per input.
266 ``None`` removes any previously configured custom rules and restores the
267 default Cartesian-product behaviour.
269 Returns:
270 Self for method chaining.
271 """
272 if rules is None:
273 self._rules = None
274 return self
276 normalized: list[tuple[int, ...]] = []
277 for rule in rules:
278 normalized.append(tuple(int(idx) for idx in rule))
279 if not normalized:
280 raise ValueError("Rules sequence cannot be empty; pass None to restore defaults.")
281 self._rules = normalized
282 return self
284 def _build_mfs_from_layout(
285 self,
286 key: str,
287 centers: np.ndarray,
288 sigmas: np.ndarray,
289 widths: np.ndarray,
290 rmin: float,
291 rmax: float,
292 ) -> list[MembershipFunction]:
293 if key == "gaussian":
294 return [GaussianMF(mean=float(c), sigma=float(s)) for c, s in zip(centers, sigmas, strict=False)]
295 if key == "gaussian2":
296 gaussian2_mfs: list[MembershipFunction] = []
297 plateau_frac = 0.3
298 for c, s, w in zip(centers, sigmas, widths, strict=False):
299 half_plateau = (w * plateau_frac) / 2.0
300 c1 = float(max(c - half_plateau, rmin))
301 c2 = float(min(c + half_plateau, rmax))
302 if not (c1 < c2):
303 eps = 1e-6
304 c1, c2 = c - eps, c + eps
305 gaussian2_mfs.append(Gaussian2MF(sigma1=float(s), c1=c1, sigma2=float(s), c2=c2))
306 return gaussian2_mfs
307 if key in {"bell", "gbell"}:
308 return [BellMF(a=float(s), b=2.0, c=float(c)) for c, s in zip(centers, sigmas, strict=False)]
309 if key == "triangular":
310 triangular_mfs: list[MembershipFunction] = []
311 for c, w in zip(centers, widths, strict=False):
312 a = float(max(c - w / 2.0, rmin))
313 cc = float(min(c + w / 2.0, rmax))
314 b = float(c)
315 if not (a < b < cc):
316 eps = 1e-6
317 a, b, cc = c - 2 * eps, c, c + 2 * eps
318 triangular_mfs.append(TriangularMF(a, b, cc))
319 return triangular_mfs
320 if key == "trapezoidal":
321 trapezoidal_mfs: list[MembershipFunction] = []
322 plateau_frac = 0.3
323 for c, w in zip(centers, widths, strict=False):
324 a = float(c - w / 2.0)
325 d = float(c + w / 2.0)
326 b = float(a + (w * (1 - plateau_frac)) / 2.0)
327 cr = float(b + w * plateau_frac)
328 a = max(a, rmin)
329 d = min(d, rmax)
330 b = max(b, a + 1e-6)
331 cr = min(cr, d - 1e-6)
332 if not (a < b <= cr < d):
333 eps = 1e-6
334 a, b, cr, d = c - 2 * eps, c - eps, c + eps, c + 2 * eps
335 trapezoidal_mfs.append(TrapezoidalMF(a, b, cr, d))
336 return trapezoidal_mfs
337 if key in {"sigmoidal", "sigmoid"}:
338 sigmoidal_mfs: list[MembershipFunction] = []
339 for c, w in zip(centers, widths, strict=False):
340 a = 4.4 / max(float(w), 1e-8)
341 sigmoidal_mfs.append(SigmoidalMF(a=float(a), c=float(c)))
342 return sigmoidal_mfs
343 if key in {"linsshape", "ls"}:
344 lin_s_mfs: list[MembershipFunction] = []
345 for c, w in zip(centers, widths, strict=False):
346 a = float(max(c - w / 2.0, rmin))
347 b = float(min(c + w / 2.0, rmax))
348 if a >= b:
349 eps = 1e-6
350 a, b = c - eps, c + eps
351 lin_s_mfs.append(LinSShapedMF(a, b))
352 return lin_s_mfs
353 if key in {"linzshape", "lz"}:
354 lin_z_mfs: list[MembershipFunction] = []
355 for c, w in zip(centers, widths, strict=False):
356 a = float(max(c - w / 2.0, rmin))
357 b = float(min(c + w / 2.0, rmax))
358 if a >= b:
359 eps = 1e-6
360 a, b = c - eps, c + eps
361 lin_z_mfs.append(LinZShapedMF(a, b))
362 return lin_z_mfs
363 if key in {"diffsigmoidal", "diffsigmoid"}:
364 diff_sig_mfs: list[MembershipFunction] = []
365 for c, w in zip(centers, widths, strict=False):
366 c1 = float(max(c - w / 2.0, rmin))
367 c2 = float(min(c + w / 2.0, rmax))
368 if c1 >= c2:
369 eps = 1e-6
370 c1, c2 = c - eps, c + eps
371 a = 4.4 / max(float(w), 1e-8)
372 diff_sig_mfs.append(DiffSigmoidalMF(a1=float(a), c1=c1, a2=float(a), c2=c2))
373 return diff_sig_mfs
374 if key in {"prodsigmoidal", "prodsigmoid"}:
375 prod_sig_mfs: list[MembershipFunction] = []
376 for c, w in zip(centers, widths, strict=False):
377 c1 = float(max(c - w / 2.0, rmin))
378 c2 = float(min(c + w / 2.0, rmax))
379 if c1 >= c2:
380 eps = 1e-6
381 c1, c2 = c - eps, c + eps
382 a = 4.4 / max(float(w), 1e-8)
383 prod_sig_mfs.append(ProdSigmoidalMF(a1=float(a), c1=c1, a2=float(-a), c2=c2))
384 return prod_sig_mfs
385 if key in {"sshape", "s"}:
386 s_shape_mfs: list[MembershipFunction] = []
387 for c, w in zip(centers, widths, strict=False):
388 a = float(max(c - w / 2.0, rmin))
389 b = float(min(c + w / 2.0, rmax))
390 if a >= b:
391 eps = 1e-6
392 a, b = c - eps, c + eps
393 s_shape_mfs.append(SShapedMF(a, b))
394 return s_shape_mfs
395 if key in {"zshape", "z"}:
396 z_shape_mfs: list[MembershipFunction] = []
397 for c, w in zip(centers, widths, strict=False):
398 a = float(max(c - w / 2.0, rmin))
399 b = float(min(c + w / 2.0, rmax))
400 if a >= b:
401 eps = 1e-6
402 a, b = c - eps, c + eps
403 z_shape_mfs.append(ZShapedMF(a, b))
404 return z_shape_mfs
405 if key in {"pi", "pimf"}:
406 pi_mfs: list[MembershipFunction] = []
407 plateau_frac = 0.3
408 for c, w in zip(centers, widths, strict=False):
409 a = float(c - w / 2.0)
410 d = float(c + w / 2.0)
411 b = float(a + (w * (1 - plateau_frac)) / 2.0)
412 cr = float(b + w * plateau_frac)
413 a = max(a, rmin)
414 d = min(d, rmax)
415 b = max(b, a + 1e-6)
416 cr = min(cr, d - 1e-6)
417 if not (a < b <= cr < d):
418 eps = 1e-6
419 a, b, cr, d = c - 2 * eps, c - eps, c + eps, c + 2 * eps
420 pi_mfs.append(PiMF(a, b, cr, d))
421 return pi_mfs
422 supported = (
423 "gaussian, gaussian2, bell/gbell, triangular, trapezoidal, sigmoidal/sigmoid, sshape/s, zshape/z, pi/pimf"
424 )
425 raise ValueError(f"Initialization supports: {supported}")
427 def _create_gaussian_mfs(self, range_min: float, range_max: float, n_mfs: int, overlap: float) -> list[GaussianMF]:
428 """Create evenly spaced Gaussian membership functions."""
429 centers = np.linspace(range_min, range_max, n_mfs)
431 # Handle single MF case
432 if n_mfs == 1:
433 sigma = (range_max - range_min) * 0.25 # Use quarter of range as default sigma
434 else:
435 sigma = (range_max - range_min) / (n_mfs - 1) * overlap
437 return [GaussianMF(mean=center, sigma=sigma) for center in centers]
439 def _create_gaussian2_mfs(
440 self, range_min: float, range_max: float, n_mfs: int, overlap: float
441 ) -> list[Gaussian2MF]:
442 """Create evenly spaced two-sided Gaussian (Gaussian2) membership functions.
444 Uses Gaussian tails with a small central plateau per MF. The plateau width
445 is a fraction of the MF span controlled by overlap.
446 """
447 centers = np.linspace(range_min, range_max, n_mfs)
449 # Determine spacing and widths
450 if n_mfs == 1:
451 spacing = range_max - range_min
452 sigma = spacing * 0.25
453 width = spacing * 0.5
454 else:
455 spacing = (range_max - range_min) / (n_mfs - 1)
456 sigma = spacing * overlap
457 width = spacing * (1 + overlap)
459 plateau_frac = 0.3
460 half_plateau = (width * plateau_frac) / 2.0
462 mfs: list[Gaussian2MF] = []
463 for c in centers:
464 c1 = float(max(c - half_plateau, range_min))
465 c2 = float(min(c + half_plateau, range_max))
466 if not (c1 < c2):
467 eps = 1e-6
468 c1, c2 = c - eps, c + eps
469 mfs.append(Gaussian2MF(sigma1=float(sigma), c1=c1, sigma2=float(sigma), c2=c2))
470 return mfs
472 def _create_triangular_mfs(
473 self, range_min: float, range_max: float, n_mfs: int, overlap: float
474 ) -> list[TriangularMF]:
475 """Create evenly spaced triangular membership functions."""
476 centers = np.linspace(range_min, range_max, n_mfs)
477 width = (range_max - range_min) / (n_mfs - 1) * (1 + overlap)
479 mfs: list[TriangularMF] = []
480 for i, center in enumerate(centers):
481 a = center - width / 2
482 b = center
483 c = center + width / 2
485 # Adjust boundaries for edge cases
486 if i == 0:
487 a = range_min
488 if i == n_mfs - 1:
489 c = range_max
491 mfs.append(TriangularMF(a, b, c))
493 return mfs
495 def _create_trapezoidal_mfs(
496 self, range_min: float, range_max: float, n_mfs: int, overlap: float
497 ) -> list[TrapezoidalMF]:
498 """Create evenly spaced trapezoidal membership functions."""
499 centers = np.linspace(range_min, range_max, n_mfs)
500 width = (range_max - range_min) / (n_mfs - 1) * (1 + overlap)
501 plateau = width * 0.3 # 30% plateau
503 mfs: list[TrapezoidalMF] = []
504 for i, center in enumerate(centers):
505 a = center - width / 2
506 b = center - plateau / 2
507 c = center + plateau / 2
508 d = center + width / 2
510 # Adjust boundaries for edge cases
511 if i == 0:
512 a = range_min
513 b = max(b, range_min)
514 if i == n_mfs - 1:
515 c = min(c, range_max)
516 d = range_max
518 mfs.append(TrapezoidalMF(a, b, c, d))
520 return mfs
522 # New MF families
523 def _create_bell_mfs(self, range_min: float, range_max: float, n_mfs: int, overlap: float) -> list[BellMF]:
524 """Create evenly spaced Bell membership functions (generalized bell)."""
525 centers = np.linspace(range_min, range_max, n_mfs)
526 if n_mfs == 1:
527 a = (range_max - range_min) * 0.25
528 else:
529 spacing = (range_max - range_min) / (n_mfs - 1)
530 a = spacing * (1 + overlap) / 2.0 # half-width
531 b = 2.0 # default slope
532 return [BellMF(a=a, b=b, c=float(c)) for c in centers]
534 def _create_sigmoidal_mfs(
535 self, range_min: float, range_max: float, n_mfs: int, overlap: float
536 ) -> list[SigmoidalMF]:
537 """Create a bank of sigmoids across the range with centers and slopes."""
538 centers = np.linspace(range_min, range_max, n_mfs)
539 if n_mfs == 1:
540 width = (range_max - range_min) * 0.5
541 else:
542 spacing = (range_max - range_min) / (n_mfs - 1)
543 width = spacing * (1 + overlap)
544 # Choose slope a s.t. 0.1->0.9 transition ~ width: width ≈ 4.4 / a
545 a = 4.4 / max(width, 1e-8)
546 return [SigmoidalMF(a=float(a), c=float(c)) for c in centers]
548 def _create_linsshape_mfs(
549 self, range_min: float, range_max: float, n_mfs: int, overlap: float
550 ) -> list[LinSShapedMF]:
551 """Create linear S-shaped MFs across the range."""
552 centers = np.linspace(range_min, range_max, n_mfs)
553 if n_mfs == 1:
554 width = (range_max - range_min) * 0.5
555 else:
556 spacing = (range_max - range_min) / (n_mfs - 1)
557 width = spacing * (1 + overlap)
558 half = width / 2.0
559 mfs: list[LinSShapedMF] = []
560 for c in centers:
561 a = float(max(c - half, range_min))
562 b = float(min(c + half, range_max))
563 if a >= b:
564 eps = 1e-6
565 a, b = c - eps, c + eps
566 mfs.append(LinSShapedMF(a, b))
567 return mfs
569 def _create_linzshape_mfs(
570 self, range_min: float, range_max: float, n_mfs: int, overlap: float
571 ) -> list[LinZShapedMF]:
572 """Create linear Z-shaped MFs across the range."""
573 centers = np.linspace(range_min, range_max, n_mfs)
574 if n_mfs == 1:
575 width = (range_max - range_min) * 0.5
576 else:
577 spacing = (range_max - range_min) / (n_mfs - 1)
578 width = spacing * (1 + overlap)
579 half = width / 2.0
580 mfs: list[LinZShapedMF] = []
581 for c in centers:
582 a = float(max(c - half, range_min))
583 b = float(min(c + half, range_max))
584 if a >= b:
585 eps = 1e-6
586 a, b = c - eps, c + eps
587 mfs.append(LinZShapedMF(a, b))
588 return mfs
590 def _create_diff_sigmoidal_mfs(
591 self, range_min: float, range_max: float, n_mfs: int, overlap: float
592 ) -> list[DiffSigmoidalMF]:
593 """Create bands using difference of two sigmoids around evenly spaced centers."""
594 centers = np.linspace(range_min, range_max, n_mfs)
595 if n_mfs == 1:
596 width = (range_max - range_min) * 0.5
597 else:
598 spacing = (range_max - range_min) / (n_mfs - 1)
599 width = spacing * (1 + overlap)
600 mfs: list[DiffSigmoidalMF] = []
601 for c in centers:
602 c1 = float(max(c - width / 2.0, range_min))
603 c2 = float(min(c + width / 2.0, range_max))
604 if c1 >= c2:
605 eps = 1e-6
606 c1, c2 = c - eps, c + eps
607 a = 4.4 / max(float(width), 1e-8)
608 mfs.append(DiffSigmoidalMF(a1=float(a), c1=c1, a2=float(a), c2=c2))
609 return mfs
611 def _create_prod_sigmoidal_mfs(
612 self, range_min: float, range_max: float, n_mfs: int, overlap: float
613 ) -> list[ProdSigmoidalMF]:
614 """Create product-of-sigmoids MFs; use increasing and decreasing pair to form a bump."""
615 centers = np.linspace(range_min, range_max, n_mfs)
616 if n_mfs == 1:
617 width = (range_max - range_min) * 0.5
618 else:
619 spacing = (range_max - range_min) / (n_mfs - 1)
620 width = spacing * (1 + overlap)
621 mfs: list[ProdSigmoidalMF] = []
622 for c in centers:
623 c1 = float(max(c - width / 2.0, range_min))
624 c2 = float(min(c + width / 2.0, range_max))
625 if c1 >= c2:
626 eps = 1e-6
627 c1, c2 = c - eps, c + eps
628 a = 4.4 / max(float(width), 1e-8)
629 mfs.append(ProdSigmoidalMF(a1=float(a), c1=c1, a2=float(-a), c2=c2))
630 return mfs
632 def _create_sshape_mfs(self, range_min: float, range_max: float, n_mfs: int, overlap: float) -> list[SShapedMF]:
633 """Create S-shaped MFs with spans around evenly spaced centers."""
634 centers = np.linspace(range_min, range_max, n_mfs)
635 if n_mfs == 1:
636 width = (range_max - range_min) * 0.5
637 else:
638 spacing = (range_max - range_min) / (n_mfs - 1)
639 width = spacing * (1 + overlap)
640 half = width / 2.0
641 mfs: list[SShapedMF] = []
642 for c in centers:
643 a = float(c - half)
644 b = float(c + half)
645 # Clamp to the provided range
646 a = max(a, range_min)
647 b = min(b, range_max)
648 if a >= b:
649 # Fallback to a tiny span
650 eps = 1e-6
651 a, b = float(c - eps), float(c + eps)
652 mfs.append(SShapedMF(a, b))
653 return mfs
655 def _create_zshape_mfs(self, range_min: float, range_max: float, n_mfs: int, overlap: float) -> list[ZShapedMF]:
656 """Create Z-shaped MFs with spans around evenly spaced centers."""
657 centers = np.linspace(range_min, range_max, n_mfs)
658 if n_mfs == 1:
659 width = (range_max - range_min) * 0.5
660 else:
661 spacing = (range_max - range_min) / (n_mfs - 1)
662 width = spacing * (1 + overlap)
663 half = width / 2.0
664 mfs: list[ZShapedMF] = []
665 for c in centers:
666 a = float(c - half)
667 b = float(c + half)
668 a = max(a, range_min)
669 b = min(b, range_max)
670 if a >= b:
671 eps = 1e-6
672 a, b = float(c - eps), float(c + eps)
673 mfs.append(ZShapedMF(a, b))
674 return mfs
676 def _create_pi_mfs(self, range_min: float, range_max: float, n_mfs: int, overlap: float) -> list[PiMF]:
677 """Create Pi-shaped MFs with smooth S/Z edges and a flat top."""
678 centers = np.linspace(range_min, range_max, n_mfs)
679 if n_mfs == 1:
680 width = (range_max - range_min) * 0.5
681 else:
682 spacing = (range_max - range_min) / (n_mfs - 1)
683 width = spacing * (1 + overlap)
684 plateau = width * 0.3
685 mfs: list[PiMF] = []
686 for c in centers:
687 a = float(c - width / 2.0)
688 d = float(c + width / 2.0)
689 b = float(a + (width - plateau) / 2.0)
690 c_right = float(b + plateau)
691 # Clamp within the provided range
692 a = max(a, range_min)
693 d = min(d, range_max)
694 # Ensure ordering a < b ≤ c < d
695 b = max(b, a + 1e-6)
696 c_right = min(c_right, d - 1e-6)
697 if not (a < b <= c_right < d):
698 # Fallback to a minimal valid shape around center
699 eps = 1e-6
700 a, b, c_right, d = c - 2 * eps, c - eps, c + eps, c + 2 * eps
701 mfs.append(PiMF(a, b, c_right, d))
702 return mfs
704 def build(self) -> TSKANFIS:
705 """Build the ANFIS model with configured parameters."""
706 if not self.input_mfs:
707 raise ValueError("No input variables defined. Use add_input() to define inputs.")
709 return TSKANFIS(self.input_mfs, rules=self._rules)