Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow API functions to be used without loading hoc/mod #minor #124

Merged
merged 2 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ nrnivmodl.log
.ruff_cache
.pytest_cache
*.btr
*.whl
4 changes: 3 additions & 1 deletion bluecellulab/cell/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from bluecellulab.circuit.node_id import CellId
from bluecellulab.circuit.simulation_access import get_synapse_replay_spikes
from bluecellulab.exceptions import BluecellulabError
from bluecellulab.importer import load_hoc_and_mod_files
from bluecellulab.neuron_interpreter import eval_neuron
from bluecellulab.rngsettings import RNGSettings
from bluecellulab.stimuli import SynapseReplay
Expand All @@ -54,6 +55,7 @@ class Cell(InjectableMixin, PlottableMixin):

last_id = 0

@load_hoc_and_mod_files
def __init__(self,
template_path: str | Path,
morphology_path: str | Path,
Expand Down Expand Up @@ -134,7 +136,7 @@ def __init__(self,
self.secname_to_psection: dict[str, psection.PSection] = {}

self.emodel_properties = emodel_properties
if template_format in ['v6']:
if template_format == 'v6':
if self.emodel_properties is None:
raise BluecellulabError('EmodelProperties must be provided for v6 template')
self.hypamp: float | None = self.emodel_properties.holding_current
Expand Down
19 changes: 16 additions & 3 deletions bluecellulab/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import neuron

from bluecellulab.exceptions import BluecellulabError
from bluecellulab.utils import run_once


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -71,6 +72,18 @@ def print_header(neuron: ModuleType, mod_lib_path: str) -> None:
logger.info(f"Mod lib: {mod_lib_path}")


mod_lib_paths = import_mod_lib(neuron)
import_neurodamus(neuron)
print_header(neuron, mod_lib_paths)
@run_once
def _load_hoc_and_mod_files() -> None:
"""Import hoc and mod files."""
logger.info("Loading the mod files.")
mod_lib_paths = import_mod_lib(neuron)
logger.info("Loading the hoc files.")
import_neurodamus(neuron)
print_header(neuron, mod_lib_paths)


def load_hoc_and_mod_files(func):
def wrapper(*args, **kwargs):
_load_hoc_and_mod_files()
return func(*args, **kwargs)
return wrapper
3 changes: 2 additions & 1 deletion bluecellulab/rngsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
from bluecellulab import Singleton
from bluecellulab.circuit.circuit_access import CircuitAccess
from bluecellulab.exceptions import UndefinedRNGException
from bluecellulab.importer import load_hoc_and_mod_files

logger = logging.getLogger(__name__)


class RNGSettings(metaclass=Singleton):
"""Class that represents RNG settings in bluecellulab."""

@load_hoc_and_mod_files
def __init__(
self,
mode: Optional[str] = None,
Expand All @@ -41,7 +43,6 @@ def __init__(
circuit: circuit access object, if present seeds are read from simulation
base_seed: base seed for entire sim, overrides config value
"""

self._mode = ""
if mode is None:
if circuit_access is not None:
Expand Down
2 changes: 2 additions & 0 deletions bluecellulab/ssim.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from bluecellulab.circuit.format import determine_circuit_format, CircuitFormat
from bluecellulab.circuit.node_id import create_cell_id, create_cell_ids
from bluecellulab.circuit.simulation_access import BluepySimulationAccess, SimulationAccess, SonataSimulationAccess, _sample_array
from bluecellulab.importer import load_hoc_and_mod_files
from bluecellulab.stimuli import Noise, OrnsteinUhlenbeck, RelativeOrnsteinUhlenbeck, RelativeShotNoise, ShotNoise
import bluecellulab.stimuli as stimuli
from bluecellulab.exceptions import BluecellulabError
Expand All @@ -54,6 +55,7 @@
class SSim:
"""Class that loads a circuit simulation to do cell simulations."""

@load_hoc_and_mod_files
def __init__(
self,
simulation_config: str | Path | SimulationConfig,
Expand Down
11 changes: 11 additions & 0 deletions bluecellulab/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Utility functions."""


def run_once(func):
"""A decorator to ensure a function is only called once."""
def wrapper(*args, **kwargs):
if not wrapper.has_run:
wrapper.has_run = True
return func(*args, **kwargs)
wrapper.has_run = False
return wrapper
15 changes: 15 additions & 0 deletions tests/test_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,18 @@ def test_print_header(caplog):

assert "Imported NEURON from: /path/to/neuron" in caplog.text
assert "Mod lib: /path/to/mod_lib" in caplog.text


def test_print_header_with_decorator(caplog):
"""Ensure the decorator loading hoc and mod files work as expected."""
with caplog.at_level(logging.INFO):
@importer.load_hoc_and_mod_files
def x():
pass

x() # call 3 times to ensure the decorator is called only once
x()
x()

assert caplog.text.count("Loading the mod files.") == 1
assert caplog.text.count("Loading the hoc files.") == 1
2 changes: 2 additions & 0 deletions tests/test_neuron_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@

import bluecellulab
from bluecellulab.exceptions import NeuronEvalError
from bluecellulab.importer import _load_hoc_and_mod_files
from bluecellulab.neuron_interpreter import eval_neuron

script_dir = os.path.dirname(__file__)


def test_eval_neuron():
"""Unit test for the eval_neuron function."""
_load_hoc_and_mod_files()
eval_neuron("neuron.h.nil", neuron=neuron)
with raises(NeuronEvalError):
eval_neuron("1+1")
Expand Down
2 changes: 2 additions & 0 deletions tests/test_simulation/test_neuron_globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import neuron

from bluecellulab.circuit.config.sections import ConditionEntry, Conditions, MechanismConditions
from bluecellulab.importer import _load_hoc_and_mod_files
from bluecellulab.simulation.neuron_globals import set_global_condition_parameters, set_init_depleted_values, set_minis_single_vesicle_values, set_tstop_value


def test_set_tstop_value():
_load_hoc_and_mod_files() # this is a run_once function
set_tstop_value(100.0)
assert neuron.h.tstop == 100.0

Expand Down
26 changes: 26 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from bluecellulab.utils import run_once


# Decorated function for testing
@run_once
def increment_counter(counter):
counter[0] += 1
return "Executed"


def test_run_once_execution():
"""Test that the decorated function runs only once."""
counter = [0] # Using a list for mutability

assert increment_counter(counter) == "Executed"
increment_counter(counter)
assert counter[0] == 1

# Called 3 times but increased once
increment_counter(counter)
increment_counter(counter)
increment_counter(counter)

assert counter[0] == 1

assert increment_counter(counter) is None
Loading