diff --git a/.gitignore b/.gitignore index 293ef8fe..805b0ccd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ nrnivmodl.log .ruff_cache .pytest_cache *.btr +*.whl diff --git a/bluecellulab/cell/core.py b/bluecellulab/cell/core.py index af5b1c80..194c0e50 100644 --- a/bluecellulab/cell/core.py +++ b/bluecellulab/cell/core.py @@ -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 @@ -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, @@ -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 diff --git a/bluecellulab/importer.py b/bluecellulab/importer.py index 18a65d51..2f76a61c 100644 --- a/bluecellulab/importer.py +++ b/bluecellulab/importer.py @@ -21,6 +21,7 @@ import neuron from bluecellulab.exceptions import BluecellulabError +from bluecellulab.utils import run_once logger = logging.getLogger(__name__) @@ -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 diff --git a/bluecellulab/rngsettings.py b/bluecellulab/rngsettings.py index ccfb90b1..fe4925b8 100644 --- a/bluecellulab/rngsettings.py +++ b/bluecellulab/rngsettings.py @@ -21,6 +21,7 @@ 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__) @@ -28,6 +29,7 @@ class RNGSettings(metaclass=Singleton): """Class that represents RNG settings in bluecellulab.""" + @load_hoc_and_mod_files def __init__( self, mode: Optional[str] = None, @@ -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: diff --git a/bluecellulab/ssim.py b/bluecellulab/ssim.py index 25fd0054..8a51342b 100644 --- a/bluecellulab/ssim.py +++ b/bluecellulab/ssim.py @@ -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 @@ -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, diff --git a/bluecellulab/utils.py b/bluecellulab/utils.py new file mode 100644 index 00000000..00cec6fd --- /dev/null +++ b/bluecellulab/utils.py @@ -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 diff --git a/tests/test_importer.py b/tests/test_importer.py index 21f88d6a..c9da6142 100644 --- a/tests/test_importer.py +++ b/tests/test_importer.py @@ -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 diff --git a/tests/test_neuron_interpreter.py b/tests/test_neuron_interpreter.py index a5a21407..5fd3872d 100644 --- a/tests/test_neuron_interpreter.py +++ b/tests/test_neuron_interpreter.py @@ -8,6 +8,7 @@ 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__) @@ -15,6 +16,7 @@ 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") diff --git a/tests/test_simulation/test_neuron_globals.py b/tests/test_simulation/test_neuron_globals.py index 2b90ca1c..a9310faf 100644 --- a/tests/test_simulation/test_neuron_globals.py +++ b/tests/test_simulation/test_neuron_globals.py @@ -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 diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..d3dc58ec --- /dev/null +++ b/tests/test_utils.py @@ -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