# Copyright (c) yibocat 2025 All Rights Reserved
# Python: 3.12.7
# Date: 2025/8/18 18:23
# Author: yibow
# Email: yibocat@yeah.net
# Software: AxisFuzzy
"""
Core abstractions for single-element fuzzy-number implementations.
This module defines :class:`FuzznumStrategy`, the primary abstract base
class that concrete fuzzy-number types (``mtype``) must inherit from.
It provides attribute declaration/validation, change callbacks, per-instance
operation caching and a uniform operation dispatch helper that integrates
with the operation registry.
Notes
-----
- Implementations of specific fuzzy types live under ``axisfuzzy/fuzztype/``.
- See examples in ``axisfuzzy/fuzztype/qrofs/qrofn.py`` and
``axisfuzzy/fuzztype/qrohfs/qrohfn.py`` for concrete usages.
"""
import json
from abc import ABC, abstractmethod
from typing import Optional, Set, Dict, Callable, Any, Union, List, TYPE_CHECKING
from .cache import LruCache
from .triangular import OperationTNorm
from ..config import get_config
if TYPE_CHECKING:
from .operation import OperationScheduler
[docs]
class FuzznumStrategy(ABC):
"""
Abstract base for a single-element fuzzy-number strategy.
The strategy represents the data and behavior of a single fuzzy number.
Subclasses declare data attributes (via annotations or class-level defaults)
and implement the presentation and operation-specific logic. Key responsibilities
include:
- Enforce a "declared attribute" contract: assignments to attributes not
declared in the subclass raise ``AttributeError`` (strict mode).
- Provide attribute validators, transformers and change callbacks.
- Offer an operation dispatcher with caching via :meth:`execute_operation`.
- Collect declared attributes at class creation via ``__init_subclass__``.
Parameters
----------
q : int, optional
q-rung parameter used by many fuzzy types. If ``None`` the library
default (from :func:`axisfuzzy.config.get_config`) is used.
Attributes
----------
mtype : str
Registered fuzzy-number type identifier for the concrete strategy class.
q : int or None
Effective q-rung for the instance.
Notes
-----
- Subclasses should call ``super().__init__()`` in their ``__init__``.
- Use :meth:`add_attribute_validator`, :meth:`add_attribute_transformer`
and :meth:`add_change_callback` inside subclass initialization to register
type-specific rules or reactive behavior.
- The default ``q`` validator enforces an integer in [1, 100]; subclasses
may override or refine this by registering a custom validator.
Raises
------
AttributeError
When assigning to an undeclared attribute (strict mode).
ValueError
If a validator rejects a new attribute value.
RuntimeError
If a registered change callback raises an unexpected error.
Examples
--------
Minimal subclass pattern and validators. The following examples mirror
patterns used in the repository:
.. code-block:: python
# Example (simple numeric attributes)
class MyStrategy(FuzznumStrategy):
mtype = 'mytype'
a: float = 0.0
b: float = 1.0
def __init__(self, q=None):
super().__init__(q=q)
# ensure a and b are in [0, 1]
self.add_attribute_validator('a', lambda v: isinstance(v, (int, float)) and 0.0 <= v <= 1.0)
self.add_attribute_validator('b', lambda v: isinstance(v, (int, float)) and 0.0 <= v <= 1.0)
def report(self) -> str:
return f"a={self.a}, b={self.b}"
def str(self) -> str:
return f"<{self.a},{self.b}>"
Concrete examples from the codebase:
- q-rung orthopair fuzzy number (QROFN):
see :mod:`axisfuzzy.fuzztype.qrofs.qrofn` where membership attributes
``md`` and ``nmd`` are registered with validators and change callbacks
that enforce the q-rung constraint :math:`\\mu^q + \\nu^q \\leq 1`
- q-rung hesitant fuzzy number (QROHFN):
see :mod:`axisfuzzy.fuzztype.qrohfs.qrohfn` which demonstrates usage
of attribute transformers to coerce inputs into numpy arrays and
validators that check elementwise ranges and shapes.
Implementation hints
--------------------
- Use transformers to normalise inputs (e.g., convert lists -> np.ndarray)
before validation and storage.
- Register change callbacks when inter-dependent attributes must trigger
cross-checks (e.g., enforcing md/nmd constraints whenever either changes).
- Prefer registering validators/transformers in ``__init__`` so that they
are instance-local and do not unintentionally share state across
instances.
See Also
--------
:class:`axisfuzzy.fuzztype.qrofs.qrofn.QROFNStrategy`
:class:`axisfuzzy.fuzztype.qrohfs.qrohfn.QROHFNStrategy`
"""
mtype: str = get_config().DEFAULT_MTYPE
q: Optional[int] = get_config().DEFAULT_Q
_declared_attributes: List[str] = []
_op_cache: LruCache = LruCache()
_attribute_validators: Dict[str, Callable[[Any], bool]] = {}
_change_callbacks: Dict[str, Callable[[str, Any, Any], None]] = {}
_attribute_transformers: Dict[str, Callable[[Any], Any]] = {}
def __init__(self, q: Optional[int] = None):
"""
Initialize base strategy internals.
Parameters
----------
q : int, optional
q-rung value for this instance. If None, default will be used.
Notes
-----
- Uses object.__setattr__ to avoid recursion through custom __setattr__.
- Registers a basic validator for `q` and performs initial `_validate()`.
"""
# Set 'q' attribute directly using object.__setattr__ to bypass custom __setattr__
# during initialization, preventing potential recursion or validation issues
# before the object is fully set up.
object.__setattr__(self, 'q', q)
# Initialize the operation cache for this instance.
# Using object.__setattr__ ensures that each instance gets its own cache,
# rather than sharing a class-level cache.
object.__setattr__(self, '_op_cache', LruCache(maxsize=get_config().CACHE_SIZE))
# Ensure each instance has its own independent validators and callbacks dictionaries.
# This prevents instances from inadvertently sharing or overwriting each other's
# validation rules or callback functions, which would happen if they were
# initialized as class attributes without this check.
if (not hasattr(self, '_attribute_validators')
or self._attribute_validators is FuzznumStrategy._attribute_validators):
object.__setattr__(self, '_attribute_validators', {})
if (not hasattr(self, '_change_callbacks')
or self._change_callbacks is FuzznumStrategy._change_callbacks):
object.__setattr__(self, '_change_callbacks', {})
if (not hasattr(self, '_attribute_transformers')
or self._attribute_transformers is FuzznumStrategy._attribute_transformers):
object.__setattr__(self, '_attribute_transformers', {})
# Add a validator for the 'q' attribute.
# The 'q' attribute is fundamental to many fuzzy number types (e.g., q-rung orthopair).
# This validator ensures that 'q' is an integer and falls within a reasonable range (1 to 100).
# This validation is applied whenever 'q' is set via __setattr__.
self.add_attribute_validator(
'q', lambda x: isinstance(x, int) and 1 <= x <= 100)
# Perform initial validation of the instance's state.
# This calls the _validate method, which can be overridden by subclasses
# to include specific validation logic for their attributes.
self._validate()
def __init_subclass__(cls, **kwargs):
"""
Hook executed when a subclass is defined.
This collects declared attributes (from annotations and class-level
assignments) and inherits declared attributes from parent strategies.
Parameters
----------
**kwargs
Forwarded to base implementation.
"""
"""Hook for subclasses to declare their own attributes."""
super().__init_subclass__(**kwargs)
# 1. Inherit declared attributes from parent strategies, preserving order.
parent_attrs = []
for base in cls.__bases__:
if hasattr(base, '_declared_attributes'):
for attr in base._declared_attributes:
if attr not in parent_attrs:
parent_attrs.append(attr)
# 2. Collect attributes defined in the current class, preserving order.
# Since Python 3.6, both __annotations__ and __dict__ preserve insertion order.
current_class_attrs = []
# 2a. Add attributes from type annotations
if hasattr(cls, '__annotations__'):
for attr in cls.__annotations__:
if not attr.startswith('_') and attr not in current_class_attrs:
current_class_attrs.append(attr)
# 2b. Add attributes from class-level assignments
for attr_name in cls.__dict__:
if not attr_name.startswith('_') and not callable(getattr(cls, attr_name)):
if attr_name not in current_class_attrs:
current_class_attrs.append(attr_name)
# 3. Combine parent and current class attributes, maintaining order.
# The final list starts with parent attributes, followed by new ones.
combined_attrs = list(parent_attrs)
for attr in current_class_attrs:
if attr not in combined_attrs:
combined_attrs.append(attr)
cls._declared_attributes = combined_attrs
# 4. Initialize other necessary structures for the new subclass
cls._attribute_validators = {}
cls._change_callbacks = {}
def __setattr__(self, name: str, value: Any) -> None:
"""
Set an attribute with validation and optional change callbacks.
The method enforces declared-attribute constraints, runs registered
validators and calls change callbacks after successful assignment.
Parameters
----------
name : str
Attribute name.
value : Any
New value to assign.
Raises
------
AttributeError
If `name` is not declared in `_declared_attributes`.
ValueError
If a registered validator rejects the value or a callback explicitly rejects the change.
RuntimeError
If a change callback raises an unexpected exception.
"""
# Fast path for internal attributes (starting with '_') and the 'mtype' attribute.
# These attributes bypass the custom logic (strict mode, validation, callbacks)
# and are set directly using the parent class's __setattr__ (object's __setattr__).
# This is crucial for internal state management and preventing infinite recursion.
if name.startswith('_') or name == 'mtype':
super().__setattr__(name, value)
return
# Strict attribute mode check.
# If `STRICT_ATTRIBUTE_MODE` is enabled in the global configuration and
# the attribute being set is not found in the `_declared_attributes` set
# (which contains attributes explicitly defined in the class or its subclasses),
# an AttributeError is raised. This prevents accidental creation of new attributes
# and helps maintain a well-defined object schema.
if name not in self._declared_attributes and name != 'q':
raise AttributeError(
f"Attribute '{name}' not declared in {self.__class__.__name__}. "
f"Declared attributes: {self._declared_attributes}"
)
# Get the old value of the attribute before it's changed.
# This old value is passed to change callback functions.
# `getattr(self, name, None)` safely retrieves the current value, returning None
# if the attribute doesn't exist yet.
old_value = getattr(self, name, None)
# Attribute value validation.
# Check if a validator function has been registered for the current attribute name.
# Validators are added via `add_attribute_validator`.
if name in self._attribute_validators:
validator = self._attribute_validators[name]
# If the validator function returns False, it means the new value is invalid,
# and a ValueError is raised, preventing the invalid value from being set.
if not validator(value):
raise ValueError(f"Validation failed for attribute '{name}' with value '{value}'")
# Pre-process or transform the value before setting.
if name in self._attribute_transformers:
transformer = self._attribute_transformers[name]
value = transformer(value)
# Actually set the attribute value.
# After all checks (strict mode, validation) have passed, the attribute's value
# is finally set using the parent class's __setattr__ method.
super().__setattr__(name, value)
# Execute attribute change callbacks.
# Check if any callback functions are registered for the current attribute name.
# Callbacks are added via `add_change_callback`.
if name in self._change_callbacks:
callback = self._change_callbacks[name]
try:
# Call the callback function with the attribute name, old value, and new value.
callback(name, old_value, value)
except ValueError as e:
# If the callback explicitly raises a ValueError, it indicates that the
# change is rejected by the callback's logic. This error is re-raised.
raise ValueError(f"Attribute '{name}' change rejected by callback: {e}") from e
except Exception as e:
# Catch any other unexpected exceptions raised by the callback.
# This indicates a problem within the callback itself, and a RuntimeError is raised.
raise RuntimeError(f"Callback for attribute '{name}' failed, "
f"change has been rolled back.") from e
[docs]
def add_attribute_validator(self,
attr_name: str,
validator: Callable[[Any], bool]) -> None:
"""
Register a validator for a specific attribute.
Validators are primarily used to ensure that attribute values meet
specific conditions or constraints when they are set.
The validator is called whenever the attribute is set via
``__setattr__``. If the validator returns ``False`` the assignment is
rejected and a :class:`ValueError` is raised.
Parameters
----------
attr_name : str
Name of the attribute to validate. Should be one of the names
returned by :meth:`get_declared_attributes` or an instance attribute.
validator : Callable[[Any], bool]
Callable that accepts the candidate value and returns ``True`` if
the value is acceptable, ``False`` otherwise.
Raises
------
TypeError
If ``validator`` is not callable.
Notes
-----
- Validators are stored per-instance (registered inside ``__init__`` of
the concrete strategy). Registering a validator replaces any
previously registered validator for the same attribute.
- Validators are executed before transformers.
Examples
--------
.. code-block:: python
# inside a strategy __init__:
self.add_attribute_validator('md', lambda v: v is None or 0.0 <= float(v) <= 1.0)
"""
self._attribute_validators[attr_name] = validator
[docs]
def add_change_callback(self,
attr_name: str,
callback: Callable[[str, Any, Any], None]) -> None:
"""
Register a post-assignment change callback for an attribute.
The callback is invoked after the attribute value has been successfully
assigned. Its signature must be ``(attr_name, old_value, new_value)``.
Callbacks may raise :class:`ValueError` to reject an assignment, or any
other exception which will be wrapped as :class:`RuntimeError` by
``__setattr__``.
Parameters
----------
attr_name : str
Attribute name to monitor.
callback : Callable[[str, Any, Any], None]
Callable invoked after assignment; may perform cross-attribute checks
or trigger side-effects.
Raises
------
TypeError
If ``callback`` is not callable.
Notes
-----
- Callbacks are executed after validators and transformers and after the
value has been stored on the instance.
- If multiple callbacks for the same attribute are needed, wrap them in
a small dispatcher function or register a single callback that calls
others.
Examples
--------
.. code-block:: python
def on_md_change(name, old, new):
# enforce relationship with other attribute(s)
if new is not None and self.nmd is not None and self.q is not None:
if new**self.q + self.nmd**self.q > 1.0:
raise ValueError("q-rung constraint violated")
self.add_change_callback('md', on_md_change)
"""
self._change_callbacks[attr_name] = callback
def _validate(self) -> None:
"""
Perform internal consistency checks.
This is a protected method intended to be overridden by subclasses to
implement specific validation logic relevant to their fuzzy number type.
It's typically used for complex constraints involving multiple attributes
or for validation that cannot be performed at the individual attribute
setting level.
The default implementation performs a basic check on the `mtype` attribute.
Subclasses should call `super()._validate()` to ensure base class validation
is also performed.
Raises
------
ValueError
If any core validation constraint is violated.
"""
# Check if 'mtype' exists and is a non-empty string.
# This is a fundamental property for all fuzzy number strategies.
if hasattr(self, 'mtype') and (not isinstance(self.mtype, str) or not self.mtype.strip()):
raise ValueError(f"mtype must be a non-empty string, got '{self.mtype}'")
def _generate_cache_key(self,
op_name: str,
operands: Union['FuzznumStrategy', int, float],
tnorm: OperationTNorm) -> Optional[str]:
"""
Generates a unique cache key for an operation.
The key is based on the operation name, the state of the operands (including
the current FuzznumStrategy instance and the other operand), and the
parameters of the t-norm used. This ensures that identical operations
with identical inputs and t-norm settings can retrieve cached results.
Parameters
----------
op_name : str
Operation name (e.g., 'add', 'mul').
operands : FuzznumStrategy or int or float
The second operand.
tnorm : OperationTNorm
T-norm instance describing operator parameters.
Returns
-------
str or None
MD5 hex digest representing the key, or None if generation fails.
"""
def get_state_dict(obj: 'FuzznumStrategy'):
"""Helper function to get a serializable state dictionary of a FuzznumStrategy."""
# Get all explicitly declared attributes of the FuzznumStrategy instance.
attrs = obj.get_declared_attributes()
# Create a dictionary mapping attribute names to their current values.
state = {attr: getattr(obj, attr, None) for attr in attrs}
# Include the 'q' attribute, which is crucial for many fuzzy number types.
state['q'] = obj.q
return state
try:
import hashlib
# Serialize the state of the first operand (self) into a JSON string.
# `sort_keys=True` ensures consistent key order for reproducible hashes.
operand_1 = json.dumps(get_state_dict(self), sort_keys=True)
# Serialize the second operand based on its type.
if isinstance(operands, FuzznumStrategy):
operand_2 = json.dumps(get_state_dict(operands), sort_keys=True)
else:
# For scalar operands (int, float), convert directly to string.
operand_2 = str(operands)
# Get serializable information about the t-norm object.
t_norm_info = json.dumps(tnorm.get_info(), sort_keys=True)
# Combine all parts into a list.
key_parts = [op_name, operand_1, operand_2, t_norm_info]
# Join the parts and encode to bytes for hashing.
key_str = '_'.join(key_parts)
# Compute the MD5 hash and return its hexadecimal representation.
return hashlib.md5(key_str.encode()).hexdigest()
except (TypeError, AttributeError):
# Return None if any part of the key generation fails (e.g., non-serializable data).
return None
[docs]
def execute_operation(self,
op_name: str,
operand: Optional[Union['FuzznumStrategy', int, float]]) -> Dict[str, Any]:
"""
Dispatch and execute a named operation for this strategy instance.
This method is the per-instance entry point to the operation subsystem.
It resolves the appropriate :class:`OperationMixin` implementation from
the global operation registry, builds a t-norm configuration, performs
caching, and finally calls the concrete operation implementation.
Parameters
----------
op_name : str
Operation identifier (for example: ``'add'``, ``'complement'``,
``'gt'``). See :meth:`get_available_operations` for supported names.
operand : FuzznumStrategy or int or float or None
Second operand for binary/comparison operations, scalar for
unary-with-operand operations, or ``None`` for pure unary ops.
Returns
-------
dict
Operation-specific result. Concrete operations define the returned
dictionary structure (commonly includes keys like ``'value'`` or
component arrays).
Raises
------
NotImplementedError
If no operation implementation is registered for this instance's
``mtype`` and the requested ``op_name``.
TypeError
If the provided ``operand`` has an incompatible type for the
requested operation (e.g. scalar where a strategy was expected).
ValueError
If ``op_name`` is unknown or preprocessing fails.
Notes
-----
- Results are cached per-instance when a stable cache key can be created.
- The t-norm configuration used is obtained from the global operation
registry; this method wraps the concrete operation call with timing
and caching logic.
Examples
--------
.. code-block:: python
# binary operation with another strategy instance
res = my_strategy.execute_operation('add', other_strategy)
print(res) # operation-defined dict
# unary complement
res = my_strategy.execute_operation('complement', None)
"""
# Get the global operation registry.
from .operation import get_registry_operation
registry = get_registry_operation()
# Retrieve the specific operation handler for the given operation name and fuzzy number type.
operation = registry.get_operation(op_name, self.mtype)
if operation is None:
# If no operation handler is found, raise an error indicating lack of support.
raise NotImplementedError(
f"Operation '{op_name}' is not supported for mtype '{self.mtype}'."
f" Available operations for '{self.mtype}': {self.get_available_operations()}"
)
# Get the default t-norm configuration from the registry.
norm_type, norm_params = registry.get_default_t_norm_config()
# Create an OperationTNorm instance with the retrieved configuration and the current q-rung.
tnorm = OperationTNorm(norm_type=norm_type, q=self.q, norm_params=norm_params)
# Generate a unique cache key for the current operation.
cache_key = self._generate_cache_key(op_name, operand, tnorm)
# If a valid cache key is generated, attempt to retrieve the result from the cache.
if cache_key:
cached_result = self._op_cache.get(cache_key)
if cached_result is not None:
return cached_result # Return cached result if found.
# Dispatch to the appropriate operation execution method based on the operation name.
if op_name in ['add', 'sub', 'mul', 'div',
'intersection', 'union', 'implication',
'equivalence', 'difference', 'symdiff']:
# These are binary operations requiring another FuzznumStrategy as an operand.
if not isinstance(operand, FuzznumStrategy):
raise TypeError(f"Operands must be a fuzznum strategy, got '{type(operand)}'")
result = operation.execute_binary_op(self, operand, tnorm)
elif op_name in ['pow', 'tim', 'exp', 'log']:
# These are operations with a scalar operand (int or float).
if not isinstance(operand, (int, float)):
raise TypeError(f"Operands must be a number of 'int' or 'float', got '{type(operand)}'")
result = operation.execute_unary_op_operand(self, operand, tnorm)
elif op_name in ['complement']:
# This is a pure unary operation that takes no additional operand.
if operand is not None:
raise TypeError(f"Pure unary operation '{op_name}' takes no additional operands.")
result = operation.execute_unary_op_pure(self, tnorm)
elif op_name in ['gt', 'lt', 'ge', 'le', 'eq', 'ne']:
# These are comparison operations requiring another FuzznumStrategy as an operand.
if not isinstance(operand, FuzznumStrategy):
raise TypeError(f"Comparison operation '{op_name}' requires one FuzznumStrategy operand, "
f"got '{type(operand)}'")
result = operation.execute_comparison_op(self, operand, tnorm)
else:
# If the operation name is not recognized, raise a ValueError.
raise ValueError(f"Unknown operation type: {op_name}")
# If a valid cache key was generated and the operation was successful, cache the result.
if cache_key:
self._op_cache.put(cache_key, result)
return result
[docs]
def get_declared_attributes(self) -> List[str]:
"""
Retrieves a copy of the list of declared attribute names for this strategy.
This method provides introspection capabilities, allowing external code
to discover which attributes are explicitly defined as data members
of a `FuzznumStrategy` instance or its subclasses, in their definition order.
Returns
-------
List[str]
Copy of the declared attributes list, preserving definition order.
Examples
--------
.. code-block:: python
attrs = instance.get_declared_attributes()
# e.g. ['md', 'nmd']
"""
return self._declared_attributes.copy()
[docs]
def get_available_operations(self) -> List[str]:
"""
Gets the names of all operations supported by the current fuzzy number type.
This method queries the global operation registry to determine which
operations are registered and available for the `mtype` of this
`FuzznumStrategy` instance.
Returns
-------
List[str]
A list of strings, where each string is the name of an
operation supported by this fuzzy number type.
Examples
--------
.. code-block:: python
ops = s.get_available_operations()
# ['add', 'complement', 'gt', ...]
"""
from .operation import get_registry_operation
operation = get_registry_operation()
return operation.get_available_ops(self.mtype)
[docs]
def validate_all_attributes(self) -> Dict[str, Any]:
"""Validates all attributes of the FuzznumStrategy instance.
This method runs both the centralized _validate() hook (for
inter-attribute constraints) and any per-attribute validators registered via
:meth:`add_attribute_validator`. It collects failures rather than
raising immediately to provide a consolidated report.
Returns
-------
dict
A dictionary with two keys:
- ``is_valid`` (bool): True if all checks passed.
- ``errors`` (List[str]): Human-readable error messages for failures.
Notes
-----
- Use this method in tests or validation pipelines to obtain a full
diagnostic report without raising exceptions.
- For strict enforcement, call :meth:`_validate` directly and let it
raise exceptions on serious violations.
Examples
--------
.. code-block:: python
report = s.validate_all_attributes()
if not report['is_valid']:
print('\\n'.join(report['errors']))
"""
validation_result = {
'is_valid': True,
'errors': [],
}
# Initialize a dictionary to store the validation results.
# 'is_valid' flag indicates overall validation success, 'errors' list collects all error messages.
try:
# First, call the `_validate()` method.
# `_validate()` is a protected method, typically overridden by subclasses,
# to perform core or complex validation logic specific to that strategy type.
# For example, checking relationships between multiple attributes (e.g., `md + nmd <= 1`).
# If `_validate()` finds a serious issue, it will directly raise an exception (e.g., `ValueError`),
# which will be caught by the outer `try-except` block and recorded as an error.
self._validate()
# Next, iterate through all attributes explicitly declared in the class.
# These attributes are collected and stored in the `_declared_attributes` set
# during `__init_subclass__`.
# This ensures that only data attributes belonging to this strategy model are validated,
# not internal private attributes or methods.
for attr_name in self._declared_attributes:
if hasattr(self, attr_name):
value = getattr(self, attr_name)
# Check if a specific validator has been registered for this attribute.
# These validators are added via the `add_attribute_validator()` method,
# and they are typically used to validate the legality of individual attributes
# (e.g., whether a value is within a certain range, or if it's of a specific type).
if attr_name in self._attribute_validators:
validator = self._attribute_validators[attr_name]
# Call the validator function with the attribute value.
# If the validator function returns `False`, it means the attribute's value
# does not meet the requirements.
if not validator(value):
validation_result['errors'].append(
f"Attribute '{attr_name}' validation failed with value '{value}'"
)
# Mark the overall validation result as False.
validation_result['is_valid'] = False
# Catch any exception that might be raised during the `_validate()` method
# or during individual attribute validation.
# This ensures that the `validate_all_attributes` method itself does not crash
# due to internal validation failures, but instead gracefully returns error information.
except Exception as e:
validation_result['errors'].append(f"Validation error: {e}")
# Add the captured exception message to the `errors` list.
validation_result['is_valid'] = False
# Mark the overall validation status as `False`.
return validation_result
# ======================== Representation & Properties ========================
[docs]
@abstractmethod
def report(self) -> str:
"""
Produce a detailed textual report of the fuzzy number.
Returns
-------
str
Multi-line detailed representation.
Notes
-----
- Must be implemented by concrete strategies.
"""
raise NotImplementedError
[docs]
@abstractmethod
def str(self) -> str:
"""
Produce a concise string representation.
Returns
-------
str
Short one-line representation.
Notes
-----
- Subclasses may override; default can delegate to `report`.
"""
return NotImplemented