|
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