Source code for axisfuzzy.random.base

#  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
"""
Abstract base classes for random fuzzy number generation.

This module provides the foundational interfaces and utilities for implementing
random generators for different fuzzy number types (mtypes). The system follows
a plugin architecture where each fuzzy number type can register its own
specialized random generator.

The module defines two main abstract base classes:

1. :class:`BaseRandomGenerator` - Core interface that all generators must implement
2. :class:`ParameterizedRandomGenerator` - Helper class with distribution sampling utilities

Architecture
------------
The random generation system is designed around the following principles:

- **Type Specialization**: Each fuzzy number type (mtype) has its own generator
- **High Performance**: Vectorized batch generation for Fuzzarray creation
- **Flexibility**: Parameterized control over distributions and generation modes
- **Reproducibility**: Integration with NumPy's random number generation system

Random generators are registered globally and can be accessed through the
registry system. Each generator is responsible for creating both individual
Fuzznum instances and batch Fuzzarray instances with optimal performance.

Classes
-------
BaseRandomGenerator
    Abstract interface for all random generators.
ParameterizedRandomGenerator
    Helper base class with distribution sampling utilities.

See Also
--------
axisfuzzy.random.registry : Registration system for random generators
axisfuzzy.random.api : High-level API for random generation
axisfuzzy.fuzztype.qrofn.random : Example implementation for QROFN type

Examples
--------
Implementing a custom random generator:

.. code-block:: python

    from axisfuzzy.random.base import ParameterizedRandomGenerator
    from axisfuzzy.random import register_random

    @register_random
    class CustomRandomGenerator(ParameterizedRandomGenerator):
        mtype = "custom"

        def get_default_parameters(self):
            return {
                'param1': 1.0,
                'param2': 0.5,
                'distribution': 'uniform'
            }

        def validate_parameters(self, **params):
            if 'param1' in params and params['param1'] <= 0:
                raise ValueError("param1 must be positive")

        def fuzznum(self, rng, **params):
            # Implementation for single fuzzy number generation
            merged = self._merge_parameters(**params)
            value = self._sample_from_distribution(
                rng, dist=merged['distribution'],
                low=0, high=merged['param1']
            )
            return Fuzznum(mtype='custom').create(value=value)

        def fuzzarray(self, rng, shape, **params):
            # Implementation for batch generation
            merged = self._merge_parameters(**params)
            # ... batch generation logic
            return Fuzzarray(...)

Using the registered generator:

.. code-block:: python

    import axisfuzzy.random as fr

    # Generate single instance
    num = fr.rand('custom', param1=2.0)

    # Generate batch
    arr = fr.rand('custom', shape=(100,), param2=0.8)
"""

from abc import ABC, abstractmethod
from typing import Dict, Any, Tuple, Union, Optional

import numpy as np

from ..core import Fuzznum, Fuzzarray


[docs] class BaseRandomGenerator(ABC): """ Abstract base class for fuzzy number random generators. This class defines the interface that all random generators must implement to be compatible with the AxisFuzzy random generation system. Each fuzzy number type (mtype) should have a corresponding generator that inherits from this class. The generator is responsible for creating both individual :class:`Fuzznum` instances and batch :class:`Fuzzarray` instances with high performance vectorized operations. Attributes ---------- mtype : str The fuzzy number type identifier that this generator handles. Must be set by concrete implementations. Notes ----- Concrete implementations must set the ``mtype`` class attribute and implement all abstract methods. The generator should be stateless to ensure thread safety and consistent behavior. For high-performance batch generation, implementations should avoid creating intermediate Fuzznum objects and instead populate backend arrays directly. See Also -------- ParameterizedRandomGenerator : Helper base class with distribution utilities axisfuzzy.random.registry.register_random : Decorator for automatic registration Examples -------- Basic generator structure: .. code-block:: python class MyGenerator(BaseRandomGenerator): mtype = "mytype" def get_default_parameters(self): return {'param1': 1.0, 'param2': 0.5} def validate_parameters(self, **params): # Validate parameter values pass def fuzznum(self, rng, **params): # Generate single fuzzy number return Fuzznum(mtype=self.mtype).create(...) def fuzzarray(self, rng, shape, **params): # Generate batch array return Fuzzarray(...) """ mtype: str = 'unknown' """str: The fuzzy number type identifier handled by this generator."""
[docs] @abstractmethod def get_default_parameters(self) -> Dict[str, Any]: """ Get the default parameters for random generation. Returns the default configuration for all parameters that control the random generation process. These defaults can be overridden when calling the generation methods. Returns ------- dict Dictionary mapping parameter names to their default values. The exact parameters depend on the specific fuzzy number type and generation strategy. Notes ----- Default parameters should include all necessary configuration for the generator to function properly. Structural parameters like 'q' (q-rung) are typically not included in defaults as they are usually provided explicitly. Examples -------- .. code-block:: python def get_default_parameters(self): return { 'md_dist': 'uniform', 'md_low': 0.0, 'md_high': 1.0, 'nu_mode': 'orthopair', 'distribution_params': {'a': 2.0, 'b': 2.0} } """ pass
[docs] @abstractmethod def validate_parameters(self, **params) -> None: """ Validate parameters for random generation. Checks that all provided parameters have valid values and that parameter combinations are mathematically consistent. This method should raise appropriate exceptions for invalid configurations. Parameters ---------- **params : dict Parameter values to validate, including both structural parameters (like 'q') and procedural parameters (like distribution settings). Raises ------ ValueError If any parameter value is invalid or if parameter combinations violate mathematical constraints. TypeError If parameters have incorrect types. Notes ----- Validation should be comprehensive but efficient, as it may be called frequently during batch generation. Consider caching validation results for repeated parameter sets. Examples -------- .. code-block:: python def validate_parameters(self, **params): if 'q' in params: q = params['q'] if not isinstance(q, int) or q <= 0: raise ValueError(f"q must be positive integer, got {q}") if 'md_low' in params and 'md_high' in params: if params['md_low'] > params['md_high']: raise ValueError("md_low cannot exceed md_high") """ pass
[docs] @abstractmethod def fuzznum(self, rng: np.random.Generator, **params) -> 'Fuzznum': """ Generate a single random Fuzznum instance. Creates one fuzzy number with the specified parameters using the provided random number generator for reproducible results. Parameters ---------- rng : numpy.random.Generator NumPy random number generator instance for sampling. **params : dict Generation parameters including structural parameters (like 'q') and procedural parameters (like distribution settings). Parameters not provided will use default values. Returns ------- Fuzznum A single fuzzy number instance of the appropriate mtype. Notes ----- This method should be implemented efficiently but the primary performance focus should be on the :meth:`fuzzarray` method for batch generation scenarios. Examples -------- .. code-block:: python def fuzznum(self, rng, **params): merged = self._merge_parameters(**params) self.validate_parameters(**merged) # Generate membership degree md = rng.uniform(merged['md_low'], merged['md_high']) # Generate non-membership degree with constraints max_nmd = (1 - md**merged['q']) ** (1/merged['q']) nmd = rng.uniform(0, max_nmd) return Fuzznum(mtype=self.mtype, q=merged['q']).create( md=md, nmd=nmd ) """ pass
[docs] @abstractmethod def fuzzarray(self, rng: np.random.Generator, shape: Tuple[int, ...], **params) -> 'Fuzzarray': """ Generate a Fuzzarray of random fuzzy numbers. Creates a multi-dimensional array of fuzzy numbers with the specified shape and parameters. This method is designed for high-performance batch generation using vectorized operations. Parameters ---------- rng : numpy.random.Generator NumPy random number generator instance for sampling. shape : tuple of int The desired shape of the output Fuzzarray. **params : dict Generation parameters including structural parameters (like 'q') and procedural parameters (like distribution settings). Returns ------- Fuzzarray Multi-dimensional array of fuzzy numbers with the specified shape. Notes ----- This method should be highly optimized for performance: - Use vectorized NumPy operations for sampling - Avoid creating intermediate Fuzznum objects - Populate backend arrays directly when possible - Handle constraints efficiently using array operations For large arrays, consider memory-efficient generation strategies and avoid operations that scale quadratically with array size. Examples -------- .. code-block:: python def fuzzarray(self, rng, shape, **params): merged = self._merge_parameters(**params) self.validate_parameters(**merged) size = int(np.prod(shape)) # Vectorized generation mds = rng.uniform( merged['md_low'], merged['md_high'], size=size ) # Constraint handling max_nmds = (1 - mds**merged['q']) ** (1/merged['q']) nmds = rng.uniform(0, max_nmds) # Create backend directly backend = MyBackend.from_arrays( mds=mds.reshape(shape), nmds=nmds.reshape(shape), q=merged['q'] ) return Fuzzarray(backend=backend) """ pass
[docs] class ParameterizedRandomGenerator(BaseRandomGenerator, ABC): """ Helper base class for generators using parameterized distributions. This class extends :class:`BaseRandomGenerator` with common utilities for parameter management and statistical distribution sampling. It simplifies the implementation of concrete generators by providing ready-to-use methods for common operations. The class provides: - Parameter merging and default handling - Vectorized sampling from standard distributions - Parameter validation utilities - Efficient distribution parameter management Notes ----- This class is designed for generators that use standard statistical distributions (uniform, beta, normal) for sampling fuzzy number components. Generators with highly specialized sampling logic may inherit directly from :class:`BaseRandomGenerator`. The class maintains cached default parameters for efficiency during repeated generation calls. See Also -------- BaseRandomGenerator : Core interface for all generators Examples -------- Using the parameterized base class: .. code-block:: python @register_random class MyGenerator(ParameterizedRandomGenerator): mtype = "mytype" def get_default_parameters(self): return { 'md_dist': 'uniform', 'md_low': 0.0, 'md_high': 1.0, 'a': 2.0, # Beta parameter 'b': 2.0 # Beta parameter } def fuzznum(self, rng, **params): merged = self._merge_parameters(**params) # Use built-in distribution sampling md = self._sample_from_distribution( rng, dist=merged['md_dist'], low=merged['md_low'], high=merged['md_high'], a=merged['a'], b=merged['b'] ) return Fuzznum(mtype=self.mtype).create(md=md) """ def __init__(self): """ Initialize the parameterized generator. Caches the default parameters for efficient access during generation operations. """ self._default_params = self.get_default_parameters() def _merge_parameters(self, **params) -> Dict[str, Any]: """ Merge user-provided parameters with generator defaults. Combines the default parameters with user-specified overrides, giving priority to user values while ensuring all necessary parameters are present. Parameters ---------- **params : dict User-provided parameter overrides. Returns ------- dict Complete parameter dictionary with user overrides applied to default values. Examples -------- .. code-block:: python # With defaults: {'md_low': 0.0, 'md_high': 1.0, 'dist': 'uniform'} merged = self._merge_parameters(md_high=0.8, dist='beta') # Result: {'md_low': 0.0, 'md_high': 0.8, 'dist': 'beta'} """ # FIX: Create a copy of defaults, then update with user params. merged_params = self._default_params.copy() merged_params.update(params) return merged_params def _validate_range(self, name: str, value: float, min_val: float, max_val: float): """ Validate that a numeric parameter is within a specified range. Convenience method for common range validation operations in parameter validation routines. Parameters ---------- name : str Parameter name for error messages. value : float Value to validate. min_val : float Minimum allowed value (inclusive). max_val : float Maximum allowed value (inclusive). Raises ------ ValueError If value is outside the specified range. Examples -------- .. code-block:: python def validate_parameters(self, **params): if 'md_low' in params: self._validate_range('md_low', params['md_low'], 0.0, 1.0) if 'sigma' in params: self._validate_range('sigma', params['sigma'], 0.001, 10.0) """ if not (min_val <= value <= max_val): raise ValueError(f"Parameter '{name}' must be between " f"{min_val} and {max_val}, but got {value}.") def _sample_from_distribution( self, rng: np.random.Generator, size: Optional[int] = None, dist: str = 'uniform', low: float = 0.0, high: float = 1.0, **dist_params ) -> Union[float, np.ndarray]: """ Sample values from a specified distribution with range clipping. Provides a unified interface for sampling from common statistical distributions with automatic range normalization. This method is optimized for vectorized operations and supports both scalar and array generation. Parameters ---------- rng : numpy.random.Generator NumPy random number generator instance. size : int, optional Number of samples to generate. If None, returns a single float. dist : str, default 'uniform' Distribution name. Supported values: - 'uniform': Uniform distribution over [low, high] - 'beta': Beta distribution scaled to [low, high] - 'normal': Normal distribution clipped to [low, high] low : float, default 0.0 Lower bound of output range (inclusive). high : float, default 1.0 Upper bound of output range (inclusive). **dist_params : dict Distribution-specific parameters: - For 'beta': 'a' and 'b' (shape parameters) - For 'normal': 'loc' (mean) and 'scale' (standard deviation) Returns ------- float or numpy.ndarray Random sample(s) within the specified range. Returns float if size is None, otherwise returns ndarray. Raises ------ ValueError If low > high or if an unsupported distribution is specified. Notes ----- Distribution scaling and clipping behavior: - **Uniform**: Direct sampling within [low, high] - **Beta**: Samples from Beta(a,b) then linearly scaled to [low, high] - **Normal**: Samples from Normal(loc, scale) then clipped to [low, high] For 'normal' distribution, default loc is (low+high)/2 and default scale is (high-low)/6 to provide reasonable coverage. Examples -------- Single value sampling: .. code-block:: python # Uniform sampling val = self._sample_from_distribution(rng, dist='uniform', low=0.2, high=0.8) # Beta distribution sampling val = self._sample_from_distribution( rng, dist='beta', low=0, high=1, a=2.0, b=5.0 ) # Normal distribution sampling val = self._sample_from_distribution( rng, dist='normal', low=0, high=1, loc=0.5, scale=0.15 ) Vectorized sampling: .. code-block:: python # Generate 1000 samples from beta distribution samples = self._sample_from_distribution( rng, size=1000, dist='beta', low=0.1, high=0.9, a=3.0, b=2.0 ) # Generate array for backend initialization size = int(np.prod(shape)) values = self._sample_from_distribution( rng, size=size, dist='uniform', low=0, high=1 ).reshape(shape) """ if low > high: raise ValueError(f"low ({low}) cannot be greater than high ({high}).") if dist == 'uniform': return rng.uniform(low, high, size) elif dist == 'beta': a = dist_params.get('a', 2.0) b = dist_params.get('b', 2.0) samples = rng.beta(a, b, size) return low + samples * (high - low) elif dist == 'normal': loc = dist_params.get('loc', (low + high) / 2) scale = dist_params.get('scale', (high - low) / 6) samples = rng.normal(loc, scale, size) return np.clip(samples, low, high) else: raise ValueError(f"Unsupported distribution: '{dist}'. " f"Available distributions are 'uniform', 'beta', 'normal'.")