Source code for boolforge.generate.dispatch

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
This module provides functions for generating random Boolean functions and
Boolean networks with specified structural and dynamical properties.

The :mod:`~boolforge.generate` module enables the systematic creation of
Boolean functions and networks that satisfy particular constraints, such as
specified canalization depth, sensitivity range, bias, or connectivity.
Generated instances can be used for statistical analysis, benchmarking, or
simulation studies.

Several generation routines leverage Numba acceleration for efficient sampling
and evaluation of large function spaces. While Numba is **recommended** to
achieve near-native performance, it is **not required** for functionality; all
functions have pure Python fallbacks.

This module complements :mod:`~boolforge.boolean_function` and
:mod:`~boolforge.boolean_network` by facilitating reproducible generation of
synthetic test cases and large ensembles of random networks.

Example
-------
>>> import boolforge
>>> boolforge.random_function(n=3)
>>> boolforge.random_network(N=5, n=2)
"""

import numpy as np

from ..boolean_function import BooleanFunction
from .. import utils

from .canalization import random_k_canalizing_function_with_specific_layer_structure
from .canalization import random_k_canalizing_function
from .canalization import random_non_canalizing_function
from .canalization import random_non_canalizing_non_degenerate_function
from .functions import random_function_with_bias
from .functions import random_function_with_exact_hamming_weight
from .functions import random_parity_function
from .functions import random_non_degenerate_function


def _validate_bias(bias: float) -> None:
    if not isinstance(bias, (float, int, np.floating)):
        raise TypeError("bias must be a float")
    if not (0.0 <= bias <= 1.0):
        raise ValueError("bias must be in [0, 1]")

def _validate_absolute_bias(absolute_bias: float) -> None:
    if not isinstance(absolute_bias, (float, int, np.floating)):
        raise TypeError("absolute_bias must be a float")
    if not (0.0 <= absolute_bias <= 1.0):
        raise ValueError("absolute_bias must be in [0, 1]")

def _validate_hamming_weight(
    n: int,
    hamming_weight: int,
    *,
    exact_depth: bool,
) -> None:
    if not isinstance(hamming_weight, (int, np.integer)):
        raise TypeError("hamming_weight must be an integer")
    if not (0 <= hamming_weight <= 2**n):
        raise ValueError("hamming_weight must satisfy 0 <= hamming_weight <= 2**n")

    if exact_depth and not (1 < hamming_weight < 2**n - 1):
        raise ValueError(
            "If exact_depth=True and depth=0, hamming_weight must be in "
            "{2, 3, ..., 2**n - 2}. "
            "Functions with weights 0, 1, 2**n-1, 2**n are canalizing."
        )


[docs] def random_function( n: int, depth: int = 0, exact_depth: bool = False, uniform_over_functions: bool = True, layer_structure: list[int] | None = None, parity: bool = False, allow_degenerate_functions: bool = False, bias: float = 0.5, absolute_bias: float = 0, use_absolute_bias: bool = False, hamming_weight: int | None = None, *, rng = None, ) -> BooleanFunction: """ Generate a random Boolean function under flexible structural constraints. This function acts as a high-level generator that unifies several common ensembles of Boolean functions, including parity functions, canalizing functions of specified depth or layer structure, functions with fixed Hamming weight, and biased random functions. The first applicable generation rule (in the order described below) is applied. Selection logic (first applicable rule is used) 1. If ``parity`` is True, return a random parity function (see ``random_parity_function``). 2. Else, if ``layer_structure`` is provided, return a Boolean function with the specified canalizing layer structure using ``random_k_canalizing_function_with_specific_layer_structure``. Exactness of the canalizing depth is controlled by ``exact_depth``. 3. Else, if ``depth > 0``, return a k-canalizing function with ``k = min(depth, n)`` using ``random_k_canalizing_function``. If ``exact_depth`` is True, the function has exactly this depth; otherwise, its canalizing depth is at least ``k``. If ``uniform_over_functions`` is True, canalizing layer structures are sampled uniformly at random (up to the imposed constraints). If False, canalized outputs are sampled independently and uniformly as bitstrings, which biases the distribution toward more symmetric layer structures. 4. Else, if ``hamming_weight`` is provided, repeatedly sample Boolean functions with the specified Hamming weight until additional constraints implied by ``exact_depth`` and ``allow_degenerate_functions`` are satisfied. 5. Else, generate a random Boolean function using a Bernoulli model with either: - fixed bias ``bias``, or - an automatically chosen bias determined by ``absolute_bias`` if ``use_absolute_bias`` is True. Additional constraints on canalization and degeneracy are enforced depending on ``exact_depth`` and ``allow_degenerate_functions``. Parameters ---------- n : int Number of input variables. Must be a positive integer. depth : int, optional Requested canalizing depth. Used only if ``layer_structure`` is None and ``depth > 0``. If ``exact_depth`` is True, the function has exactly this canalizing depth (clipped at ``n``); otherwise, its depth is at least ``depth``. Default is 0. exact_depth : bool, optional Enforce exact canalizing depth where applicable. If ``depth == 0``, setting ``exact_depth=True`` enforces that the function is non-canalizing. Default is False. uniform_over_functions : bool, optional If True (default), canalizing layer structures are sampled uniformly at random in canalizing-function branches. If False, canalized outputs are sampled independently as bitstrings, inducing a bias toward more symmetric structures. layer_structure : list[int] or None, optional Explicit canalizing layer structure ``[k1, ..., kr]``. If provided, this takes precedence over ``depth``. Default is None. parity : bool, optional If True, ignore all other options and return a random parity function. Default is False. allow_degenerate_functions : bool, optional If True, functions with non-essential variables may be returned in random-generation branches. If False, non-degenerate functions are enforced whenever possible. Default is False. bias : float, optional Probability of a 1 when sampling truth-table entries independently. Used only if ``use_absolute_bias`` is False and no other branch applies. Must lie in ``[0, 1]``. Default is 0.5. absolute_bias : float, optional Absolute deviation from 0.5 used to determine the bias when ``use_absolute_bias`` is True. The bias is chosen uniformly from ``{0.5*(1 - absolute_bias), 0.5*(1 + absolute_bias)}``. Must lie in ``[0, 1]``. Default is 0. use_absolute_bias : bool, optional If True, ignore ``bias`` and determine the bias using ``absolute_bias``. Default is False. hamming_weight : int or None, optional If provided, enforce that the Boolean function has exactly this many ones in its truth table. Additional constraints are enforced depending on ``exact_depth`` and ``allow_degenerate_functions``. Default is None. rng : int, numpy.random.Generator, numpy.random.RandomState, random.Random, or None, optional Random number generator or seed specification. Passed to ``utils._coerce_rng``. Returns ------- BooleanFunction A randomly generated Boolean function of arity ``n``. Raises ------ TypeError If parameters have invalid types. ValueError If parameter values or combinations are invalid. Notes ----- For any fixed combination of parameters, this function samples **uniformly at random** from the set of Boolean functions satisfying the corresponding constraints. Non-uniformity arises only when explicitly requested via ``uniform_over_functions=False``. Extremely biased functions are often degenerate or highly canalizing; under restrictive parameter choices, some branches may reject repeatedly before returning a valid function. Examples -------- >>> # Unbiased, non-degenerate random function >>> f = random_function(n=3) >>> # Function with canalizing depth at least 2 >>> f = random_function(n=5, depth=2) >>> # Function with exact canalizing depth 2 >>> f = random_function(n=5, depth=2, exact_depth=True) >>> # Function with a specific canalizing layer structure >>> f = random_function(n=6, layer_structure=[2, 1]) >>> # Parity function >>> f = random_function(n=4, parity=True) >>> # Fixed Hamming weight with non-canalizing and non-degenerate constraints >>> f = random_function( ... n=5, ... hamming_weight=10, ... exact_depth=True, ... allow_degenerate_functions=False ... ) """ if not isinstance(n, (int, np.integer)) or n <= 0: raise ValueError("n must be a positive integer") rng = utils._coerce_rng(rng) # ------------------------------------------------------------ # Parity branch (highest priority) # ------------------------------------------------------------ if parity: return random_parity_function(n, rng=rng) # ------------------------------------------------------------ # Layer structure branch # ------------------------------------------------------------ if layer_structure is not None: return random_k_canalizing_function_with_specific_layer_structure( n, layer_structure, exact_depth=exact_depth, rng=rng, ) # ------------------------------------------------------------ # Canalizing depth branch # ------------------------------------------------------------ if not isinstance(depth, (int, np.integer)) or depth < 0: raise ValueError("depth must be a nonnegative integer") if depth > 0: return random_k_canalizing_function( n, min(depth, n), exact_depth=exact_depth, uniform_over_functions=uniform_over_functions, rng=rng, ) # ------------------------------------------------------------ # Fixed Hamming weight branch # ------------------------------------------------------------ if hamming_weight is not None: _validate_hamming_weight(n, hamming_weight, exact_depth=exact_depth) while True: f = random_function_with_exact_hamming_weight( n, hamming_weight, rng=rng ) if allow_degenerate_functions and exact_depth: if not f.is_canalizing(): return f elif allow_degenerate_functions: return f elif exact_depth: if not f.is_canalizing() and not f.is_degenerate(): return f else: if not f.is_degenerate(): return f # ------------------------------------------------------------ # Bias-based random generation # ------------------------------------------------------------ if use_absolute_bias: _validate_absolute_bias(absolute_bias) bias_of_function = rng.choice( [0.5 * (1 - absolute_bias), 0.5 * (1 + absolute_bias)] ) else: _validate_bias(bias) bias_of_function = bias if allow_degenerate_functions: if exact_depth: return random_non_canalizing_function( n, bias_of_function, rng=rng ) else: return random_function_with_bias( n, bias_of_function, rng=rng ) else: if exact_depth: return random_non_canalizing_non_degenerate_function( n, bias_of_function, rng=rng ) else: return random_non_degenerate_function( n, bias_of_function, rng=rng )