Source code for zuffy._fpt_operators

"""
This module contains a set of fuzzy logic operators designed for use with Zuffy,
suitable for constructing Fuzzy Pattern Trees (FPT). Each operator performs
a specific mathematical operation on NumPy arrays, representing fuzzy set memberships.

The functions can be organised thus:
    1. Basic Fuzzy Operators
        * _minimum (MINIMUM/and)
        * _maximum (MAXIMUM/or)
        * _complement (COMPLEMENT/not)

    2. Linguistic Hedges
        * _diluter (DILUTER)
        * _diluter_power (used by DILUTER3, DILUTER4)
        * _concentrator (CONCENTRATOR)
        * _concentrator_power (used by CONCENTRATOR3, CONCENTRATOR4)
        * _intensifier (INTENSIFIER)
        * _diffuser (DIFFUSER)

    3. Averaging Operators
        * _weighted_average (used by WA_P1 to WA_P9)
        * _ordered_weighted_average (used by OWA_P1 to OWA_P9)

    4. T-Norms and T-Conorms
        * _fuzzy_and (FUZZY_AND - specifically product t-norm)
        * _fuzzy_or (FUZZY_OR - specifically probabilistic sum t-conorm)
        * _lukasiewicz_t_norm (LUKASIEWICZ/AND)
        * _lukasiewicz_t_conorm (LUKASIEWICZ/OR)
        * _hamacher_t_norm (used by HAMACHER025, HAMACHER050)
        * _product_t_norm (PRODUCT)

    5. Conditional Operators
        * _if_gte (IFGTE)
        * _if_gte_else (IFGTE2)
        * _if_lt (IFLT)
        * _if_lt_else (IFLT2)

"""


from typing import Union

import numpy as np
from gplearn import functions

# Define type alias for clarity
ArrayLike = Union[np.ndarray, float]

[docs] def _weighted_average(a: ArrayLike, b: ArrayLike, x: float) -> ArrayLike: """ Calculates the Weighted Average: x*a + (1-x)*b. Parameters ---------- a : np.ndarray or float The first operand, typically a fuzzy set membership value or array. b : np.ndarray or float The second operand, typically a fuzzy set membership value or array. x : float The weight to apply to 'a', with (1-x) applied to 'b'. Should be in the range [0, 1] for typical fuzzy operations. Returns ------- np.ndarray or float The result of the weighted average operation. """ # Ensure x is a float for consistent calculations x = float(x) return x * a + (1.0 - x) * b
[docs] def _ordered_weighted_average(a: ArrayLike, b: ArrayLike, x: float) -> ArrayLike: """ Calculates the Ordered Weighted Average (OWA): x*max(a, b) + (1-x)*min(a, b). Parameters ---------- a : np.ndarray or float The first operand. b : np.ndarray or float The second operand. x : float The weight to apply to the maximum of 'a' and 'b', with (1-x) applied to the minimum. Should be in the range [0, 1]. Returns ------- np.ndarray or float The result of the OWA operation. """ x = float(x) return x * np.maximum(a, b) + (1.0 - x) * np.minimum(a, b)
[docs] def _minimum(x0: ArrayLike, x1: ArrayLike) -> ArrayLike: """ Performs the Minimum operation, equivalent to a boolean AND in fuzzy sets. Parameters ---------- x0 : np.ndarray or float The first operand. x1 : np.ndarray or float The second operand. Returns ------- np.ndarray or float The element-wise minimum of x0 and x1. """ return np.minimum(x0, x1)
[docs] def _maximum(x0: ArrayLike, x1: ArrayLike) -> ArrayLike: """ Performs the Maximum operation, equivalent to a boolean OR in fuzzy sets. Parameters ---------- x0 : np.ndarray or float The first operand. x1 : np.ndarray or float The second operand. Returns ------- np.ndarray or float The element-wise maximum of x0 and x1. """ return np.maximum(x0, x1)
[docs] def _diluter(x0: ArrayLike) -> ArrayLike: """ Applies a Diluter operation (square root) to fuzzy set memberships. Typically used to "fuzzify" or expand the meaning of a fuzzy set. Ensures non-negative output for non-negative input. Parameters ---------- x0 : np.ndarray or float The input fuzzy set membership value or array. Returns ------- np.ndarray or float The result of the dilution (square root) operation. """ with np.errstate(divide='ignore', invalid='ignore'): # Ensure that negative values don't result in NaNs from sqrt return np.where(x0 < 0, 0.0, x0**0.5)
[docs] def _diluter_power(x0: ArrayLike, power: float) -> ArrayLike: """ Applies a generalized Diluter operation (x0^power) to fuzzy set memberships. Ensures non-negative output for non-negative input. Parameters ---------- x0 : np.ndarray or float The input fuzzy set membership value or array. power : float The power to raise x0 to (e.g., 1/3 for cube root, 0.25 for fourth root). Returns ------- np.ndarray or float The result of the dilution operation. """ with np.errstate(divide='ignore', invalid='ignore'): return np.where(x0 < 0, 0.0, x0**power)
[docs] def _concentrator(x0: ArrayLike) -> ArrayLike: """ Applies a Concentrator operation (squaring) to fuzzy set memberships. Typically used to "sharpen" or narrow the meaning of a fuzzy set. Parameters ---------- x0 : np.ndarray or float The input fuzzy set membership value or array. Returns ------- np.ndarray or float The result of the concentration (squaring) operation. """ return x0**2
[docs] def _concentrator_power(x0: ArrayLike, power: int) -> ArrayLike: """ Applies a generalized Concentrator operation (x0^power) to fuzzy set memberships. Parameters ---------- x0 : np.ndarray or float The input fuzzy set membership value or array. power : int The integer power to raise x0 to (e.g., 3 for cubing, 4 for power of 4). Returns ------- np.ndarray or float The result of the concentration operation. """ return x0**power
[docs] def _fuzzy_and(a: ArrayLike, b: ArrayLike) -> ArrayLike: """ Calculates the fuzzy AND using the product (a * b) t-norm. Parameters ---------- a : np.ndarray or float The first fuzzy set membership value or array. b : np.ndarray or float The second fuzzy set membership value or array. Returns ------- np.ndarray or float The result of the fuzzy AND operation. """ return a * b
[docs] def _fuzzy_or(a: ArrayLike, b: ArrayLike) -> ArrayLike: """ Calculates the fuzzy OR using the probabilistic sum (a + b - a*b) t-conorm. Parameters ---------- a : np.ndarray or float The first fuzzy set membership value or array. b : np.ndarray or float The second fuzzy set membership value or array. Returns ------- np.ndarray or float The result of the fuzzy OR operation. """ return a + b - a * b
[docs] def _complement(x0: ArrayLike) -> ArrayLike: """ Calculates the fuzzy complement (1.0 - x0). Parameters ---------- x0 : np.ndarray or float The fuzzy set membership value or array. Returns ------- np.ndarray or float The result of the fuzzy complement operation. """ return 1.0 - x0
[docs] def _intensifier(x0: ArrayLike) -> ArrayLike: """ Applies an Intensifier linguistic hedge (from "Expanding the definitions of linguistic hedges"). This operation increases membership values above 0.5 and decreases those below 0.5, making the set "more true". Parameters ---------- x0 : np.ndarray or float The input fuzzy set membership value or array. Returns ------- np.ndarray or float The result of the intensifier operation. """ n = 2 # A common parameter for intensifier with np.errstate(divide='ignore', invalid='ignore'): return np.where( x0 < 0, 0, np.where(x0 < 0.5, 0.5**(1.0-n) * (x0**n), 1.0 - 0.5**(1.0-n) * (1.0 - x0)**n ) )
[docs] def _diffuser(x0: ArrayLike) -> ArrayLike: """ Applies a Diffuser linguistic hedge (from "Expanding the definitions of linguistic hedges"). This operation decreases membership values above 0.5 and increases those below 0.5, making the set "less true" or "fuzzier". Parameters ---------- x0 : np.ndarray or float The input fuzzy set membership value or array. Returns ------- np.ndarray or float The result of the diffuser operation. """ n = 2 # A common parameter for diffuser with np.errstate(divide='ignore', invalid='ignore'): return np.where( x0 < 0, 0, np.where(x0 < 0.5, 0.5**(1.0 - 1.0/n) * x0**(1.0/n), 1.0 - 0.5**(1.0 - 1.0/n) * (1.0 - x0)**(1.0/n) ) )
[docs] def _if_gte(x1: ArrayLike, x2: ArrayLike) -> ArrayLike: """ Returns x1 if x1 >= x2, otherwise returns x2. Parameters ---------- x1 : np.ndarray or float The first operand. x2 : np.ndarray or float The second operand (threshold). Returns ------- np.ndarray or float The result based on the condition. """ return np.where(x1 >= x2, x1, x2)
[docs] def _if_gte_else(x1: ArrayLike, x2: ArrayLike, x3: ArrayLike, x4: ArrayLike) -> ArrayLike: """ Returns x3 if x1 >= x2, otherwise returns x4. Parameters ---------- x1 : np.ndarray or float The comparison value. x2 : np.ndarray or float The threshold value. x3 : np.ndarray or float The value to return if the condition is true. x4 : np.ndarray or float The value to return if the condition is false. Returns ------- np.ndarray or float The result based on the condition. """ return np.where(x1 >= x2, x3, x4)
[docs] def _if_lt(x1: ArrayLike, x2: ArrayLike) -> ArrayLike: """ Returns x1 if x1 < x2, otherwise returns x2. Parameters ---------- x1 : np.ndarray or float The first operand. x2 : np.ndarray or float The second operand (threshold). Returns ------- np.ndarray or float The result based on the condition. """ return np.where(x1 < x2, x1, x2)
[docs] def _if_lt_else(x1: ArrayLike, x2: ArrayLike, x3: ArrayLike, x4: ArrayLike) -> ArrayLike: """ Returns x3 if x1 < x2, otherwise returns x4. Parameters ---------- x1 : np.ndarray or float The comparison value. x2 : np.ndarray or float The threshold value. x3 : np.ndarray or float The value to return if the condition is true. x4 : np.ndarray or float The value to return if the condition is false. Returns ------- np.ndarray or float The result based on the condition. """ return np.where(x1 < x2, x3, x4)
[docs] def _lukasiewicz_t_norm(x0: ArrayLike, x1: ArrayLike) -> ArrayLike: """ Calculates the Łukasiewicz t-norm: max(0, x0 + x1 - 1.0). Parameters ---------- x0 : np.ndarray or float First value, typically in the range [0, 1]. x1 : np.ndarray or float Second value, typically in the range [0, 1]. Returns ------- np.ndarray or float The Łukasiewicz t-norm of x0 and x1. """ return np.maximum(0, x0 + x1 - 1.0)
[docs] def _lukasiewicz_t_conorm(x0: ArrayLike, x1: ArrayLike) -> ArrayLike: """ Calculates the Łukasiewicz t-conorm: min(1, x0 + x1). Parameters ---------- x0 : np.ndarray or float First value, typically in the range [0, 1]. x1 : np.ndarray or float Second value, typically in the range [0, 1]. Returns ------- np.ndarray or float The Łukasiewicz t-conorm of x0 and x1. """ return np.minimum(1.0, x0 + x1)
[docs] def _hamacher_t_norm(x0: ArrayLike, x1: ArrayLike, lambda_param: float) -> ArrayLike: """ Computes the Hamacher T-norm. Parameters ---------- x0 : np.ndarray or float First value, should be in the range [0, 1]. x1 : np.ndarray or float Second value, should be in the range [0, 1]. lambda_param : float Parameter lambda, should be >= 0. Returns ------- np.ndarray or float The Hamacher T-norm of x0 and x1. Raises ------ ValueError If lambda_param is negative. """ # These checks are more robust if the inputs are single values or if # it's critical to halt execution for *any* out-of-range value in an array. # For general fuzzy set operations, often the domain [0,1] is assumed # by design, so explicit checks might be omitted for performance. if lambda_param < 0: raise ValueError("Hamacher t-norm parameter lambda_param must be non-negative.") numerator = x0 * x1 denominator = lambda_param + (1.0 - lambda_param) * (x0 + x1 - x0 * x1) # Handle potential division by zero for denominator == 0 # This specifically addresses the case where lambda_param is 0 and x0+x1-x0*x1 is also 0 with np.errstate(divide='ignore', invalid='ignore'): result = np.where(denominator == 0, 0, numerator / denominator) # Ensure result stays within [0, 1] range if any floating point inaccuracies occur return np.clip(result, 0, 1)
[docs] def _product_t_norm(x0: ArrayLike, x1: ArrayLike) -> ArrayLike: """ Computes the product t-norm (x0 * x1). This is the standard fuzzy AND. Parameters ---------- x0 : np.ndarray or float The first operand. x1 : np.ndarray or float The second operand. Returns ------- np.ndarray or float The product of x0 and x1. """ return x0 * x1
# --- gplearn Function Definitions --- # These operators are wrapped for compatibility with gplearn's genetic programming framework. MAXIMUM = functions.make_function(function=_maximum, name='MAXIMUM/or', arity=2) MINIMUM = functions.make_function(function=_minimum, name='MINIMUM/and', arity=2) COMPLEMENT = functions.make_function(function=_complement, name='COMPLEMENT/not', arity=1) DILUTER = functions.make_function(function=_diluter, name='DILUTER', arity=1) DILUTER3 = functions.make_function(function=lambda x0: _diluter_power(x0, 1/3), name='DILUTER3', arity=1) DILUTER4 = functions.make_function(function=lambda x0: _diluter_power(x0, 0.25), name='DILUTER4', arity=1) CONCENTRATOR = functions.make_function(function=_concentrator, name='CONCENTRATOR', arity=1) CONCENTRATOR3 = functions.make_function(function=lambda x0: _concentrator_power(x0, 3), name='CONCENTRATOR3', arity=1) CONCENTRATOR4 = functions.make_function(function=lambda x0: _concentrator_power(x0, 4), name='CONCENTRATOR4', arity=1) FUZZY_AND = functions.make_function(function=_fuzzy_and, name='FUZZY_AND', arity=2) FUZZY_OR = functions.make_function(function=_fuzzy_or, name='FUZZY_OR', arity=2) INTENSIFIER = functions.make_function(function=_intensifier, name='INTENSIFIER', arity=1) DIFFUSER = functions.make_function(function=_diffuser, name='DIFFUSER', arity=1) IFGTE = functions.make_function(function=_if_gte, name='IFGTE', arity=2) IFGTE2 = functions.make_function(function=_if_gte_else, name='IFGTE2', arity=4) IFLT = functions.make_function(function=_if_lt, name='IFLT', arity=2) IFLT2 = functions.make_function(function=_if_lt_else, name='IFLT2', arity=4) LUKASIEWICZ_AND = functions.make_function(function=_lukasiewicz_t_norm, name='LUKASIEWICZ/AND', arity=2) LUKASIEWICZ_OR = functions.make_function(function=_lukasiewicz_t_conorm, name='LUKASIEWICZ/OR', arity=2) HAMACHER025 = functions.make_function(function=lambda x0, x1: _hamacher_t_norm(x0, x1, 0.25), name='HAMACHER025', arity=2) HAMACHER050 = functions.make_function(function=lambda x0, x1: _hamacher_t_norm(x0, x1, 0.50), name='HAMACHER050', arity=2) PRODUCT = functions.make_function(function=_product_t_norm, name='PRODUCT', arity=2) # --- Dynamic Generation of Fixed-Parameter Averaging Operators --- # This section dynamically creates Weighted Average (WA) and Ordered Weighted Average (OWA) # operators with fixed parameters, significantly reducing code duplication.
[docs] def _generate_wa_operator(param: float): """Generates a gplearn-compatible WA operator with a fixed weight.""" def wa_func(a: ArrayLike, b: ArrayLike) -> ArrayLike: return _weighted_average(a, b, param) wa_func.__name__ = f'_wa_op_param_{param}'.replace('.', '_') return wa_func
[docs] def _generate_owa_operator(param: float): """Generates a gplearn-compatible OWA operator with a fixed weight.""" def owa_func(a: ArrayLike, b: ArrayLike) -> ArrayLike: return _ordered_weighted_average(a, b, param) owa_func.__name__ = f'_owa_op_param_{param}'.replace('.', '_') return owa_func
# Create WA_P1 to WA_P9 for i in range(1, 10): param_val = round(i * 0.1, 1) # Calculate parameter value (0.1, 0.2, ..., 0.9) # Define the name as a string and use globals() to assign the dynamically created function globals()[f'WA_P{i}'] = functions.make_function( function=_generate_wa_operator(param_val), name=f'WA_P{i}', arity=2 ) # Create OWA_P1 to OWA_P9 for i in range(1, 10): param_val = round(i * 0.1, 1) # Calculate parameter value (0.1, 0.2, ..., 0.9) globals()[f'OWA_P{i}'] = functions.make_function( function=_generate_owa_operator(param_val), name=f'OWA_P{i}', arity=2 )