Skip to content

Top-level API (myogen)

Reproducibility

Module-level state:

  • myogen.RANDOM_GENERATOR — deprecated compatibility attribute; use get_random_generator() instead.
  • myogen.SEED — deprecated compatibility attribute; use get_random_seed() instead.

set_random_seed

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 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:

Name Type Description Default
seed int

Seed value to set, by default 180319.

_DEFAULT_SEED
Source code in myogen/__init__.py
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 ``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)

    # Reset per-class cell ID counters so deterministic seeds produce
    # deterministic ``global__ID`` / ``pool__ID`` assignments across runs.
    # Lazy import avoids a circular dependency during package initialization.
    try:
        from myogen.simulator.neuron.cells import reset_cell_id_counters

        reset_cell_id_counters()
    except Exception:
        pass

    print(f"Random seed set to {seed}.")

get_random_generator

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.

Source code in myogen/__init__.py
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

get_random_seed

get_random_seed() -> int

Return the seed currently in effect.

Source code in myogen/__init__.py
def get_random_seed() -> int:
    """Return the seed currently in effect."""
    return _current_seed

derive_subseed

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 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 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 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:

Type Description
int

A non-negative 32-bit integer suitable for passing as a seed to NumPy, sklearn, or the bundled Cython RNG wrappers.

Source code in myogen/__init__.py
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
    ``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
    ``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
    ``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])

load_nmodl_mechanisms

load_nmodl_mechanisms(quiet: bool = True, strict: bool = False) -> bool

Load pre-compiled NMODL mechanisms into current NEURON session.

This function loads previously compiled mechanisms into NEURON. It should be called at the start of every script that uses NEURON.

Args: quiet: If True, suppress output messages strict: If True, raise exceptions on errors instead of returning False. Recommended for production code to catch configuration issues early.

Returns: bool: True if mechanisms loaded successfully, False otherwise

Raises: NMODLLoadError: If strict=True and mechanisms fail to load

Source code in myogen/utils/nmodl.py
def load_nmodl_mechanisms(quiet: bool = True, strict: bool = False) -> bool:
    """
    Load pre-compiled NMODL mechanisms into current NEURON session.

    This function loads previously compiled mechanisms into NEURON.
    It should be called at the start of every script that uses NEURON.

    Args:
        quiet: If True, suppress output messages
        strict: If True, raise exceptions on errors instead of returning False.
                Recommended for production code to catch configuration issues early.

    Returns:
        bool: True if mechanisms loaded successfully, False otherwise

    Raises:
        NMODLLoadError: If strict=True and mechanisms fail to load
    """

    def log(msg):
        return print(msg) if not quiet else None

    def error(msg):
        """Handle errors based on strict mode."""
        if strict:
            raise NMODLLoadError(msg)
        else:
            print(f"WARNING: {msg}")
            return False

    # On Windows, add NEURON paths to PATH before importing
    if platform.system() == "Windows":
        # Priority: Registry > NEURONHOME env var > hardcoded C: paths
        neuron_home = _find_neuron_home_from_registry(quiet=quiet)

        if not neuron_home and os.environ.get("NEURONHOME"):
            neuron_home = Path(os.environ.get("NEURONHOME"))
            if not neuron_home.exists():
                neuron_home = None

        # Fallback to hardcoded C: drive paths
        if not neuron_home:
            neuron_homes_fallback = [
                Path("C:/nrn"),
                Path("C:/Program Files/NEURON"),
            ]
            for home in neuron_homes_fallback:
                if home.exists():
                    neuron_home = home
                    break

        if neuron_home:
            # Add both bin and lib/python directories
            neuron_bin = neuron_home / "bin"
            neuron_lib_path = neuron_home / "lib" / "python"

            paths_to_add = []
            if neuron_bin.exists():
                paths_to_add.append(str(neuron_bin))
            if neuron_lib_path.exists():
                paths_to_add.append(str(neuron_lib_path))

            if paths_to_add:
                current_path = os.environ.get("PATH", "")
                for path in paths_to_add:
                    if path not in current_path:
                        os.environ["PATH"] = f"{path};{os.environ['PATH']}"
                log(f"Added NEURON paths to PATH: {', '.join(paths_to_add)}")

    try:
        import neuron
        from neuron import h

        # Test if mechanisms are already loaded
        try:
            test_section = h.Section()
            test_section.insert("caL")
            test_section = None  # Clean up
            log("NMODL mechanisms already loaded, skipping reload")
            return True
        except Exception:
            pass  # Mechanisms not loaded, continue

        # Load mechanisms from MyoGen's nmodl directory
        nmodl_path = find_nmodl_directory()
        log(f"Loading NMODL mechanisms from {nmodl_path}")

        neuron.load_mechanisms(str(nmodl_path), warn_if_already_loaded=quiet)
        log("Successfully loaded NMODL mechanisms")
        return True

    except ImportError as e:
        return error(f"NEURON not available, cannot load mechanisms: {str(e)}")
    except Exception as e:
        return error(f"Failed to load NMODL mechanisms: {str(e)}")

get_mechanism_parameters

get_mechanism_parameters(mechanism_name: str) -> list

Get list of valid parameters for a NEURON mechanism.

Args: mechanism_name: Name of the mechanism (e.g., "mAHP", "na3rp")

Returns: List of parameter names available for this mechanism

Raises: ValueError: If mechanism is not loaded or doesn't exist

Source code in myogen/utils/nmodl.py
def get_mechanism_parameters(mechanism_name: str) -> list:
    """
    Get list of valid parameters for a NEURON mechanism.

    Args:
        mechanism_name: Name of the mechanism (e.g., "mAHP", "na3rp")

    Returns:
        List of parameter names available for this mechanism

    Raises:
        ValueError: If mechanism is not loaded or doesn't exist
    """
    from neuron import h

    # Create a temporary section to inspect the mechanism
    temp_section = h.Section()
    try:
        temp_section.insert(mechanism_name)
    except Exception as e:
        raise ValueError(f"Mechanism '{mechanism_name}' not found. Is it loaded? Error: {e}")

    seg = temp_section(0.5)
    mech = getattr(seg, mechanism_name)

    # Get all non-private, non-callable attributes
    params = []
    for attr in dir(mech):
        if not attr.startswith("_"):
            try:
                val = getattr(mech, attr)
                if not callable(val):
                    params.append(attr)
            except Exception:
                pass

    # Clean up
    temp_section = None

    return params

validate_mechanism_parameter

validate_mechanism_parameter(section, param_name: str, mechanism_name: str = None) -> None

Validate that a parameter exists on a section before setting it.

Args: section: NEURON Section object param_name: Parameter name (e.g., "gcamax_mAHP" or just "gcamax" with mechanism_name) mechanism_name: Optional mechanism name if param_name doesn't include suffix

Raises: AttributeError: If the parameter doesn't exist on the section

Example: >>> validate_mechanism_parameter(soma, "gcamax_mAHP") # Validates before setting >>> soma.gcamax_mAHP = 1e-5 # Now safe to set

Source code in myogen/utils/nmodl.py
def validate_mechanism_parameter(section, param_name: str, mechanism_name: str = None) -> None:
    """
    Validate that a parameter exists on a section before setting it.

    Args:
        section: NEURON Section object
        param_name: Parameter name (e.g., "gcamax_mAHP" or just "gcamax" with mechanism_name)
        mechanism_name: Optional mechanism name if param_name doesn't include suffix

    Raises:
        AttributeError: If the parameter doesn't exist on the section

    Example:
        >>> validate_mechanism_parameter(soma, "gcamax_mAHP")  # Validates before setting
        >>> soma.gcamax_mAHP = 1e-5  # Now safe to set
    """
    # If mechanism name provided, construct full parameter name
    if mechanism_name:
        full_param = f"{param_name}_{mechanism_name}"
    else:
        full_param = param_name

    # Try to access the parameter - will raise AttributeError if invalid
    try:
        _ = getattr(section, full_param)
    except AttributeError:
        # Extract mechanism name from param if not provided (e.g., "gcamax_mAHP" -> "mAHP")
        inferred_mech = None
        if "_" in full_param and not mechanism_name:
            parts = full_param.rsplit("_", 1)
            if len(parts) == 2:
                inferred_mech = parts[1]

        # Get available parameters for the mechanism
        available = []
        mech_to_check = mechanism_name or inferred_mech
        mech_found = False

        if mech_to_check:
            try:
                available = get_mechanism_parameters(mech_to_check)
                available = [f"{p}_{mech_to_check}" for p in available]
                mech_found = True
            except ValueError:
                # Mechanism doesn't exist - will suggest alternatives below
                pass

        # Fallback: show all section params
        if not available:
            for attr in dir(section):
                if not attr.startswith("_") and not callable(getattr(section, attr, None)):
                    available.append(attr)

        # Build helpful error message
        msg = f"Parameter '{full_param}' does not exist on section.\n"

        # If mechanism wasn't found, suggest similar ones
        if mech_to_check and not mech_found:
            # Get list of inserted mechanisms
            inserted = []
            seg = section(0.5)
            for mech in seg:
                inserted.append(mech.name())
            if inserted:
                msg += f"Inserted mechanisms: {inserted}\n"
                # Check for similar mechanism names (simple prefix match)
                if len(mech_to_check) >= 3:
                    similar = [m for m in inserted if m.lower().startswith(mech_to_check[:3].lower())]
                    if similar:
                        msg += f"Did you mean: {similar}?\n"

        msg += f"Available parameters{' for ' + mech_to_check if mech_found else ''}: {available}"
        raise AttributeError(msg)

set_mechanism_param

set_mechanism_param(section, param_name: str, value, validate: bool = True) -> None

Set a mechanism parameter with optional validation.

Args: section: NEURON Section object param_name: Parameter name (e.g., "gcamax_mAHP") value: Value to set validate: If True, validate parameter exists first (default: True)

Raises: AttributeError: If validate=True and parameter doesn't exist

Example: >>> set_mechanism_param(soma, "gcamax_mAHP", 1e-5) # Safe setting >>> set_mechanism_param(soma, "gcamax_mAHPr", 1e-5) # Raises AttributeError (typo)

Source code in myogen/utils/nmodl.py
def set_mechanism_param(section, param_name: str, value, validate: bool = True) -> None:
    """
    Set a mechanism parameter with optional validation.

    Args:
        section: NEURON Section object
        param_name: Parameter name (e.g., "gcamax_mAHP")
        value: Value to set
        validate: If True, validate parameter exists first (default: True)

    Raises:
        AttributeError: If validate=True and parameter doesn't exist

    Example:
        >>> set_mechanism_param(soma, "gcamax_mAHP", 1e-5)  # Safe setting
        >>> set_mechanism_param(soma, "gcamax_mAHPr", 1e-5)  # Raises AttributeError (typo)
    """
    if validate:
        validate_mechanism_parameter(section, param_name)
    setattr(section, param_name, value)