Source code for myogen

import warnings

import numpy as np
from numpy.random import Generator

_DEFAULT_SEED: int = 180319
_current_seed: int = _DEFAULT_SEED
_random_generator: Generator = np.random.default_rng(_DEFAULT_SEED)


def get_random_generator() -> Generator:
    """
    Return the current global RNG.

    Always reflects the most recent ``set_random_seed`` call. Prefer this
    accessor over importing ``RANDOM_GENERATOR`` directly — a direct import
    captures a stale reference that will not update when the seed changes.
    """
    return _random_generator


def get_random_seed() -> int:
    """Return the seed currently in effect."""
    return _current_seed


def derive_subseed(*labels: int) -> int:
    """
    Derive a deterministic, seed-tracking sub-seed from the current seed and a tuple of integer labels.

    Intended for seeding non-NumPy generators (Cython Mersenne spike
    generators, sklearn ``random_state``, etc.) so that a call to
    :func:`set_random_seed` propagates to them. Label order matters:
    ``derive_subseed(a, b)`` and ``derive_subseed(b, a)`` yield different
    sub-seeds. Each label must be a **non-negative** integer; callers with
    signed identifiers should offset them beforehand (NumPy's
    :class:`~numpy.random.SeedSequence`, which backs this helper, rejects
    negatives).

    This replaces the pre-existing ``SEED + (class_id+1)*(global_id+1)``
    derivation, which collided on swapped factors — e.g. ``(0, 5)`` and
    ``(1, 2)`` both produced ``+6``. The present mixing function uses
    :class:`numpy.random.SeedSequence` to fold the inputs into a 32-bit
    integer; collisions remain possible in principle (birthday-paradox
    probability ≈ ``N² / 2³³``) but are negligible for realistic motor-unit
    pool sizes (≲ 10⁻⁷ at 1000 cells).

    Returns
    -------
    int
        A non-negative 32-bit integer suitable for passing as a seed to
        NumPy, sklearn, or the bundled Cython RNG wrappers.
    """
    seq = np.random.SeedSequence(entropy=(_current_seed, *labels))
    return int(seq.generate_state(1, dtype=np.uint32)[0])


[docs] def set_random_seed(seed: int = _DEFAULT_SEED) -> None: """ Set the random seed for reproducibility. Rebuilds the global NumPy ``Generator``. All modules that read the RNG through :func:`get_random_generator` will observe the new state on their next draw; this includes seeds derived for non-NumPy RNGs (e.g. sklearn ``random_state`` arguments or Cython Mersenne generators), which are now drawn from the global RNG rather than read from a frozen module constant. Parameters ---------- seed : int, optional Seed value to set, by default 180319. """ global _random_generator, _current_seed _current_seed = seed _random_generator = np.random.default_rng(seed) print(f"Random seed set to {seed}.")
def __getattr__(name: str): """Backwards-compatible access for the deprecated ``RANDOM_GENERATOR`` and ``SEED`` module attributes.""" if name == "RANDOM_GENERATOR": warnings.warn( "myogen.RANDOM_GENERATOR is deprecated; use myogen.get_random_generator() " "to always retrieve the current RNG. Module-level imports of " "RANDOM_GENERATOR capture a stale reference that does not update when " "set_random_seed() is called.", DeprecationWarning, stacklevel=2, ) return _random_generator if name == "SEED": warnings.warn( "myogen.SEED is deprecated; use myogen.get_random_seed() to retrieve " "the seed currently in effect.", DeprecationWarning, stacklevel=2, ) return _current_seed raise AttributeError(f"module {__name__!r} has no attribute {name!r}") class MyoGenSetupError(Exception): """Exception raised when MyoGen setup fails.""" pass def _setup_myogen(quiet: bool = False, force_rebuild: bool = False, strict: bool = False) -> bool: """ Set up MyoGen with NEURON mechanism compilation and loading. This function handles the compilation and loading of NMODL files required for neural simulations. It also compiles Cython extensions if they are not already available (typically only needed in development mode). Parameters ---------- quiet : bool, optional If True, suppress most output messages, by default False force_rebuild : bool, optional If True, force recompilation of Cython extensions even if they appear to be already compiled, by default False strict : bool, optional If True, raise exceptions on errors instead of returning False. Recommended for CI/CD pipelines and production deployments. Returns ------- bool True if setup completed successfully, False otherwise Raises ------ MyoGenSetupError If strict=True and setup fails """ def error(msg): """Handle errors based on strict mode.""" if strict: raise MyoGenSetupError(msg) else: print(f"ERROR: {msg}") return False # Check if Cython extensions are already compiled (from installed package) cython_modules = [ "myogen.simulator.neuron._cython._spindle", "myogen.simulator.neuron._cython._hill", "myogen.simulator.neuron._cython._gto", "myogen.simulator.neuron._cython._poisson_process_generator", "myogen.simulator.neuron._cython._gamma_process_generator", "myogen.simulator.neuron._cython._simulate_fiber", ] all_compiled = True if not force_rebuild: for module_name in cython_modules: try: __import__(module_name) except ImportError: all_compiled = False break else: all_compiled = False # Only compile Cython extensions if not already available if not all_compiled: if not quiet: print("Compiling Cython extensions (development mode)...") import os from pathlib import Path # Check if .pyx files exist (development install) myogen_root = Path(__file__).parent pyx_files_exist = ( myogen_root / "simulator" / "neuron" / "_cython" / "_spindle.pyx" ).exists() if not pyx_files_exist: if not quiet: print("Cython source files not found. This is expected for installed packages.") print("Cython extensions should have been compiled during installation.") # Try importing again to give a clearer error if truly missing try: from myogen.simulator.neuron._cython import _spindle if not quiet: print("Cython extensions are available.") except ImportError as e: return error( f"Cython extensions are not available: {e}\n" "Please reinstall MyoGen or run setup from a development clone." ) else: # Development mode: compile in-place from Cython.Build import cythonize from setuptools import Extension, setup setup( ext_modules=cythonize( [ Extension( "myogen.simulator.neuron._cython._spindle", ["myogen/simulator/neuron/_cython/_spindle.pyx"], extra_compile_args=["-O2", "-march=native", "-ffast-math"], ), Extension( "myogen.simulator.neuron._cython._hill", ["myogen/simulator/neuron/_cython/_hill.pyx"], extra_compile_args=["-O2", "-march=native"], ), Extension( "myogen.simulator.neuron._cython._gto", ["myogen/simulator/neuron/_cython/_gto.pyx"], extra_compile_args=["-O2", "-march=native", "-ffast-math"], ), Extension( "myogen.simulator.neuron._cython._poisson_process_generator", ["myogen/simulator/neuron/_cython/_poisson_process_generator.pyx"], extra_compile_args=["-O2", "-march=native", "-ffast-math"], ), Extension( "myogen.simulator.neuron._cython._gamma_process_generator", ["myogen/simulator/neuron/_cython/_gamma_process_generator.pyx"], extra_compile_args=["-O2", "-march=native", "-ffast-math"], ), Extension( "myogen.simulator.neuron._cython._simulate_fiber", ["myogen/simulator/neuron/_cython/_simulate_fiber.pyx"], extra_compile_args=["-O2", "-march=native", "-ffast-math"], ), ], compiler_directives={"embedsignature": True}, nthreads=4, ), script_args=["build_ext", "--inplace"], include_dirs=[np.get_include()], ) if not quiet: print("Cython extensions compiled successfully.") elif not quiet: print("Cython extensions already available.") # Compile NMODL files for NEURON try: from pathlib import Path import platform # Check if NMODL files are already compiled myogen_root = Path(__file__).parent nmodl_path = myogen_root / "simulator" / "nmodl_files" # Check for compiled NMODL libraries nmodl_compiled = False if platform.system() == "Windows": # On Windows, look for nrnmech.dll nmodl_compiled = any(nmodl_path.glob("*nrnmech.dll")) else: # On Unix, look for libnrnmech.so or x86_64 directory nmodl_compiled = (nmodl_path / "x86_64").exists() or any(nmodl_path.glob("*nrnmech.so")) if nmodl_compiled and not force_rebuild: if not quiet: print("NMODL mechanisms already compiled.") return True else: if not quiet: print("Compiling NMODL mechanisms...") from myogen.utils.nmodl import compile_nmodl_files result = compile_nmodl_files(quiet=quiet) if result and not quiet: print("NMODL mechanisms compiled successfully.") return result except ImportError as e: return error(f"NEURON not available, cannot compile mechanisms: {e}") except Exception as e: return error(f"MyoGen setup failed: {e}") from myogen.utils.nmodl import ( load_nmodl_mechanisms, NMODLLoadError, get_mechanism_parameters, validate_mechanism_parameter, set_mechanism_param, ) # Auto-load NMODL mechanisms when MyoGen is imported # quiet=True suppresses success messages, but warnings are still shown # Use strict=True in your code to raise exceptions on failure _nmodl_loaded = load_nmodl_mechanisms(quiet=True, strict=False) __all__ = [ "get_random_generator", "get_random_seed", "derive_subseed", "set_random_seed", "load_nmodl_mechanisms", "NMODLLoadError", "MyoGenSetupError", "get_mechanism_parameters", "validate_mechanism_parameter", "set_mechanism_param", "_setup_myogen", ]