Random Generation System

The random generation system in axisfuzzy is a cornerstone for simulation, analysis, and testing in fuzzy logic applications. It provides a powerful and flexible framework for creating stochastic fuzzy numbers, which is essential for modeling uncertainty and variability in real-world systems. Whether you are performing Monte Carlo simulations, generating synthetic data for machine learning models, or testing the robustness of your fuzzy algorithms, this system offers the tools you need for reproducible and statistically sound random generation.

This guide will walk you through the architecture and usage of the random generation system, covering everything from basic high-level APIs to the underlying mechanics of reproducibility and extensibility. You will learn how to:

  • Ensure reproducibility of your random experiments using a sophisticated seeding mechanism.

  • Leverage the high-level API to effortlessly generate various types of random fuzzy numbers.

  • Understand the core components of the system, including the generator blueprint and the discovery mechanism.

  • Extend the system by creating and registering your own custom random generators.

By the end of this guide, you will have a comprehensive understanding of how to harness the full potential of axisfuzzy’s random generation capabilities.

Architectural Deep Dive

The axisfuzzy.random system is built upon a philosophy of modularity, extensibility, and reproducibility. Its architecture is composed of four core components that work in concert to provide a powerful yet easy-to-use framework for random fuzzy number generation. Understanding this architecture is key to leveraging the system’s full potential and extending it with custom functionality.

Design Philosophy

  • Modularity & Extensibility: The system is designed as a “plug-in” architecture. New random generators for different fuzzy membership types (mtype) can be defined and registered without altering any core code. This ensures that the system can easily adapt to future fuzzy number representations.

  • Reproducibility: Scientific computing demands reproducible results. A centralized seed management system ensures that the same seed will always produce the exact same sequence of random fuzzy numbers, which is critical for debugging, validation, and sharing experiments.

  • Unified API: Despite the complexity of the underlying implementations for different fuzzy types, the system exposes a simple and consistent high-level API (e.g., axisfuzzy.random.rand()). Users can generate any supported fuzzy type through the same interface, significantly lowering the barrier to entry.

  • High Performance: For large-scale simulations, performance is paramount. The system leverages vectorized operations and interacts directly with the Struct of Arrays (SoA) backend of Fuzzarray, avoiding inefficient Python-level loops and achieving performance close to that of NumPy.

Core Components

The system’s functionality is divided among four key modules:

┌─────────────────────────────────────────────────────────────────┐
│                        User Layer (e.g., your script)           │
├─────────────────────────────────────────────────────────────────┤
│  fr.rand()  │ fr.choice() │ fr.uniform() │ fr.set_seed() │ ...  │
└─────────────┬───────────────────────────────────────────────────┘
              │
┌─────────────▼───────────────────────────────────────────────────┐
│                       API Layer (api.py)                        │
│  • Unified entry points (rand, choice, etc.)                    │
│  • Parameter parsing and validation                             │
│  • Dispatches to the appropriate generator                      │
└─────────────┬───────────────────────────────────────────────────┘
              │
    ┌─────────▼─────────┐           ┌──────────────────────┐
    │   Registry Layer  │           │   Seed Management    │
    │  (registry.py)    │           │      (seed.py)       │
    │                   │           │                      │
    │• Manages generator│◄─────────►│• Global random state │
    │  registration     │           │• RNG instantiation   │
    │• Maps `mtype` to  │           │• Spawns independent  │
    │  generator        │           │  streams for parallel│
    └─────────┬─────────┘           └──────────────────────┘
              │                               │
┌─────────────▼───────────────────────────────▼───────────────────┐
│                  Generator Layer (base.py)                      │
│                                                                 │
│  BaseRandomGenerator          ParameterizedRandomGenerator      │
│  ├─ Defines the common        ├─ _merge_parameters()            │
│  │  interface for all         ├─ _sample_from_distribution()    │
│  │  generators                ├─ _validate_range()              │
│  └─ (fuzznum, fuzzarray)      └─ ... and other utilities        │
└─────────────┬───────────────────────────────────────────────────┘
              │
┌─────────────▼───────────────────────────────────────────────────┐
│               Concrete Implementation Layer                     │
│                                                                 │
│  QROFNRandomGenerator    │  IVFNRandomGenerator    │  ...       │
│  ├─ mtype = "qrofn"      │  ├─ mtype = "ivfn"      │            │
│  ├─ Handles specific     │  ├─ Implements interval-│            │
│  │  constraints          │  │  based logic         │            │
└─────────────────────────────────────────────────────────────────┘
  1. The API Layer (api.py): This is the primary user-facing entry point. It provides the high-level functions like rand() and choice(). Its role is to interpret the user’s request (e.g., mtype, shape, and other parameters) and coordinate the other components to fulfill it.

  2. The Registry Layer (registry.py): This acts as the system’s “dispatch center.” It maintains a mapping from an mtype string (like 'qrofn') to a specific generator instance. When you request a random number of a certain type, the registry looks up and provides the correct generator to the API layer. The @register_random decorator allows new generators to be added to this registry automatically.

  3. The Generator Blueprint (base.py): This module defines the “abstract blueprint” that all random generators must follow. The BaseRandomGenerator class establishes a common interface, requiring every generator to implement methods for creating both single fuzzy numbers (fuzznum()) and batches of them (fuzzarray()). It also provides the ParameterizedRandomGenerator subclass, which includes helpful utilities for parameter management and sampling from standard distributions, simplifying the development of new generators.

  4. The Seed Management Layer (seed.py): This component is the guardian of reproducibility. It manages a global numpy.random.Generator instance that all random generation tasks draw from. By setting a global seed with set_seed(), you fix the starting point of this random number generator, guaranteeing that every subsequent run of your code will produce identical results. It also supports creating independent random streams for advanced use cases like parallel computing.

A Call’s Lifecycle

To see how these components collaborate, let’s trace a typical call:

import axisfuzzy.random as fr

# User requests 100 q-Rung Orthopair Fuzzy Numbers
arr = fr.rand('qrofn', shape=(100,), q=3)
  1. API Layer: The rand() function in api.py receives the request with mtype='qrofn' and shape=(100,).

  2. Registry Lookup: The API layer asks the registry.py to find the generator for 'qrofn'. The registry returns an instance of the QROFNRandomGenerator.

  3. RNG Provision: The API layer retrieves the global random number generator (RNG) instance from seed.py to ensure the operation is part of the reproducible sequence.

  4. Generator Execution: Since a shape is specified, the API layer calls the high-performance fuzzarray() method on the QROFNRandomGenerator instance. This method uses the provided RNG to perform vectorized sampling, efficiently generates the membership and non-membership degrees according to the q-ROFN constraints, and directly constructs a Fuzzarray object for maximum performance.

The Generator Blueprint (base.py)

At the core of the axisfuzzy.random system’s extensibility is the “generator blueprint” defined in base.py. This module establishes a clear and consistent contract that all random generators must follow, ensuring that new fuzzy number types can be seamlessly integrated into the high-level API. It provides two key abstract base classes: BaseRandomGenerator and ParameterizedRandomGenerator.

BaseRandomGenerator: The Fundamental Contract

The BaseRandomGenerator is an abstract base class (ABC) that defines the fundamental interface for any random generator. It cannot be instantiated directly. Instead, its purpose is to enforce a “contract” that guarantees every generator, regardless of the fuzzy number type it produces, will have a consistent structure and set of capabilities.

Any class that inherits from BaseRandomGenerator must implement the following four components:

  1. mtype Attribute A class attribute that declares the fuzzy number type (e.g., 'qrofn', 'ivfn') this generator is responsible for. The registry system uses this string to map a user’s request to the correct generator.

    class QROFNRandomGenerator(BaseRandomGenerator):
        mtype = 'qrofn'
    
  2. get_default_parameters() Method This method must return a dictionary containing the default parameters for the generation process. These values are used when the user does not explicitly provide them, ensuring predictable behavior.

    def get_default_parameters(self) -> Dict[str, Any]:
        return {
            'md_dist': 'uniform',
            'md_low': 0.0,
            'md_high': 1.0,
            'nu_mode': 'orthopair'
        }
    
  3. validate_parameters(**params) Method Before generation, the system calls this method to validate the user-provided and default parameters. It should raise a ValueError or TypeError if any parameter is invalid, preventing errors during the generation process. For example, it might check that low is not greater than high.

  4. fuzznum(rng, **params) and fuzzarray(rng, shape, **params) Methods These are the core generation methods. The distinction is critical for performance:

    • fuzznum(): Generates a single Fuzznum instance. Its implementation is typically straightforward and easy to debug.

    • fuzzarray(): Generates a batch of fuzzy numbers as a Fuzzarray of a given shape. This method is designed for high performance and must be implemented using vectorized operations (e.g., with NumPy) to avoid slow Python loops.

This dual-method design allows for both simple, readable logic for single instances and highly optimized, scalable logic for large-scale simulations.

ParameterizedRandomGenerator: The Developer’s Toolkit

While BaseRandomGenerator defines the contract, implementing every generator from scratch would involve repetitive boilerplate code for common tasks like merging parameters or sampling from statistical distributions.

The ParameterizedRandomGenerator is a helper class that inherits from BaseRandomGenerator and provides a powerful toolkit to solve these common problems. It is also an abstract class but comes with pre-built utility methods. Developers are strongly encouraged to inherit from it whenever the generation logic is based on sampling from standard statistical distributions.

Key utility methods provided by ParameterizedRandomGenerator:

  1. _merge_parameters(**params) Automatically merges the user-provided parameters with the default parameters returned by get_default_parameters(). User parameters always take precedence. This simplifies parameter management significantly.

  2. _validate_range(name, value, min_val, max_val) A convenient helper to check if a given parameter value falls within a specified [min_val, max_val] range, raising a ValueError if it doesn’t.

  3. _sample_from_distribution(...) This is the most powerful utility. It provides a unified interface to sample from various statistical distributions and automatically scales the output to a desired [low, high] range. Supported distributions include:

    • 'uniform': Uniform distribution.

    • 'beta': Beta distribution (requires a and b shape parameters).

    • 'normal': Normal distribution (requires loc and scale parameters). The output is clipped to the [low, high] range to prevent out-of-bounds values.

    This method abstracts away the complexities of different NumPy sampling functions, allowing developers to focus on the generation logic itself.

Choosing the Right Base Class

  • Inherit from :class:`BaseRandomGenerator` directly if your generation logic is highly specialized, does not rely on standard statistical distributions, or requires complete control over every step of the process.

  • Inherit from :class:`ParameterizedRandomGenerator` (the recommended approach for most cases) if your generation logic involves sampling values from uniform, beta, or normal distributions to construct the fuzzy number’s components. This dramatically reduces development time and ensures consistency.

Reproducibility and State Management (seed.py)

In scientific computing and machine learning, reproducibility is a fundamental requirement. Whether you are validating an algorithm, debugging code, or comparing experimental results, the ability to control random processes is essential. The seed.py module is designed to solve this problem by providing a robust and centralized system for managing the random state across the entire axisfuzzy library.

The Core Concept: GlobalRandomState

At the heart of the seed.py module is the GlobalRandomState class, which operates as a thread-safe singleton. This design ensures that:

  • Single Source of Truth: The entire library draws random numbers from a single, globally managed numpy.random.Generator instance.

  • Controllability: You can set a seed at any point to make subsequent random operations deterministic.

  • Thread Safety: A built-in threading.Lock protects the internal state, making it safe to use in multi-threaded applications.

This centralized approach prevents inconsistencies and makes it easy to track and control the source of randomness in your applications.

The GlobalRandomState Class

While users typically interact with the random state through the high-level functions described below (like set_seed()), the GlobalRandomState class is the engine that powers the entire system. It is implemented as a singleton, meaning a single instance of this class manages the random state for the entire application lifecycle.

This class encapsulates a numpy.random.Generator instance and provides thread-safe methods to manage it. Its primary responsibilities are:

  • Initialization: When first instantiated, it initializes a Generator with entropy from the operating system, ensuring that initial random numbers are unpredictable until a seed is explicitly set.

  • Seed Management: It handles the logic for setting and retrieving the seed, and re-creating the generator when the seed is changed via set_seed().

  • Generator Access: It provides methods to get the global generator (get_generator()) or spawn new, independent generators (spawn_generator()).

The class is designed for internal use within the axisfuzzy library, but understanding its structure can be helpful for advanced debugging or extension. The key methods of the class are wrapped and exposed by the module-level functions for ease of use.

Core Functions for Random State Management

The module exposes a simple yet powerful API for interacting with the global random state.

set_seed()

This is the most crucial function for ensuring reproducibility. By calling set_seed() with a specific integer, you initialize the global random number generator to a known state. Every time you run your script with the same seed, you will get the exact same sequence of random numbers.

import axisfuzzy.random as fr

# Set the global seed to ensure reproducibility
fr.set_seed(42)

# All subsequent random generations are now deterministic
num1 = fr.rand('qrofn', q=2)
arr1 = fr.rand('ivfn', shape=(5,))

# Resetting to the same seed will produce the exact same results
fr.set_seed(42)
num2 = fr.rand('qrofn', q=2)  # num2 is identical to num1
arr2 = fr.rand('ivfn', shape=(5,))  # arr2 is identical to arr1

assert num1 == num2
assert (arr1 == arr2).all()

get_rng()

For more advanced use cases where you need to perform custom random operations, get_rng() provides direct access to the global numpy.random.Generator instance. This is useful when you need to integrate with other NumPy-based libraries while maintaining a consistent random state.

Note

Using the generator returned by get_rng() will advance the global random state, affecting all subsequent random operations within axisfuzzy.

import axisfuzzy.random as fr
import numpy as np

# Set the global seed to ensure reproducibility
fr.set_seed(123)

# Get the global random number generator
rng = fr.get_rng()

# Use it for custom random sampling
# This advances the global state
custom_noise = rng.normal(loc=0, scale=0.1, size=10)

# The next call to fr.rand() will use the advanced state
fuzzy_num = fr.rand('qrofn', q=2)

spawn_rng()

This powerful function allows you to create statistically independent random number generators derived from the global state. It is essential for parallel computing scenarios where you need to ensure that multiple processes or threads are generating random numbers without interfering with each other. Each spawned generator manages its own independent state.

import axisfuzzy.random as fr
from concurrent.futures import ThreadPoolExecutor

fr.set_seed(456)

def worker_task(worker_id):
    # Each worker gets its own independent random stream
    worker_rng = fr.spawn_rng()
    # Generate data using the independent generator
    # This does NOT affect the global state or other workers
    data = worker_rng.uniform(0, 1, size=5)
    return f"Worker {worker_id} data: {data.round(2)}"

# Execute tasks in parallel
with ThreadPoolExecutor(max_workers=2) as executor:
    for result in executor.map(worker_task, range(2)):
        print(result)

# The global state remains unaffected by the worker tasks
global_num = fr.rand('qrofn', q=2)
print(f"Global random number after parallel tasks: {global_num}")

get_seed()

This utility function allows you to retrieve the seed that was last used to initialize the global random state. This is useful for logging, debugging, or storing the configuration of an experiment for later replication.

import axisfuzzy.random as fr

fr.set_seed(789)
current_seed = fr.get_seed()
print(f"Experiment started with seed: {current_seed}")

# ... run experiment ...
results = fr.rand('qrofn', shape=(10,), q=2)

print(f"Experiment finished. Seed was: {fr.get_seed()}")

The Discovery Mechanism (registry.py)

The axisfuzzy.random system is designed to be extensible, allowing developers to add support for new fuzzy number types without modifying the core library. This “plug-in” architecture is powered by the discovery and registration mechanism in registry.py. It acts as a central directory, or “phone book,” that maps each fuzzy number type (mtype) to its corresponding random generator.

This system ensures that when you call fr.rand('my_new_type'), the library automatically knows where to find and how to use the generator for 'my_new_type'.

The Core: RandomGeneratorRegistry

The RandomGeneratorRegistry is a thread-safe singleton class that serves as the heart of the discovery mechanism. It maintains an internal dictionary mapping mtype strings to generator instances. Its key responsibilities are:

  • Centralized Management: Provides a single, global source of truth for all available random generators.

  • Automatic Discovery: Works with the @register_random decorator to automatically find and register new generators when they are imported.

  • Dynamic Dispatch: Enables the high-level API to look up and retrieve the correct generator instance at runtime based on the user’s request.

Seamless Registration with @register_random

The easiest and recommended way to add a new generator to the system is by using the @register_random class decorator. When you apply this decorator to a generator class, it automatically performs the following steps at module import time:

  1. Reads the mtype class attribute to identify which fuzzy type the generator handles.

  2. Creates an instance of the generator class.

  3. Registers this instance with the global RandomGeneratorRegistry.

This process is completely transparent and requires no manual intervention.

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

# The decorator automatically registers this class upon import
@register_random
class MyNewTypeGenerator(ParameterizedRandomGenerator):
    # The mtype that this generator is responsible for
    mtype = "my_new_type"

    # --- Implement the required methods ---
    def get_default_parameters(self):
        return {'param': 0.5}

    def validate_parameters(self, **params):
        pass

    def fuzznum(self, rng, **params):
        # ... logic to generate a single fuzzy number
        pass

    def fuzzarray(self, rng, shape, **params):
        # ... logic to generate an array of fuzzy numbers
        pass

Once this module is imported anywhere in your project, the 'my_new_type' generator becomes immediately available through the high-level API.

Querying and Using Generators

The registry.py module also provides a set of user-friendly global functions to interact with the registry.

get_random_generator()

This function is the primary way to retrieve a generator instance for a specific mtype. The high-level API uses it internally to dispatch requests.

from axisfuzzy.random.registry import get_random_generator

# Retrieve the generator for q-Rung Orthopair Fuzzy Numbers
qrofn_generator = get_random_generator('qrofn')

if qrofn_generator:
    # You can now use the generator's methods directly
    print(qrofn_generator.get_default_parameters())

list_registered_random() & is_registered_random() & get_registry_random()

To discover which generators are available or to check for the existence of a specific one, you can use these utility functions.

from axisfuzzy.random.registry import list_registered_random, is_registered_random, get_registry_random

# List all available random generators
available_generators = list_registered_random()
print(f"Available generators: {available_generators}")
# Expected output might include: ['qrofn', 'ivfn', 'qrohfn', ...]

# Check if a specific generator is registered
if is_registered_random('qrofn'):
    print("The 'qrofn' generator is ready to use.")
else:
    print("The 'qrofn' generator is not available.")

# Get the global registry instance for advanced operations
registry = get_registry_random()
print(f"Registry contains {len(registry._generators)} generators")

This registration and discovery system makes axisfuzzy highly modular and easy to extend, encouraging community contributions and custom adaptations without compromising the stability of the core framework.

The High-Level API (api.py)

The api.py module serves as the primary user-facing interface for all random generation tasks in axisfuzzy. It abstracts the underlying complexity of seed management, generator registration, and type-specific logic into a set of powerful yet intuitive functions. This is the recommended entry point for most users.

The architecture places this API at the top, providing a unified gateway to the entire random generation subsystem.

Key Functions:

  • rand: The main factory function for creating random Fuzznum and Fuzzarray instances.

  • choice: A function for random sampling from an existing Fuzzarray, similar to NumPy’s equivalent.

The Primary Factory: rand()

The rand() function is the cornerstone of the high-level API. It is a versatile factory that can generate both single fuzzy numbers and large, multi-dimensional fuzzy arrays with high performance.

rand(
    mtype: Optional[str] = None,
    q: int = 1,
    shape: Optional[Union[int, Tuple[int, ...]]] = None,
    rng: Optional[np.random.Generator] = None,
    **params
) -> Union[Fuzznum, Fuzzarray]:

Core Parameters:

  • mtype: The type of fuzzy number to generate (e.g., 'qrofn', 'ivfn'). If omitted, the library uses the default mtype from the global configuration.

  • q: The q-rung parameter, a structural property for types like q-Rung Orthopair Fuzzy Numbers.

  • shape: Defines the output dimensions. If None (default), it returns a single Fuzznum. If an int or tuple, it returns a Fuzzarray of the specified shape.

  • seed & rng: Control reproducibility. See the note on randomness control below.

  • **params: Keyword arguments passed directly to the type-specific generator. These control the statistical properties of the generated numbers, such as the distribution (md_dist) or range (md_low, md_high).

Randomness Control Priority

The API provides a flexible, two-tiered system for managing random state:

  1. `rng` (Highest Priority): Pass an existing numpy.random.Generator instance for full control. All randomness will be drawn from this generator.

  2. Global State (Lowest Priority): If rng is not provided, the function uses the library’s global generator, which is managed by fr.set_seed().

Reproducible Sampling:

# Reproducible sampling with seed
fr.set_seed(123)
sample1 = fr.choice(population, size=5)
fr.set_seed(123)
sample2 = fr.choice(population, size=5)  # Identical to sample1

Examples

Basic Generation:

import axisfuzzy.random as fr

# Generate a single q-Rung Orthopair Fuzzy Number (q=2)
num = fr.rand('qrofn', q=2)

# Generate a 1D array of 50 Interval-Valued Fuzzy Numbers
arr = fr.rand('ivfn', shape=50)

# Generate a 10x20 2D array
arr_2d = fr.rand('qrofn', q=3, shape=(10, 20))

Advanced Parameter Control:

This example generates 1,000 fuzzy numbers where the membership degree follows a Beta distribution.

arr_beta = fr.rand(
    'qrofn',
    q=4,
    shape=(1000,),
    md_dist='beta',       # Membership degree from Beta distribution
    md_low=0.2,           # Clipped to a minimum of 0.2
    md_high=0.8,          # Clipped to a maximum of 0.8
    a=2.0,                # Beta distribution shape parameter `a`
    b=5.0,                # Beta distribution shape parameter `b`
    nu_mode='orthopair'   # Non-membership calculated to satisfy constraint
)

Random Sampling with choice()

The choice() function allows you to draw random samples from an existing 1-dimensional Fuzzarray. It supports sampling with or without replacement and allows for weighted probabilities, mirroring the functionality of numpy.random.choice.

choice(
    obj: Fuzzarray,
    size: Optional[Union[int, Tuple[int, ...]]] = None,
    replace: bool = True,
    p: Optional[Sequence[float]] = None,
    rng: Optional[np.random.Generator] = None
) -> Union[Any, Fuzzarray]:

Core Parameters:

  • obj: The 1D Fuzzarray to sample from.

  • size: The desired shape of the output. If None, returns a single Fuzznum.

  • replace: If True (default), samples are drawn with replacement. If False, they are drawn without replacement.

  • p: An array-like sequence of probabilities associated with each element in obj.

Examples

Basic Sampling:

import axisfuzzy.random as fr

# 1. Create a source array
source_arr = fr.rand('qrofn', q=2, shape=(1000,))

# 2. Sample a single element
single_sample = fr.choice(source_arr)

# 3. Sample 50 elements with replacement
samples_with_replacement = fr.choice(source_arr, size=50)

# 4. Sample 50 unique elements without replacement
samples_without_replacement = fr.choice(source_arr, size=50, replace=False)

Weighted Sampling:

This is useful when certain fuzzy numbers in the source array are more important than others.

import numpy as np

# Assume we have a source array of 100 elements
source_arr = fr.rand('qrofn', q=2, shape=(100,))

# Create random weights and normalize them to sum to 1
weights = np.random.rand(100)
weights /= weights.sum()

# Draw a weighted sample of 20 elements
weighted_sample = fr.choice(source_arr, size=20, p=weights)

Utility Functions

The API also includes helper functions like uniform(), normal(), and beta(), which are convenient wrappers around NumPy’s random generation functions. They are integrated with AxisFuzzy’s seed management system, making them useful for generating auxiliary numerical data for your fuzzy logic models.

Additionally, the API provides several utility functions for advanced random generation scenarios:

  • ``get_rng()``: Returns the global random number generator instance

  • ``set_seed()``: Sets the global random seed for reproducible generation

  • ``spawn_rng()``: Creates an independent random generator for parallel operations

  • ``get_seed()``: Retrieves the current global seed value

Note

Currently, these utility functions return standard floating-point numbers (float or numpy.ndarray). In the future, they may be enhanced to generate fuzzy numbers that follow these distributions directly.

Extending the System

The axisfuzzy.random framework is engineered for extensibility. Its plug-in architecture allows you to add custom random generators for new or existing fuzzy number types without modifying the core library. This section provides a comprehensive, end-to-end tutorial on how to create, register, and use a new random generator.

As a practical case study, we will develop an alternative random generator for the existing qrofn (q-Rung Orthopair Fuzzy Number) type. This new generator will have a different default behavior, demonstrating how you can tailor generation logic to specific experimental needs.

The Three-Step Process

Adding a new generator follows a consistent, three-step pattern that leverages the concepts from base.py and registry.py:

  1. Implement the Generator Class: Create a Python class that inherits from a suitable base class, typically ParameterizedRandomGenerator. This class will contain all the logic for generating your fuzzy numbers.

  2. Register the Generator: Apply the @register_random decorator to your class. This automatically makes the generator available to the high-level API (e.g., fr.rand()) as soon as the module containing it is imported.

  3. Ensure Module Import: Make sure the Python module containing your new generator is imported at an appropriate place in your project, so the registration process is triggered.

End-to-End Example: An Alternative Generator for qrofn

Let’s imagine we need a qrofn generator that, by default, produces numbers with membership and non-membership degrees clustered around the middle of the range, rather than uniformly distributed. We can achieve this by using a Beta distribution as the default.

We will name our new generator 'qrofn_beta' to distinguish it from the standard 'qrofn' generator.

Step 1 & 2: Implement and Register the Generator

We create a new Python file (e.g., in your project’s utility module) and add the following code. This single block of code defines the class and registers it.

import numpy as np
from typing import Any, Dict, Tuple

from axisfuzzy.core import Fuzznum, Fuzzarray
from axisfuzzy.random.base import ParameterizedRandomGenerator
from axisfuzzy.random.registry import register_random
from axisfuzzy.fuzztype.qrofs.backend import QROFNBackend

@register_random  # The decorator that makes it all work
class QROFNBetaRandomGenerator(ParameterizedRandomGenerator):
    """
    An alternative random generator for q-Rung Orthopair Fuzzy Numbers
    that defaults to a Beta distribution for more centralized values.
    """
    # The mtype key that users will use to call this generator
    mtype = "qrofn_beta"

    def get_default_parameters(self) -> Dict[str, Any]:
        """
        Define default parameters. Here, we set the distribution to 'beta'.
        """
        return {
            'md_dist': 'beta',      # Default to Beta distribution
            'md_low': 0.0,
            'md_high': 1.0,
            'nu_mode': 'orthopair', # Generate non-membership under the constraint
            'nu_dist': 'uniform',
            'nu_low': 0.0,
            'nu_high': 1.0,
            # Default shape parameters for the Beta distribution
            'a': 2.0,
            'b': 2.0,
            # Default parameters for other distributions (e.g., normal)
            'loc': 0.5,
            'scale': 0.15
        }

    def validate_parameters(self, **params) -> None:
        """
        Validate the structural and distributional parameters.
        """
        # Validate the q-rung parameter
        if 'q' not in params or not isinstance(params['q'], int) or params['q'] <= 0:
            raise ValueError(f"q must be a positive integer, got {params.get('q')}")

        # Use the built-in helper to validate ranges
        self._validate_range('md_low', params.get('md_low', 0.0), 0.0, 1.0)
        self._validate_range('md_high', params.get('md_high', 1.0), 0.0, 1.0)
        if params.get('md_low', 0.0) > params.get('md_high', 1.0):
            raise ValueError("md_low cannot be greater than md_high")

    def fuzznum(self, rng: np.random.Generator, **params) -> 'Fuzznum':
        """
        Generate a single 'qrofn_beta' fuzzy number.
        """
        # 1. Merge user-provided parameters with our defaults
        p = self._merge_parameters(**params)
        self.validate_parameters(**p)

        # 2. Generate membership degree (md) using our sampling utility
        md = self._sample_from_distribution(
            rng, dist=p['md_dist'], low=p['md_low'], high=p['md_high'],
            a=p['a'], b=p['b'], loc=p['loc'], scale=p['scale']
        )

        # 3. Generate non-membership degree (nmd) based on the q-ROFN constraint
        max_nmd = (1 - md**p['q'])**(1 / p['q'])
        nmd = rng.uniform(0, max_nmd) # Sample uniformly within the valid range

        # 4. Create the Fuzznum instance
        return Fuzznum(mtype=self.mtype, q=p['q']).create(md=md, nmd=nmd)

    def fuzzarray(self, rng: np.random.Generator, shape: Tuple[int, ...], **params) -> 'Fuzzarray':
        """
        High-performance batch generation of 'qrofn_beta' fuzzy arrays.
        """
        # 1. Merge and validate parameters
        p = self._merge_parameters(**params)
        self.validate_parameters(**p)
        size = int(np.prod(shape))

        # 2. Vectorized generation of membership degrees
        mds = self._sample_from_distribution(
            rng, size=size, dist=p['md_dist'], low=p['md_low'], high=p['md_high'],
            a=p['a'], b=p['b'], loc=p['loc'], scale=p['scale']
        )

        # 3. Vectorized generation of non-membership degrees
        max_nmds = (1 - mds**p['q'])**(1 / p['q'])
        # Efficiently sample within the constraint for the whole array
        nmds = rng.uniform(0, 1, size=size) * max_nmds

        # 4. Directly construct the backend for maximum performance
        backend = QROFNBackend.from_arrays(
            mds=mds.reshape(shape),
            nmds=nmds.reshape(shape),
            q=p['q']
        )
        return Fuzzarray(backend=backend)

Step 3: Use the New Generator

Now, as long as the Python module containing AlternativeQROFNGenerator is imported, you can use it directly with fr.rand() by specifying mtype='qrofn_beta'.

import axisfuzzy.random as fr
# Make sure the module with AlternativeQROFNGenerator is imported, e.g.:
# from .my_custom_generators import QROFNBetaRandomGenerator

# Generate a 100-element array using our new generator
# It will use a Beta(2,2) distribution by default
beta_arr = fr.rand('qrofn_beta', q=3, shape=100)

# We can still override the defaults, just like with any other generator
uniform_arr = fr.rand('qrofn_beta', q=3, shape=100, md_dist='uniform')

# Check which generators are available
print(fr.list_registered())
# Expected output will now include: ['qrofn', 'ivfn', ..., 'qrofn_beta']

Key Takeaways from the Example

  • Inherit from `ParameterizedRandomGenerator`: This provides powerful helpers like _merge_parameters and _sample_from_distribution, saving you from writing boilerplate code.

  • Set the `mtype` Attribute: This unique string is the key that links your generator to the fr.rand() function.

  • Implement the Four Core Methods: get_default_parameters, validate_parameters, fuzznum, and fuzzarray.

  • Vectorize `fuzzarray` for Performance: The most critical performance aspect is to use vectorized NumPy operations for batch generation and construct the backend directly, avoiding slow Python loops.

  • The `@register_random` Decorator Does the Magic: This simple decorator handles all the complexity of making your generator discoverable by the rest of the library.

Conclusion

This guide has provided a comprehensive tour of the axisfuzzy.random system, a powerful and flexible framework for generating random fuzzy numbers. We have explored its modular architecture, designed for extensibility and performance, and delved into the core components that ensure reproducibility and ease of use.

You have learned how to:

  • Leverage the high-level API: Use functions like fr.rand() and fr.choice() to effortlessly generate various types of random fuzzy numbers.

  • Ensure Reproducibility: Control the random state with fr.set_seed() to guarantee that your experiments are deterministic and verifiable.

  • Understand the Architecture: Appreciate how the API, Registry, Generator Blueprint, and Seed Management layers work together to provide a seamless experience.

  • Extend the System: Create and register your own custom random generators by inheriting from ParameterizedRandomGenerator and using the @register_random decorator.

By mastering these concepts, you are now equipped to harness the full potential of axisfuzzy for a wide range of applications, from sophisticated Monte Carlo simulations to robust algorithm testing. The system’s design not only meets current needs but also provides a solid foundation for future extensions, ensuring that axisfuzzy remains at the forefront of fuzzy logic research and development.