Skip to content

Commit bbef392

Browse files
fuzzy_logic: descriptive parameter names + drop __future__ annotations
Address review on #15166: - rename single-letter params (x->grid, a/b->membership_a/membership_b, fuzzy_complement arg -> membership) per algorithms-keeper + cclauss - drop 'from __future__ import annotations' (repo is Python 3.14t-only)
1 parent 2045806 commit bbef392

1 file changed

Lines changed: 188 additions & 0 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""
2+
Zadeh's fuzzy-set operators on membership vectors.
3+
4+
A fuzzy set over a universe of discourse ``X`` is described by a *membership
5+
function* ``mu: X -> [0, 1]``. Once the universe is sampled on a grid, that
6+
function becomes a NumPy vector of membership degrees and the classic set
7+
operations reduce to element-wise arithmetic.
8+
9+
This module implements the standard (Zadeh) operators plus a few common
10+
alternatives. Unlike ``fuzzy_operations.FuzzySet`` -- which stores a *triangular
11+
fuzzy number* by its three defining points -- the functions here work on the
12+
sampled membership vectors directly, so they apply to *any* membership shape
13+
(triangular, trapezoidal, Gaussian, ...).
14+
15+
References:
16+
- https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations
17+
- https://en.wikipedia.org/wiki/Fuzzy_logic
18+
- https://en.wikipedia.org/wiki/T-norm
19+
20+
Requirements:
21+
- numpy
22+
23+
Originally contributed as a ``scikit-fuzzy`` demo by Jigyasa Gandhi; rewritten
24+
here to be dependency-free (NumPy only) and covered by doctests.
25+
"""
26+
27+
import numpy as np
28+
from numpy.typing import NDArray
29+
30+
31+
def triangular_membership(
32+
grid: NDArray[np.float64], left: float, peak: float, right: float
33+
) -> NDArray[np.float64]:
34+
"""
35+
Sample a triangular membership function on the ``grid``.
36+
37+
The membership rises linearly from 0 at ``left`` to 1 at ``peak`` and falls
38+
back to 0 at ``right``.
39+
40+
>>> grid = np.array([0.0, 25.0, 50.0])
41+
>>> triangular_membership(grid, 0, 25, 50)
42+
array([0., 1., 0.])
43+
>>> triangular_membership(np.array([10.0, 12.5]), 0, 25, 50)
44+
array([0.4, 0.5])
45+
"""
46+
if not left <= peak <= right:
47+
msg = f"Expected left <= peak <= right, got {left}, {peak}, {right}"
48+
raise ValueError(msg)
49+
left_slope = (
50+
(grid - left) / (peak - left) if peak > left else np.where(grid < peak, 0, 1)
51+
)
52+
right_slope = (
53+
(right - grid) / (right - peak) if right > peak else np.where(grid > peak, 0, 1)
54+
)
55+
return np.clip(np.minimum(left_slope, right_slope), 0.0, 1.0)
56+
57+
58+
def fuzzy_union(
59+
membership_a: NDArray[np.float64], membership_b: NDArray[np.float64]
60+
) -> NDArray[np.float64]:
61+
"""
62+
Union (logical OR): ``max(mu_A(x), mu_B(x))``.
63+
64+
>>> fuzzy_union(np.array([0.2, 0.7]), np.array([0.5, 0.1]))
65+
array([0.5, 0.7])
66+
"""
67+
return np.maximum(membership_a, membership_b)
68+
69+
70+
def fuzzy_intersection(
71+
membership_a: NDArray[np.float64], membership_b: NDArray[np.float64]
72+
) -> NDArray[np.float64]:
73+
"""
74+
Intersection (logical AND): ``min(mu_A(x), mu_B(x))``.
75+
76+
>>> fuzzy_intersection(np.array([0.2, 0.7]), np.array([0.5, 0.1]))
77+
array([0.2, 0.1])
78+
"""
79+
return np.minimum(membership_a, membership_b)
80+
81+
82+
def fuzzy_complement(membership: NDArray[np.float64]) -> NDArray[np.float64]:
83+
"""
84+
Complement (logical NOT): ``1 - mu_A(x)``.
85+
86+
>>> fuzzy_complement(np.array([0.0, 0.3, 1.0]))
87+
array([1. , 0.7, 0. ])
88+
"""
89+
return 1.0 - membership
90+
91+
92+
def fuzzy_difference(
93+
membership_a: NDArray[np.float64], membership_b: NDArray[np.float64]
94+
) -> NDArray[np.float64]:
95+
"""
96+
Difference ``A / B``: ``min(mu_A(x), 1 - mu_B(x))``.
97+
98+
>>> fuzzy_difference(np.array([0.6, 0.4]), np.array([0.2, 0.9]))
99+
array([0.6, 0.1])
100+
"""
101+
return np.minimum(membership_a, 1.0 - membership_b)
102+
103+
104+
def algebraic_sum(
105+
membership_a: NDArray[np.float64], membership_b: NDArray[np.float64]
106+
) -> NDArray[np.float64]:
107+
"""
108+
Algebraic (probabilistic) sum: ``mu_A + mu_B - mu_A * mu_B``.
109+
110+
>>> algebraic_sum(np.array([0.5, 1.0]), np.array([0.5, 0.2]))
111+
array([0.75, 1. ])
112+
"""
113+
return membership_a + membership_b - membership_a * membership_b
114+
115+
116+
def algebraic_product(
117+
membership_a: NDArray[np.float64], membership_b: NDArray[np.float64]
118+
) -> NDArray[np.float64]:
119+
"""
120+
Algebraic product: ``mu_A * mu_B``.
121+
122+
>>> algebraic_product(np.array([0.5, 1.0]), np.array([0.5, 0.2]))
123+
array([0.25, 0.2 ])
124+
"""
125+
return membership_a * membership_b
126+
127+
128+
def bounded_sum(
129+
membership_a: NDArray[np.float64], membership_b: NDArray[np.float64]
130+
) -> NDArray[np.float64]:
131+
"""
132+
Bounded sum (Lukasiewicz t-conorm): ``min(1, mu_A + mu_B)``.
133+
134+
>>> bounded_sum(np.array([0.5, 0.8]), np.array([0.2, 0.7]))
135+
array([0.7, 1. ])
136+
"""
137+
return np.minimum(1.0, membership_a + membership_b)
138+
139+
140+
def bounded_difference(
141+
membership_a: NDArray[np.float64], membership_b: NDArray[np.float64]
142+
) -> NDArray[np.float64]:
143+
"""
144+
Bounded difference (Lukasiewicz t-norm): ``max(0, mu_A + mu_B - 1)``.
145+
146+
>>> bounded_difference(np.array([0.5, 0.8]), np.array([0.2, 0.7]))
147+
array([0. , 0.5])
148+
"""
149+
return np.maximum(0.0, membership_a + membership_b - 1.0)
150+
151+
152+
if __name__ == "__main__":
153+
from doctest import testmod
154+
155+
testmod()
156+
157+
# Reproduce the original "young vs. middle-aged" demo, dependency-free.
158+
universe = np.linspace(start=0, stop=75, num=75)
159+
young = triangular_membership(universe, 0, 25, 50)
160+
middle_aged = triangular_membership(universe, 25, 50, 75)
161+
162+
operations = {
163+
"young": young,
164+
"middle_aged": middle_aged,
165+
"union": fuzzy_union(young, middle_aged),
166+
"intersection": fuzzy_intersection(young, middle_aged),
167+
"complement(young)": fuzzy_complement(young),
168+
"difference young/middle": fuzzy_difference(young, middle_aged),
169+
"algebraic_sum": algebraic_sum(young, middle_aged),
170+
"algebraic_product": algebraic_product(young, middle_aged),
171+
"bounded_sum": bounded_sum(young, middle_aged),
172+
"bounded_difference": bounded_difference(young, middle_aged),
173+
}
174+
175+
try:
176+
import matplotlib.pyplot as plt
177+
178+
plt.figure()
179+
for index, (title, values) in enumerate(operations.items(), start=1):
180+
plt.subplot(4, 3, index)
181+
plt.plot(universe, values)
182+
plt.title(title)
183+
plt.grid(True)
184+
plt.subplots_adjust(hspace=0.5)
185+
plt.show()
186+
except ImportError:
187+
for title, values in operations.items():
188+
print(f"{title}: peak membership = {values.max():.3f}")

0 commit comments

Comments
 (0)