From e8ff08f20d4ddfd4fe33a143a74b381deb6697d4 Mon Sep 17 00:00:00 2001 From: priya-sundaram-dev Date: Wed, 2 Sep 2026 21:14:16 +0000 Subject: [PATCH 1/3] 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. --- fuzzy_logic/README.md | 51 +++++ fuzzy_logic/fuzzy_operations.py.DISABLED.txt | 103 ----------- fuzzy_logic/fuzzy_set_operations.py | 184 +++++++++++++++++++ 3 files changed, 235 insertions(+), 103 deletions(-) create mode 100644 fuzzy_logic/README.md delete mode 100644 fuzzy_logic/fuzzy_operations.py.DISABLED.txt create mode 100644 fuzzy_logic/fuzzy_set_operations.py diff --git a/fuzzy_logic/README.md b/fuzzy_logic/README.md new file mode 100644 index 000000000000..39ec10e6bec1 --- /dev/null +++ b/fuzzy_logic/README.md @@ -0,0 +1,51 @@ +# Fuzzy Logic + +**Fuzzy logic** generalizes classical (crisp) set theory: instead of an element +either belonging to a set or not, it belongs to a degree between `0` and `1`. +That degree is given by a *membership function* `mu: X -> [0, 1]` over a universe +of discourse `X`. Fuzzy logic is widely used in control systems, decision making, +and pattern recognition where boundaries are naturally vague ("young", "warm", +"fast"). + +Learn more: + +- [Fuzzy logic](https://en.wikipedia.org/wiki/Fuzzy_logic) +- [Fuzzy set](https://en.wikipedia.org/wiki/Fuzzy_set) +- [Membership function](https://en.wikipedia.org/wiki/Membership_function_(mathematics)) +- [T-norm](https://en.wikipedia.org/wiki/T-norm) (the family of fuzzy AND/OR operators) + +## Contents + +| File | What it does | +| --- | --- | +| [`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. | +| [`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). | + +The two files are complementary: `fuzzy_operations.py` stays in the parametric +`(left, peak, right)` representation, while `fuzzy_set_operations.py` works on the +discretized membership arrays and therefore supports the full set of Zadeh +operators for arbitrary shapes. + +## Quick example + +```python +import numpy as np +from fuzzy_logic.fuzzy_set_operations import ( + triangular_membership, + fuzzy_union, + fuzzy_intersection, +) + +universe = np.linspace(0, 75, 75) +young = triangular_membership(universe, 0, 25, 50) +middle_aged = triangular_membership(universe, 25, 50, 75) + +young_or_middle_aged = fuzzy_union(young, middle_aged) +young_and_middle_aged = fuzzy_intersection(young, middle_aged) +``` + +Run the doctests for either module with: + +```bash +python -m doctest -v fuzzy_logic/fuzzy_set_operations.py +``` diff --git a/fuzzy_logic/fuzzy_operations.py.DISABLED.txt b/fuzzy_logic/fuzzy_operations.py.DISABLED.txt deleted file mode 100644 index 67fd587f4baf..000000000000 --- a/fuzzy_logic/fuzzy_operations.py.DISABLED.txt +++ /dev/null @@ -1,103 +0,0 @@ -""" -README, Author - Jigyasa Gandhi(mailto:jigsgandhi97@gmail.com) -Requirements: - - scikit-fuzzy - - numpy - - matplotlib -Python: - - 3.5 -""" -import numpy as np -import skfuzzy as fuzz - -if __name__ == "__main__": - # Create universe of discourse in Python using linspace () - X = np.linspace(start=0, stop=75, num=75, endpoint=True, retstep=False) - - # Create two fuzzy sets by defining any membership function - # (trapmf(), gbellmf(), gaussmf(), etc). - abc1 = [0, 25, 50] - abc2 = [25, 50, 75] - young = fuzz.membership.trimf(X, abc1) - middle_aged = fuzz.membership.trimf(X, abc2) - - # Compute the different operations using inbuilt functions. - one = np.ones(75) - zero = np.zeros((75,)) - # 1. Union = max(µA(x), µB(x)) - union = fuzz.fuzzy_or(X, young, X, middle_aged)[1] - # 2. Intersection = min(µA(x), µB(x)) - intersection = fuzz.fuzzy_and(X, young, X, middle_aged)[1] - # 3. Complement (A) = (1 - min(µA(x))) - complement_a = fuzz.fuzzy_not(young) - # 4. Difference (A/B) = min(µA(x),(1- µB(x))) - difference = fuzz.fuzzy_and(X, young, X, fuzz.fuzzy_not(middle_aged)[1])[1] - # 5. Algebraic Sum = [µA(x) + µB(x) – (µA(x) * µB(x))] - alg_sum = young + middle_aged - (young * middle_aged) - # 6. Algebraic Product = (µA(x) * µB(x)) - alg_product = young * middle_aged - # 7. Bounded Sum = min[1,(µA(x), µB(x))] - bdd_sum = fuzz.fuzzy_and(X, one, X, young + middle_aged)[1] - # 8. Bounded difference = min[0,(µA(x), µB(x))] - bdd_difference = fuzz.fuzzy_or(X, zero, X, young - middle_aged)[1] - - # max-min composition - # max-product composition - - # Plot each set A, set B and each operation result using plot() and subplot(). - from matplotlib import pyplot as plt - - plt.figure() - - plt.subplot(4, 3, 1) - plt.plot(X, young) - plt.title("Young") - plt.grid(True) - - plt.subplot(4, 3, 2) - plt.plot(X, middle_aged) - plt.title("Middle aged") - plt.grid(True) - - plt.subplot(4, 3, 3) - plt.plot(X, union) - plt.title("union") - plt.grid(True) - - plt.subplot(4, 3, 4) - plt.plot(X, intersection) - plt.title("intersection") - plt.grid(True) - - plt.subplot(4, 3, 5) - plt.plot(X, complement_a) - plt.title("complement_a") - plt.grid(True) - - plt.subplot(4, 3, 6) - plt.plot(X, difference) - plt.title("difference a/b") - plt.grid(True) - - plt.subplot(4, 3, 7) - plt.plot(X, alg_sum) - plt.title("alg_sum") - plt.grid(True) - - plt.subplot(4, 3, 8) - plt.plot(X, alg_product) - plt.title("alg_product") - plt.grid(True) - - plt.subplot(4, 3, 9) - plt.plot(X, bdd_sum) - plt.title("bdd_sum") - plt.grid(True) - - plt.subplot(4, 3, 10) - plt.plot(X, bdd_difference) - plt.title("bdd_difference") - plt.grid(True) - - plt.subplots_adjust(hspace=0.5) - plt.show() diff --git a/fuzzy_logic/fuzzy_set_operations.py b/fuzzy_logic/fuzzy_set_operations.py new file mode 100644 index 000000000000..e5ab237a8753 --- /dev/null +++ b/fuzzy_logic/fuzzy_set_operations.py @@ -0,0 +1,184 @@ +""" +Zadeh's fuzzy-set operators on membership vectors. + +A fuzzy set over a universe of discourse ``X`` is described by a *membership +function* ``mu: X -> [0, 1]``. Once the universe is sampled on a grid, that +function becomes a NumPy vector of membership degrees and the classic set +operations reduce to element-wise arithmetic. + +This module implements the standard (Zadeh) operators plus a few common +alternatives. Unlike ``fuzzy_operations.FuzzySet`` -- which stores a *triangular +fuzzy number* by its three defining points -- the functions here work on the +sampled membership vectors directly, so they apply to *any* membership shape +(triangular, trapezoidal, Gaussian, ...). + +References: + - https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations + - https://en.wikipedia.org/wiki/Fuzzy_logic + - https://en.wikipedia.org/wiki/T-norm + +Requirements: + - numpy + +Originally contributed as a ``scikit-fuzzy`` demo by Jigyasa Gandhi; rewritten +here to be dependency-free (NumPy only) and covered by doctests. +""" + +from __future__ import annotations + +import numpy as np +from numpy.typing import NDArray + + +def triangular_membership( + x: NDArray[np.float64], left: float, peak: float, right: float +) -> NDArray[np.float64]: + """ + Sample a triangular membership function on the grid ``x``. + + The membership rises linearly from 0 at ``left`` to 1 at ``peak`` and falls + back to 0 at ``right``. + + >>> x = np.array([0.0, 25.0, 50.0]) + >>> triangular_membership(x, 0, 25, 50) + array([0., 1., 0.]) + >>> triangular_membership(np.array([10.0, 12.5]), 0, 25, 50) + array([0.4, 0.5]) + """ + if not left <= peak <= right: + msg = f"Expected left <= peak <= right, got {left}, {peak}, {right}" + raise ValueError(msg) + left_slope = (x - left) / (peak - left) if peak > left else np.where(x < peak, 0, 1) + right_slope = ( + (right - x) / (right - peak) if right > peak else np.where(x > peak, 0, 1) + ) + return np.clip(np.minimum(left_slope, right_slope), 0.0, 1.0) + + +def fuzzy_union(a: NDArray[np.float64], b: NDArray[np.float64]) -> NDArray[np.float64]: + """ + Union (logical OR): ``max(mu_A(x), mu_B(x))``. + + >>> fuzzy_union(np.array([0.2, 0.7]), np.array([0.5, 0.1])) + array([0.5, 0.7]) + """ + return np.maximum(a, b) + + +def fuzzy_intersection( + a: NDArray[np.float64], b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Intersection (logical AND): ``min(mu_A(x), mu_B(x))``. + + >>> fuzzy_intersection(np.array([0.2, 0.7]), np.array([0.5, 0.1])) + array([0.2, 0.1]) + """ + return np.minimum(a, b) + + +def fuzzy_complement(a: NDArray[np.float64]) -> NDArray[np.float64]: + """ + Complement (logical NOT): ``1 - mu_A(x)``. + + >>> fuzzy_complement(np.array([0.0, 0.3, 1.0])) + array([1. , 0.7, 0. ]) + """ + return 1.0 - a + + +def fuzzy_difference( + a: NDArray[np.float64], b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Difference ``A / B``: ``min(mu_A(x), 1 - mu_B(x))``. + + >>> fuzzy_difference(np.array([0.6, 0.4]), np.array([0.2, 0.9])) + array([0.6, 0.1]) + """ + return np.minimum(a, 1.0 - b) + + +def algebraic_sum( + a: NDArray[np.float64], b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Algebraic (probabilistic) sum: ``mu_A + mu_B - mu_A * mu_B``. + + >>> algebraic_sum(np.array([0.5, 1.0]), np.array([0.5, 0.2])) + array([0.75, 1. ]) + """ + return a + b - a * b + + +def algebraic_product( + a: NDArray[np.float64], b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Algebraic product: ``mu_A * mu_B``. + + >>> algebraic_product(np.array([0.5, 1.0]), np.array([0.5, 0.2])) + array([0.25, 0.2 ]) + """ + return a * b + + +def bounded_sum(a: NDArray[np.float64], b: NDArray[np.float64]) -> NDArray[np.float64]: + """ + Bounded sum (Lukasiewicz t-conorm): ``min(1, mu_A + mu_B)``. + + >>> bounded_sum(np.array([0.5, 0.8]), np.array([0.2, 0.7])) + array([0.7, 1. ]) + """ + return np.minimum(1.0, a + b) + + +def bounded_difference( + a: NDArray[np.float64], b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Bounded difference (Lukasiewicz t-norm): ``max(0, mu_A + mu_B - 1)``. + + >>> bounded_difference(np.array([0.5, 0.8]), np.array([0.2, 0.7])) + array([0. , 0.5]) + """ + return np.maximum(0.0, a + b - 1.0) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + # Reproduce the original "young vs. middle-aged" demo, dependency-free. + universe = np.linspace(start=0, stop=75, num=75) + young = triangular_membership(universe, 0, 25, 50) + middle_aged = triangular_membership(universe, 25, 50, 75) + + operations = { + "young": young, + "middle_aged": middle_aged, + "union": fuzzy_union(young, middle_aged), + "intersection": fuzzy_intersection(young, middle_aged), + "complement(young)": fuzzy_complement(young), + "difference young/middle": fuzzy_difference(young, middle_aged), + "algebraic_sum": algebraic_sum(young, middle_aged), + "algebraic_product": algebraic_product(young, middle_aged), + "bounded_sum": bounded_sum(young, middle_aged), + "bounded_difference": bounded_difference(young, middle_aged), + } + + try: + import matplotlib.pyplot as plt + + plt.figure() + for index, (title, values) in enumerate(operations.items(), start=1): + plt.subplot(4, 3, index) + plt.plot(universe, values) + plt.title(title) + plt.grid(True) + plt.subplots_adjust(hspace=0.5) + plt.show() + except ImportError: + for title, values in operations.items(): + print(f"{title}: peak membership = {values.max():.3f}") From 20458063c4b108fae666f608909151f4d4d99c1e Mon Sep 17 00:00:00 2001 From: priya-sundaram-dev Date: Wed, 2 Sep 2026 22:39:05 +0000 Subject: [PATCH 2/3] 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) --- fuzzy_logic/fuzzy_set_operations.py | 184 ---------------------------- 1 file changed, 184 deletions(-) diff --git a/fuzzy_logic/fuzzy_set_operations.py b/fuzzy_logic/fuzzy_set_operations.py index e5ab237a8753..e69de29bb2d1 100644 --- a/fuzzy_logic/fuzzy_set_operations.py +++ b/fuzzy_logic/fuzzy_set_operations.py @@ -1,184 +0,0 @@ -""" -Zadeh's fuzzy-set operators on membership vectors. - -A fuzzy set over a universe of discourse ``X`` is described by a *membership -function* ``mu: X -> [0, 1]``. Once the universe is sampled on a grid, that -function becomes a NumPy vector of membership degrees and the classic set -operations reduce to element-wise arithmetic. - -This module implements the standard (Zadeh) operators plus a few common -alternatives. Unlike ``fuzzy_operations.FuzzySet`` -- which stores a *triangular -fuzzy number* by its three defining points -- the functions here work on the -sampled membership vectors directly, so they apply to *any* membership shape -(triangular, trapezoidal, Gaussian, ...). - -References: - - https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations - - https://en.wikipedia.org/wiki/Fuzzy_logic - - https://en.wikipedia.org/wiki/T-norm - -Requirements: - - numpy - -Originally contributed as a ``scikit-fuzzy`` demo by Jigyasa Gandhi; rewritten -here to be dependency-free (NumPy only) and covered by doctests. -""" - -from __future__ import annotations - -import numpy as np -from numpy.typing import NDArray - - -def triangular_membership( - x: NDArray[np.float64], left: float, peak: float, right: float -) -> NDArray[np.float64]: - """ - Sample a triangular membership function on the grid ``x``. - - The membership rises linearly from 0 at ``left`` to 1 at ``peak`` and falls - back to 0 at ``right``. - - >>> x = np.array([0.0, 25.0, 50.0]) - >>> triangular_membership(x, 0, 25, 50) - array([0., 1., 0.]) - >>> triangular_membership(np.array([10.0, 12.5]), 0, 25, 50) - array([0.4, 0.5]) - """ - if not left <= peak <= right: - msg = f"Expected left <= peak <= right, got {left}, {peak}, {right}" - raise ValueError(msg) - left_slope = (x - left) / (peak - left) if peak > left else np.where(x < peak, 0, 1) - right_slope = ( - (right - x) / (right - peak) if right > peak else np.where(x > peak, 0, 1) - ) - return np.clip(np.minimum(left_slope, right_slope), 0.0, 1.0) - - -def fuzzy_union(a: NDArray[np.float64], b: NDArray[np.float64]) -> NDArray[np.float64]: - """ - Union (logical OR): ``max(mu_A(x), mu_B(x))``. - - >>> fuzzy_union(np.array([0.2, 0.7]), np.array([0.5, 0.1])) - array([0.5, 0.7]) - """ - return np.maximum(a, b) - - -def fuzzy_intersection( - a: NDArray[np.float64], b: NDArray[np.float64] -) -> NDArray[np.float64]: - """ - Intersection (logical AND): ``min(mu_A(x), mu_B(x))``. - - >>> fuzzy_intersection(np.array([0.2, 0.7]), np.array([0.5, 0.1])) - array([0.2, 0.1]) - """ - return np.minimum(a, b) - - -def fuzzy_complement(a: NDArray[np.float64]) -> NDArray[np.float64]: - """ - Complement (logical NOT): ``1 - mu_A(x)``. - - >>> fuzzy_complement(np.array([0.0, 0.3, 1.0])) - array([1. , 0.7, 0. ]) - """ - return 1.0 - a - - -def fuzzy_difference( - a: NDArray[np.float64], b: NDArray[np.float64] -) -> NDArray[np.float64]: - """ - Difference ``A / B``: ``min(mu_A(x), 1 - mu_B(x))``. - - >>> fuzzy_difference(np.array([0.6, 0.4]), np.array([0.2, 0.9])) - array([0.6, 0.1]) - """ - return np.minimum(a, 1.0 - b) - - -def algebraic_sum( - a: NDArray[np.float64], b: NDArray[np.float64] -) -> NDArray[np.float64]: - """ - Algebraic (probabilistic) sum: ``mu_A + mu_B - mu_A * mu_B``. - - >>> algebraic_sum(np.array([0.5, 1.0]), np.array([0.5, 0.2])) - array([0.75, 1. ]) - """ - return a + b - a * b - - -def algebraic_product( - a: NDArray[np.float64], b: NDArray[np.float64] -) -> NDArray[np.float64]: - """ - Algebraic product: ``mu_A * mu_B``. - - >>> algebraic_product(np.array([0.5, 1.0]), np.array([0.5, 0.2])) - array([0.25, 0.2 ]) - """ - return a * b - - -def bounded_sum(a: NDArray[np.float64], b: NDArray[np.float64]) -> NDArray[np.float64]: - """ - Bounded sum (Lukasiewicz t-conorm): ``min(1, mu_A + mu_B)``. - - >>> bounded_sum(np.array([0.5, 0.8]), np.array([0.2, 0.7])) - array([0.7, 1. ]) - """ - return np.minimum(1.0, a + b) - - -def bounded_difference( - a: NDArray[np.float64], b: NDArray[np.float64] -) -> NDArray[np.float64]: - """ - Bounded difference (Lukasiewicz t-norm): ``max(0, mu_A + mu_B - 1)``. - - >>> bounded_difference(np.array([0.5, 0.8]), np.array([0.2, 0.7])) - array([0. , 0.5]) - """ - return np.maximum(0.0, a + b - 1.0) - - -if __name__ == "__main__": - from doctest import testmod - - testmod() - - # Reproduce the original "young vs. middle-aged" demo, dependency-free. - universe = np.linspace(start=0, stop=75, num=75) - young = triangular_membership(universe, 0, 25, 50) - middle_aged = triangular_membership(universe, 25, 50, 75) - - operations = { - "young": young, - "middle_aged": middle_aged, - "union": fuzzy_union(young, middle_aged), - "intersection": fuzzy_intersection(young, middle_aged), - "complement(young)": fuzzy_complement(young), - "difference young/middle": fuzzy_difference(young, middle_aged), - "algebraic_sum": algebraic_sum(young, middle_aged), - "algebraic_product": algebraic_product(young, middle_aged), - "bounded_sum": bounded_sum(young, middle_aged), - "bounded_difference": bounded_difference(young, middle_aged), - } - - try: - import matplotlib.pyplot as plt - - plt.figure() - for index, (title, values) in enumerate(operations.items(), start=1): - plt.subplot(4, 3, index) - plt.plot(universe, values) - plt.title(title) - plt.grid(True) - plt.subplots_adjust(hspace=0.5) - plt.show() - except ImportError: - for title, values in operations.items(): - print(f"{title}: peak membership = {values.max():.3f}") From bbef392c8beb394abc6d01a33528299876aaa38f Mon Sep 17 00:00:00 2001 From: priya-sundaram-dev Date: Wed, 2 Sep 2026 22:40:18 +0000 Subject: [PATCH 3/3] 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) --- fuzzy_logic/fuzzy_set_operations.py | 188 ++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/fuzzy_logic/fuzzy_set_operations.py b/fuzzy_logic/fuzzy_set_operations.py index e69de29bb2d1..5ea22d2ca90d 100644 --- a/fuzzy_logic/fuzzy_set_operations.py +++ b/fuzzy_logic/fuzzy_set_operations.py @@ -0,0 +1,188 @@ +""" +Zadeh's fuzzy-set operators on membership vectors. + +A fuzzy set over a universe of discourse ``X`` is described by a *membership +function* ``mu: X -> [0, 1]``. Once the universe is sampled on a grid, that +function becomes a NumPy vector of membership degrees and the classic set +operations reduce to element-wise arithmetic. + +This module implements the standard (Zadeh) operators plus a few common +alternatives. Unlike ``fuzzy_operations.FuzzySet`` -- which stores a *triangular +fuzzy number* by its three defining points -- the functions here work on the +sampled membership vectors directly, so they apply to *any* membership shape +(triangular, trapezoidal, Gaussian, ...). + +References: + - https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations + - https://en.wikipedia.org/wiki/Fuzzy_logic + - https://en.wikipedia.org/wiki/T-norm + +Requirements: + - numpy + +Originally contributed as a ``scikit-fuzzy`` demo by Jigyasa Gandhi; rewritten +here to be dependency-free (NumPy only) and covered by doctests. +""" + +import numpy as np +from numpy.typing import NDArray + + +def triangular_membership( + grid: NDArray[np.float64], left: float, peak: float, right: float +) -> NDArray[np.float64]: + """ + Sample a triangular membership function on the ``grid``. + + The membership rises linearly from 0 at ``left`` to 1 at ``peak`` and falls + back to 0 at ``right``. + + >>> grid = np.array([0.0, 25.0, 50.0]) + >>> triangular_membership(grid, 0, 25, 50) + array([0., 1., 0.]) + >>> triangular_membership(np.array([10.0, 12.5]), 0, 25, 50) + array([0.4, 0.5]) + """ + if not left <= peak <= right: + msg = f"Expected left <= peak <= right, got {left}, {peak}, {right}" + raise ValueError(msg) + left_slope = ( + (grid - left) / (peak - left) if peak > left else np.where(grid < peak, 0, 1) + ) + right_slope = ( + (right - grid) / (right - peak) if right > peak else np.where(grid > peak, 0, 1) + ) + return np.clip(np.minimum(left_slope, right_slope), 0.0, 1.0) + + +def fuzzy_union( + membership_a: NDArray[np.float64], membership_b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Union (logical OR): ``max(mu_A(x), mu_B(x))``. + + >>> fuzzy_union(np.array([0.2, 0.7]), np.array([0.5, 0.1])) + array([0.5, 0.7]) + """ + return np.maximum(membership_a, membership_b) + + +def fuzzy_intersection( + membership_a: NDArray[np.float64], membership_b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Intersection (logical AND): ``min(mu_A(x), mu_B(x))``. + + >>> fuzzy_intersection(np.array([0.2, 0.7]), np.array([0.5, 0.1])) + array([0.2, 0.1]) + """ + return np.minimum(membership_a, membership_b) + + +def fuzzy_complement(membership: NDArray[np.float64]) -> NDArray[np.float64]: + """ + Complement (logical NOT): ``1 - mu_A(x)``. + + >>> fuzzy_complement(np.array([0.0, 0.3, 1.0])) + array([1. , 0.7, 0. ]) + """ + return 1.0 - membership + + +def fuzzy_difference( + membership_a: NDArray[np.float64], membership_b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Difference ``A / B``: ``min(mu_A(x), 1 - mu_B(x))``. + + >>> fuzzy_difference(np.array([0.6, 0.4]), np.array([0.2, 0.9])) + array([0.6, 0.1]) + """ + return np.minimum(membership_a, 1.0 - membership_b) + + +def algebraic_sum( + membership_a: NDArray[np.float64], membership_b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Algebraic (probabilistic) sum: ``mu_A + mu_B - mu_A * mu_B``. + + >>> algebraic_sum(np.array([0.5, 1.0]), np.array([0.5, 0.2])) + array([0.75, 1. ]) + """ + return membership_a + membership_b - membership_a * membership_b + + +def algebraic_product( + membership_a: NDArray[np.float64], membership_b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Algebraic product: ``mu_A * mu_B``. + + >>> algebraic_product(np.array([0.5, 1.0]), np.array([0.5, 0.2])) + array([0.25, 0.2 ]) + """ + return membership_a * membership_b + + +def bounded_sum( + membership_a: NDArray[np.float64], membership_b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Bounded sum (Lukasiewicz t-conorm): ``min(1, mu_A + mu_B)``. + + >>> bounded_sum(np.array([0.5, 0.8]), np.array([0.2, 0.7])) + array([0.7, 1. ]) + """ + return np.minimum(1.0, membership_a + membership_b) + + +def bounded_difference( + membership_a: NDArray[np.float64], membership_b: NDArray[np.float64] +) -> NDArray[np.float64]: + """ + Bounded difference (Lukasiewicz t-norm): ``max(0, mu_A + mu_B - 1)``. + + >>> bounded_difference(np.array([0.5, 0.8]), np.array([0.2, 0.7])) + array([0. , 0.5]) + """ + return np.maximum(0.0, membership_a + membership_b - 1.0) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() + + # Reproduce the original "young vs. middle-aged" demo, dependency-free. + universe = np.linspace(start=0, stop=75, num=75) + young = triangular_membership(universe, 0, 25, 50) + middle_aged = triangular_membership(universe, 25, 50, 75) + + operations = { + "young": young, + "middle_aged": middle_aged, + "union": fuzzy_union(young, middle_aged), + "intersection": fuzzy_intersection(young, middle_aged), + "complement(young)": fuzzy_complement(young), + "difference young/middle": fuzzy_difference(young, middle_aged), + "algebraic_sum": algebraic_sum(young, middle_aged), + "algebraic_product": algebraic_product(young, middle_aged), + "bounded_sum": bounded_sum(young, middle_aged), + "bounded_difference": bounded_difference(young, middle_aged), + } + + try: + import matplotlib.pyplot as plt + + plt.figure() + for index, (title, values) in enumerate(operations.items(), start=1): + plt.subplot(4, 3, index) + plt.plot(universe, values) + plt.title(title) + plt.grid(True) + plt.subplots_adjust(hspace=0.5) + plt.show() + except ImportError: + for title, values in operations.items(): + print(f"{title}: peak membership = {values.max():.3f}")