Skip to content

Commit e8ff08f

Browse files
TLC for fuzzy_logic/: restore disabled demo dependency-free + add README
- Restore fuzzy_operations.py.DISABLED.txt as fuzzy_set_operations.py, rewritten to drop the scikit-fuzzy dependency (NumPy only). Implements the Zadeh operators on sampled membership vectors: union, intersection, complement, difference, algebraic sum/product, bounded sum/difference, plus a triangular_membership helper. Every function has doctests; the __main__ block reproduces the original 'young vs. middle-aged' demo and degrades gracefully when matplotlib is absent. - Add fuzzy_logic/README.md covering both modules with Wikipedia links and a runnable example. - Remove the obsolete .DISABLED.txt (its intent is now the working module). Credit to Jigyasa Gandhi (original scikit-fuzzy demo) and @Shreya123714 (FuzzySet). Passes pytest --doctest-modules, ruff check, ruff format --check.
1 parent ea0d2d7 commit e8ff08f

3 files changed

Lines changed: 235 additions & 103 deletions

File tree

fuzzy_logic/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Fuzzy Logic
2+
3+
**Fuzzy logic** generalizes classical (crisp) set theory: instead of an element
4+
either belonging to a set or not, it belongs to a degree between `0` and `1`.
5+
That degree is given by a *membership function* `mu: X -> [0, 1]` over a universe
6+
of discourse `X`. Fuzzy logic is widely used in control systems, decision making,
7+
and pattern recognition where boundaries are naturally vague ("young", "warm",
8+
"fast").
9+
10+
Learn more:
11+
12+
- [Fuzzy logic](https://en.wikipedia.org/wiki/Fuzzy_logic)
13+
- [Fuzzy set](https://en.wikipedia.org/wiki/Fuzzy_set)
14+
- [Membership function](https://en.wikipedia.org/wiki/Membership_function_(mathematics))
15+
- [T-norm](https://en.wikipedia.org/wiki/T-norm) (the family of fuzzy AND/OR operators)
16+
17+
## Contents
18+
19+
| File | What it does |
20+
| --- | --- |
21+
| [`fuzzy_operations.py`](fuzzy_operations.py) | A `FuzzySet` class modelling a **triangular fuzzy number** by its `(left, peak, right)` points, with `membership`, `union`, `intersection`, `complement`, and plotting. Best when your fuzzy sets are triangular and you want to keep working with the parameters. |
22+
| [`fuzzy_set_operations.py`](fuzzy_set_operations.py) | The classic **Zadeh operators on sampled membership vectors**: `union`, `intersection`, `complement`, `difference`, `algebraic_sum`/`product`, and `bounded_sum`/`difference`. Works on *any* membership shape (triangular, trapezoidal, Gaussian, ...) because it operates on the sampled values directly. Dependency-free (NumPy only). |
23+
24+
The two files are complementary: `fuzzy_operations.py` stays in the parametric
25+
`(left, peak, right)` representation, while `fuzzy_set_operations.py` works on the
26+
discretized membership arrays and therefore supports the full set of Zadeh
27+
operators for arbitrary shapes.
28+
29+
## Quick example
30+
31+
```python
32+
import numpy as np
33+
from fuzzy_logic.fuzzy_set_operations import (
34+
triangular_membership,
35+
fuzzy_union,
36+
fuzzy_intersection,
37+
)
38+
39+
universe = np.linspace(0, 75, 75)
40+
young = triangular_membership(universe, 0, 25, 50)
41+
middle_aged = triangular_membership(universe, 25, 50, 75)
42+
43+
young_or_middle_aged = fuzzy_union(young, middle_aged)
44+
young_and_middle_aged = fuzzy_intersection(young, middle_aged)
45+
```
46+
47+
Run the doctests for either module with:
48+
49+
```bash
50+
python -m doctest -v fuzzy_logic/fuzzy_set_operations.py
51+
```

fuzzy_logic/fuzzy_operations.py.DISABLED.txt

Lines changed: 0 additions & 103 deletions
This file was deleted.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
from __future__ import annotations
28+
29+
import numpy as np
30+
from numpy.typing import NDArray
31+
32+
33+
def triangular_membership(
34+
x: NDArray[np.float64], left: float, peak: float, right: float
35+
) -> NDArray[np.float64]:
36+
"""
37+
Sample a triangular membership function on the grid ``x``.
38+
39+
The membership rises linearly from 0 at ``left`` to 1 at ``peak`` and falls
40+
back to 0 at ``right``.
41+
42+
>>> x = np.array([0.0, 25.0, 50.0])
43+
>>> triangular_membership(x, 0, 25, 50)
44+
array([0., 1., 0.])
45+
>>> triangular_membership(np.array([10.0, 12.5]), 0, 25, 50)
46+
array([0.4, 0.5])
47+
"""
48+
if not left <= peak <= right:
49+
msg = f"Expected left <= peak <= right, got {left}, {peak}, {right}"
50+
raise ValueError(msg)
51+
left_slope = (x - left) / (peak - left) if peak > left else np.where(x < peak, 0, 1)
52+
right_slope = (
53+
(right - x) / (right - peak) if right > peak else np.where(x > peak, 0, 1)
54+
)
55+
return np.clip(np.minimum(left_slope, right_slope), 0.0, 1.0)
56+
57+
58+
def fuzzy_union(a: NDArray[np.float64], b: NDArray[np.float64]) -> NDArray[np.float64]:
59+
"""
60+
Union (logical OR): ``max(mu_A(x), mu_B(x))``.
61+
62+
>>> fuzzy_union(np.array([0.2, 0.7]), np.array([0.5, 0.1]))
63+
array([0.5, 0.7])
64+
"""
65+
return np.maximum(a, b)
66+
67+
68+
def fuzzy_intersection(
69+
a: NDArray[np.float64], b: NDArray[np.float64]
70+
) -> NDArray[np.float64]:
71+
"""
72+
Intersection (logical AND): ``min(mu_A(x), mu_B(x))``.
73+
74+
>>> fuzzy_intersection(np.array([0.2, 0.7]), np.array([0.5, 0.1]))
75+
array([0.2, 0.1])
76+
"""
77+
return np.minimum(a, b)
78+
79+
80+
def fuzzy_complement(a: NDArray[np.float64]) -> NDArray[np.float64]:
81+
"""
82+
Complement (logical NOT): ``1 - mu_A(x)``.
83+
84+
>>> fuzzy_complement(np.array([0.0, 0.3, 1.0]))
85+
array([1. , 0.7, 0. ])
86+
"""
87+
return 1.0 - a
88+
89+
90+
def fuzzy_difference(
91+
a: NDArray[np.float64], b: NDArray[np.float64]
92+
) -> NDArray[np.float64]:
93+
"""
94+
Difference ``A / B``: ``min(mu_A(x), 1 - mu_B(x))``.
95+
96+
>>> fuzzy_difference(np.array([0.6, 0.4]), np.array([0.2, 0.9]))
97+
array([0.6, 0.1])
98+
"""
99+
return np.minimum(a, 1.0 - b)
100+
101+
102+
def algebraic_sum(
103+
a: NDArray[np.float64], b: NDArray[np.float64]
104+
) -> NDArray[np.float64]:
105+
"""
106+
Algebraic (probabilistic) sum: ``mu_A + mu_B - mu_A * mu_B``.
107+
108+
>>> algebraic_sum(np.array([0.5, 1.0]), np.array([0.5, 0.2]))
109+
array([0.75, 1. ])
110+
"""
111+
return a + b - a * b
112+
113+
114+
def algebraic_product(
115+
a: NDArray[np.float64], b: NDArray[np.float64]
116+
) -> NDArray[np.float64]:
117+
"""
118+
Algebraic product: ``mu_A * mu_B``.
119+
120+
>>> algebraic_product(np.array([0.5, 1.0]), np.array([0.5, 0.2]))
121+
array([0.25, 0.2 ])
122+
"""
123+
return a * b
124+
125+
126+
def bounded_sum(a: NDArray[np.float64], b: NDArray[np.float64]) -> NDArray[np.float64]:
127+
"""
128+
Bounded sum (Lukasiewicz t-conorm): ``min(1, mu_A + mu_B)``.
129+
130+
>>> bounded_sum(np.array([0.5, 0.8]), np.array([0.2, 0.7]))
131+
array([0.7, 1. ])
132+
"""
133+
return np.minimum(1.0, a + b)
134+
135+
136+
def bounded_difference(
137+
a: NDArray[np.float64], b: NDArray[np.float64]
138+
) -> NDArray[np.float64]:
139+
"""
140+
Bounded difference (Lukasiewicz t-norm): ``max(0, mu_A + mu_B - 1)``.
141+
142+
>>> bounded_difference(np.array([0.5, 0.8]), np.array([0.2, 0.7]))
143+
array([0. , 0.5])
144+
"""
145+
return np.maximum(0.0, a + b - 1.0)
146+
147+
148+
if __name__ == "__main__":
149+
from doctest import testmod
150+
151+
testmod()
152+
153+
# Reproduce the original "young vs. middle-aged" demo, dependency-free.
154+
universe = np.linspace(start=0, stop=75, num=75)
155+
young = triangular_membership(universe, 0, 25, 50)
156+
middle_aged = triangular_membership(universe, 25, 50, 75)
157+
158+
operations = {
159+
"young": young,
160+
"middle_aged": middle_aged,
161+
"union": fuzzy_union(young, middle_aged),
162+
"intersection": fuzzy_intersection(young, middle_aged),
163+
"complement(young)": fuzzy_complement(young),
164+
"difference young/middle": fuzzy_difference(young, middle_aged),
165+
"algebraic_sum": algebraic_sum(young, middle_aged),
166+
"algebraic_product": algebraic_product(young, middle_aged),
167+
"bounded_sum": bounded_sum(young, middle_aged),
168+
"bounded_difference": bounded_difference(young, middle_aged),
169+
}
170+
171+
try:
172+
import matplotlib.pyplot as plt
173+
174+
plt.figure()
175+
for index, (title, values) in enumerate(operations.items(), start=1):
176+
plt.subplot(4, 3, index)
177+
plt.plot(universe, values)
178+
plt.title(title)
179+
plt.grid(True)
180+
plt.subplots_adjust(hspace=0.5)
181+
plt.show()
182+
except ImportError:
183+
for title, values in operations.items():
184+
print(f"{title}: peak membership = {values.max():.3f}")

0 commit comments

Comments
 (0)