Source code for fuzzy_dl_owl2.fuzzydl.feature_function

import typing

from fuzzy_dl_owl2.fuzzydl.individual.created_individual import CreatedIndividual
from fuzzy_dl_owl2.fuzzydl.individual.individual import Individual
from fuzzy_dl_owl2.fuzzydl.milp.expression import Expression
from fuzzy_dl_owl2.fuzzydl.milp.milp_helper import MILPHelper
from fuzzy_dl_owl2.fuzzydl.milp.term import Term
from fuzzy_dl_owl2.fuzzydl.milp.variable import Variable
from fuzzy_dl_owl2.fuzzydl.relation import Relation
from fuzzy_dl_owl2.fuzzydl.util import constants
from fuzzy_dl_owl2.fuzzydl.util.constants import FeatureFunctionType


[docs] class FeatureFunction: """ This class encapsulates a mathematical expression defined over features, supporting atomic variables, constants, and arithmetic operations such as summation, subtraction, and scalar multiplication. The specific type of function is determined polymorphically at initialization based on the provided arguments—for instance, passing a string defines an atomic feature, a float defines a constant, and a list of functions defines a sum. It serves as an intermediate representation that can be traversed to extract dependencies or converted into a concrete linear programming expression via the `to_expression` method, which resolves atomic features to variables based on an individual's relations. :raises ValueError: Raised if the arguments passed to the constructor do not match any of the valid signatures for defining a feature function, such as having an incorrect number of arguments or incompatible type combinations. """ @typing.overload def __init__(self, feature: typing.Self) -> None: ... @typing.overload def __init__(self, feature: str) -> None: ... @typing.overload def __init__(self, n: float) -> None: ... @typing.overload def __init__(self, feature: list[typing.Self]) -> None: ... @typing.overload def __init__(self, feature1: typing.Self, feature2: typing.Self) -> None: ... @typing.overload def __init__(self, n: float, feature: typing.Self) -> None: ... def __init__(self, *args) -> None: """ Initializes a feature function object by dispatching to specific internal constructors based on the type and count of the provided arguments. The method accepts either a single argument or a pair of arguments. When given a single argument, it may be a string (interpreted as a feature name), a numeric constant, an existing FeatureFunction instance, or a sequence of FeatureFunction instances. When provided with two arguments, the method expects either two FeatureFunction instances or a numeric constant followed by a FeatureFunction instance. If the arguments do not match these specific type signatures or if the argument count is incorrect, a ValueError is raised. :param args: Variable length argument list supporting multiple initialization patterns: a single FeatureFunction, string, number, or sequence of FeatureFunctions; or a pair of two FeatureFunctions or a number and a FeatureFunction. :type args: typing.Any :raises ValueError: Raised when the provided arguments do not match any of the supported initialization signatures. Valid inputs include a single FeatureFunction, string, number, or sequence of FeatureFunctions, or a pair of FeatureFunctions, or a number and a FeatureFunction. """ assert len(args) in [1, 2] if len(args) == 1: if isinstance(args[0], FeatureFunction): self.__feature_function_init_6(*args) elif isinstance(args[0], str): self.__feature_function_init_1(*args) elif isinstance(args[0], constants.NUMBER): self.__feature_function_init_2(*args) elif isinstance(args[0], typing.Sequence) and all( isinstance(a, FeatureFunction) for a in args[0] ): self.__feature_function_init_3(*args) else: raise ValueError elif len(args) == 2: if isinstance(args[0], FeatureFunction) and isinstance( args[1], FeatureFunction ): self.__feature_function_init_4(*args) elif isinstance(args[0], constants.NUMBER) and isinstance( args[1], FeatureFunction ): self.__feature_function_init_5(*args) else: raise ValueError else: raise ValueError def __feature_function_init_1(self, feature: str) -> None: """ Initializes the object to represent an atomic feature function identified by the provided string. This configuration sets the instance's type to ATOMIC, prepares an empty list for potential child functions, assigns the specific feature name, and resets the internal counter to zero. The method directly mutates the instance's state to establish a base-level feature representation, assuming the input string is a valid identifier. :param feature: The name of the atomic feature. :type feature: str """ self.type: FeatureFunctionType = FeatureFunctionType.ATOMIC self.f: list[FeatureFunction] = [] self.feature: str = feature self.n: float = 0.0 def __feature_function_init_2(self, n: float) -> None: """ Initializes the feature function instance to represent a constant numeric value. It sets the type attribute to `NUMBER`, assigns the provided float argument to the `n` attribute, and explicitly clears the list of child functions and the feature name string to ensure a clean state. :param n: The numeric value associated with the feature function. :type n: float """ self.type: FeatureFunctionType = FeatureFunctionType.NUMBER self.f: list[FeatureFunction] = [] self.feature: str = "" self.n: float = n def __feature_function_init_3(self, feature: list[typing.Self]) -> None: """ Configures the instance to represent a summation operation over a provided list of subordinate feature functions. It sets the internal type to `SUM` and stores the input list within the instance for later aggregation. Additionally, this method resets the feature name identifier and the numeric coefficient to their default states of an empty string and 0.0, respectively, ensuring the composite function is initialized without residual values from other potential configurations. :param feature: A list of feature functions to be summed. :type feature: list[typing.Self] """ self.type: FeatureFunctionType = FeatureFunctionType.SUM self.f: list[FeatureFunction] = feature self.feature: str = "" self.n: float = 0.0 def __feature_function_init_4( self, feature1: typing.Self, feature2: typing.Self ) -> None: """ Initializes the instance to represent a subtraction operation between two feature functions. It configures the internal type to `SUBTRACTION` and stores the provided `feature1` and `feature2` as the operands for the calculation. As a side effect, this method explicitly clears the `feature` string attribute and resets the `n` numeric attribute to zero, ensuring the object state is consistent with a binary operation rather than a leaf node. :param feature1: The first operand in the subtraction operation, representing the value from which the second operand is subtracted. :type feature1: typing.Self :param feature2: The feature to subtract from the first feature. :type feature2: typing.Self """ self.type: FeatureFunctionType = FeatureFunctionType.SUBTRACTION self.f: list[FeatureFunction] = [feature1, feature2] self.feature: str = "" self.n: float = 0.0 def __feature_function_init_5(self, n: float, feature: typing.Self) -> None: """ Initializes the instance to represent a product feature function defined by a scalar coefficient and a nested feature. It sets the function type to PRODUCT, assigns the scalar value `n`, and stores the provided `feature` object in a list of sub-features. This operation overwrites existing attributes, specifically resetting the string-based feature identifier to an empty string. :param n: The numeric coefficient or scalar value associated with the feature function. :type n: float :param feature: The feature function instance to be used as a factor in the product operation. :type feature: typing.Self """ self.type: FeatureFunctionType = FeatureFunctionType.PRODUCT self.f: list[FeatureFunction] = [feature] self.feature: str = "" self.n: float = n def __feature_function_init_6(self, feature: typing.Self) -> None: """ Copies the core attributes from an existing `FeatureFunction` instance to the current object. Specifically, it assigns the `type`, `f`, `feature`, and `n` attributes from the provided `feature` argument to `self`. This method performs a shallow copy, meaning mutable attributes like the list `f` will be shared between the two instances rather than deep-copied. :param feature: An existing instance of the same class from which to copy attributes to initialize the current instance. :type feature: typing.Self """ self.type: FeatureFunctionType = feature.type self.f: list[FeatureFunction] = feature.f self.feature: str = feature.feature self.n: float = feature.n
[docs] def get_type(self) -> FeatureFunctionType: """ Retrieves the type classification of the feature function instance. This method returns the value stored in the internal `type` attribute, which typically defines the category or operational nature of the feature function within the broader system. As a simple accessor, this method performs no modifications to the object's state and has no side effects. :return: The type of the feature function. :rtype: FeatureFunctionType """ return self.type
[docs] def get_number(self) -> float: """ Retrieves the numeric value stored within the instance. This method acts as a getter for the internal attribute `n`, returning its current value as a float. The operation is read-only and does not modify the state of the object or any external entities. :return: The floating-point number associated with the instance. :rtype: float """ return self.n
[docs] def get_features(self) -> set[str]: """ Recursively collects and returns a set of feature names that are utilized within the feature function's definition. The method handles different function types by traversing the internal structure: it returns the direct feature name for atomic types, aggregates features from all sub-functions for sums, combines features from the first two sub-functions for subtractions, and extracts features only from the first sub-function for products. This operation is read-only and does not alter the state of the object. :return: A set containing the unique names of all features involved in the function. :rtype: set[str] """ features: set[str] = set() if self.type == FeatureFunctionType.ATOMIC: features.add(self.feature) elif self.type == FeatureFunctionType.PRODUCT: features.update(self.f[0].get_features()) elif self.type == FeatureFunctionType.SUBTRACTION: features.update(self.f[0].get_features()) features.update(self.f[1].get_features()) elif self.type == FeatureFunctionType.SUM: for f in self.f: features.update(f.get_features()) return features
[docs] def to_expression( self, a: Individual, milp: MILPHelper ) -> typing.Optional[Expression]: """ Converts the abstract definition of the feature function into a concrete mathematical expression suitable for a MILP solver, evaluated in the context of a specific individual `a`. Depending on the function's type, this involves retrieving the solver variable associated with a related individual for atomic features, generating constant terms for numbers, or recursively combining the expressions of child functions using arithmetic operations such as summation, subtraction, or scalar multiplication. The method relies on the provided `MILPHelper` to resolve individuals to variables and includes assertions to ensure the structural integrity of the function definition, such as the presence of required relations or a valid number of sub-functions. It returns the resulting `Expression` object, or `None` if the function type is unrecognized. :param a: The individual entity serving as the context for resolving feature relations and constructing the expression. :type a: Individual :param milp: Helper object used to look up variables associated with individuals in the MILP model. :type milp: MILPHelper :return: Returns an Expression object representing the mathematical formulation of the feature function for the given individual, or None if the function type is unsupported. :rtype: typing.Optional[Expression] """ if self.type == FeatureFunctionType.ATOMIC: # Get the filler "b" for feature(a) rel_set: list[Relation] = a.role_relations.get(self.feature) assert len(rel_set) > 0 b: CreatedIndividual = typing.cast( CreatedIndividual, rel_set[0].get_object_individual() ) # Get the variable xB x_b: Variable = milp.get_variable(b) return Expression(Term(1.0, x_b)) elif self.type == FeatureFunctionType.NUMBER: return Expression(self.n) elif self.type == FeatureFunctionType.PRODUCT: assert len(self.f) == 1 ex: Expression = self.f[0].to_expression(a, milp) return ex * self.n elif self.type == FeatureFunctionType.SUBTRACTION: assert len(self.f) == 2 ex1: Expression = self.f[0].to_expression(a, milp) ex2: Expression = self.f[1].to_expression(a, milp) return ex1 - ex2 elif self.type == FeatureFunctionType.SUM: assert len(self.f) >= 1 ex1: Expression = self.f[0].to_expression(a, milp) for i in range(1, len(self.f)): ex2: Expression = self.f[i].to_expression(a, milp) ex1 = ex1 + ex2 return ex1 return None
[docs] def __repr__(self) -> str: """ Returns the official string representation of the object by delegating to the `__str__` method. This ensures that the output is identical to the informal string representation, providing a consistent textual format for debugging and logging. The specific content of the returned string depends entirely on the implementation of the string conversion logic within the class. :return: Returns the string representation of the object, identical to the output of `str()`. :rtype: str """ return str(self)
[docs] def __str__(self) -> str: """ Returns a human-readable string representation of the feature function, formatted according to its specific type. For atomic features, it returns the feature name, while numeric types return the string value of the number. Composite operations are represented as mathematical expressions: products display a scalar multiplied by a sub-feature, subtractions show the difference between two sub-features, and sums join multiple sub-features with plus signs. If the feature function type does not match any known category, an empty string is returned. :return: A string representation of the feature function, formatted as a mathematical expression or feature name based on its type. :rtype: str """ if self.type == FeatureFunctionType.ATOMIC: return self.feature elif self.type == FeatureFunctionType.NUMBER: return str(self.n) elif self.type == FeatureFunctionType.PRODUCT: return f"({self.n} * {self.f[0]})" elif self.type == FeatureFunctionType.SUBTRACTION: return f"({self.f[0]} - {self.f[1]})" elif self.type == FeatureFunctionType.SUM: return f"({' + '.join(map(str, self.f))})" return ""