Skip to content

Commit

Permalink
Merge pull request #111 from GianlucaFicarelli/local_access_point_fro…
Browse files Browse the repository at this point in the history
…m_nexus_data

Allow LocalAccessPoint to load EModels from Nexus staged data
  • Loading branch information
AurelienJaquier authored Mar 4, 2024
2 parents 472a981 + ad6594a commit 8eb60d5
Show file tree
Hide file tree
Showing 15 changed files with 2,429 additions and 55 deletions.
127 changes: 82 additions & 45 deletions bluepyemodel/access_point/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import glob
import json
import logging
from functools import cached_property
from itertools import chain
from pathlib import Path

Expand Down Expand Up @@ -50,6 +51,8 @@
"myelinated": "myelin",
}

SUPPORTED_MORPHOLOGY_EXTENSIONS = (".asc", ".swc")


class LocalAccessPoint(DataAccessPoint):
"""Access point to access configuration files and e-models when stored locally."""
Expand Down Expand Up @@ -124,8 +127,6 @@ def __init__(
self.legacy_dir_structure = legacy_dir_structure
self.with_seeds = with_seeds

self.morph_path = None

if final_path is None:
self.final_path = self.emodel_dir / "final.json"
else:
Expand All @@ -141,6 +142,43 @@ def __init__(
self.pipeline_settings = self.load_pipeline_settings()
self.unfrozen_params = None

@cached_property
def morph_dir(self):
"""Return the morphology directory as read from the recipes, or fallback to 'morphology'."""
recipes = self.get_recipes()
return Path(self.emodel_dir, recipes.get("morph_path", "morphology"))

@cached_property
def morph_path(self):
"""Return the path to the morphology file as read from the recipes."""
recipes = self.get_recipes()

morph_file = None
if isinstance(recipes["morphology"], str):
morph_file = recipes["morphology"]
else:
for _, morph_file in recipes["morphology"]:
if morph_file.endswith(SUPPORTED_MORPHOLOGY_EXTENSIONS):
break

if not morph_file or not morph_file.endswith(SUPPORTED_MORPHOLOGY_EXTENSIONS):
raise FileNotFoundError(f"Morphology file not defined or not supported: {morph_file}")

morph_path = self.morph_dir / morph_file

if not morph_path.is_file():
raise FileNotFoundError(f"Morphology file not found: {morph_path}")
if str(Path.cwd()) not in str(morph_path.resolve()) and self.emodel_metadata.iteration:
raise FileNotFoundError(
"When using a githash or iteration tag, the path to the morphology must be local"
" otherwise it cannot be archived during the creation of the githash. To solve"
" this issue, you can copy the morphology from "
f"{morph_path.resolve()} to {Path.cwd() / 'morphologies'} and update your "
"recipes."
)

return morph_path

def set_emodel(self, emodel):
"""Setter for the name of the emodel, check it exists (with or without seed) in recipe."""
_emodel = "_".join(emodel.split("_")[:2]) if self.with_seeds else emodel
Expand All @@ -154,14 +192,40 @@ def set_emodel(self, emodel):

def load_pipeline_settings(self):
""" """

settings = self.get_recipes().get("pipeline_settings", {})

recipes = self.get_recipes()
settings = recipes.get("pipeline_settings", {})
if isinstance(settings, str):
# read the pipeline settings from file
settings = self.get_json("pipeline_settings")
if "morph_modifiers" not in settings:
settings["morph_modifiers"] = self.get_recipes().get("morph_modifiers", None)

settings["morph_modifiers"] = recipes.get("morph_modifiers", None)
return EModelPipelineSettings(**settings)

def _config_to_final(self, config):
"""Convert the configuration stored in EM_*.json to the format used for final.json."""
return {
self.emodel_metadata.emodel: {
**vars(self.emodel_metadata),
"score": config["fitness"], # float
"parameters": config["parameter"], # list[dict]
"fitness": config["score"], # list[dict]
"features": config["features"], # list[dict]
"validation_fitness": config["scoreValidation"], # list[dict]
"validated": config["passedValidation"], # bool
"seed": config["seed"], # int
}
}

def get_final_content(self, lock_file=True):
"""Return the final content from recipes if available, or fallback to final.json"""
recipes = self.get_recipes()
if "final" in recipes:
if self.final_path and self.final_path.is_file():
logger.warning("Ignored %s, using file from recipes", self.final_path)
data = self.get_json("final")
return self._config_to_final(data)
return self.get_final(lock_file=lock_file)

def get_final(self, lock_file=True):
"""Get emodel dictionary from final.json."""
if self.final_path is None:
Expand Down Expand Up @@ -380,18 +444,13 @@ def get_available_mechanisms(self):

def get_available_morphologies(self):
"""Get the list of names of available morphologies"""

names = []

morph_dir = self.emodel_dir / "morphology"
morph_dir = self.morph_dir

if not morph_dir.is_dir():
return None

for morph_file in glob.glob(str(morph_dir / "*.asc")) + glob.glob(str(morph_dir / "*.swc")):
names.append(Path(morph_file).stem)

return set(names)
patterns = ["*" + ext for ext in SUPPORTED_MORPHOLOGY_EXTENSIONS]
return {morph_file.stem for pattern in patterns for morph_file in morph_dir.glob(pattern)}

def get_model_configuration(self):
"""Get the configuration of the model, including parameters, mechanisms and distributions"""
Expand All @@ -415,7 +474,7 @@ def get_model_configuration(self):
if isinstance(parameters["mechanisms"], dict):
configuration.init_from_legacy_dict(parameters, self.get_morphologies())
else:
configuration.init_from_dict(parameters)
configuration.init_from_dict(parameters, self.get_morphologies())

configuration.mapping_multilocation = self.get_recipes().get("multiloc_map", None)

Expand Down Expand Up @@ -555,28 +614,6 @@ def get_morphologies(self):
"""

recipes = self.get_recipes()

if isinstance(recipes["morphology"], str):
morph_file = recipes["morphology"]
else:
morph_file = recipes["morphology"][0][1]

if self.morph_path is None:
self.morph_path = Path(recipes["morph_path"]) / morph_file
if not self.morph_path.is_absolute():
self.morph_path = Path(self.emodel_dir) / self.morph_path
else:
self.morph_path = Path(self.morph_path)

if str(Path.cwd()) not in str(self.morph_path.resolve()) and self.emodel_metadata.iteration:
raise FileNotFoundError(
"When using a githash or iteration tag, the path to the morphology must be local"
" otherwise it cannot be archived during the creation of the githash. To solve"
" this issue, you can copy the morphology from "
f"{self.morph_path.resolve()} to {Path.cwd() / 'morphologies'} and update your "
"recipes."
)

morphology_definition = {
"name": self.morph_path.stem,
"path": str(self.morph_path),
Expand Down Expand Up @@ -628,7 +665,7 @@ def format_emodel_data(self, model_data):
def get_emodel(self, lock_file=True):
"""Get dict with parameter of single emodel (including seed if any)"""

final = self.get_final(lock_file=lock_file)
final = self.get_final_content(lock_file=lock_file)

if self.emodel_metadata.emodel in final:
return self.format_emodel_data(final[self.emodel_metadata.emodel])
Expand All @@ -648,7 +685,7 @@ def get_emodels(self, emodels=None):
emodels = [self.emodel_metadata.emodel]

models = []
for mod_data in self.get_final().values():
for mod_data in self.get_final_content().values():
if mod_data["emodel"] in emodels:
models.append(self.format_emodel_data(mod_data))

Expand Down Expand Up @@ -692,7 +729,7 @@ def has_model_configuration(self):
return Path(recipes["params"]).is_file()

def get_emodel_etype_map(self):
final = self.get_final()
final = self.get_final_content()
return {emodel: emodel.split("_")[0] for emodel in final}

def get_emodel_names(self):
Expand All @@ -702,14 +739,14 @@ def get_emodel_names(self):
dict: keys are emodel names with seed, values are names without seed.
"""

final = self.get_final()
final = self.get_final_content()

return {mod_name: mod.get("emodel", mod_name) for mod_name, mod in final.items()}

def has_best_model(self, seed):
"""Check if the best model has been stored."""

final = self.get_final()
final = self.get_final_content()

model_name = self.get_model_name_for_final(seed)

Expand All @@ -718,7 +755,7 @@ def has_best_model(self, seed):
def is_checked_by_validation(self, seed):
"""Check if the emodel with a given seed has been checked by Validation task."""

final = self.get_final()
final = self.get_final_content()

model_name = self.get_model_name_for_final(seed)

Expand All @@ -732,7 +769,7 @@ def is_validated(self):
"""Check if enough models have been validated."""

n_validated = 0
final = self.get_final()
final = self.get_final_content()

for _, entry in final.items():
if (
Expand Down
11 changes: 11 additions & 0 deletions bluepyemodel/evaluation/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,3 +914,14 @@ def run(self, cell_model, param_values, sim=None, isolate=None, timeout=None):

cell_model.unfreeze(param_values.keys())
return responses

def __str__(self):
"""String representation"""

content = f"Sequence protocol {self.name}:\n"

content += f"{len(self.protocols)} subprotocols:\n"
for protocol in self.protocols:
content += f"{protocol}\n"

return content
12 changes: 10 additions & 2 deletions bluepyemodel/model/neuron_model_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,17 @@ def _format_locations(locations):

return locations

def init_from_dict(self, configuration_dict, auto_mechanism=False):
def init_from_dict(self, configuration_dict, morphology, auto_mechanism=False):
"""Instantiate the object from its dictionary form"""

if "distributions" in configuration_dict:
# empty the list of distributions
if self.distributions and configuration_dict["distributions"]:
# remove default uniform distribution if added at
# https://github.com/BlueBrain/BluePyEModel/blob/
# 15b8dd2824453a8bf097b2ede13dd7ecf5d07d05/bluepyemodel/access_point/local.py#L399
logger.warning("Removing %s pre-existing distribution(s)", len(self.distributions))
self.distributions = []
for distribution in configuration_dict["distributions"]:
self.add_distribution(
distribution["name"],
Expand Down Expand Up @@ -206,7 +213,8 @@ def init_from_dict(self, configuration_dict, auto_mechanism=False):
mechanism.get("ljp_corrected", None),
)

self.morphology = MorphologyConfiguration(**configuration_dict["morphology"])
morphology_params = {**configuration_dict["morphology"], **morphology}
self.morphology = MorphologyConfiguration(**morphology_params)

def init_from_legacy_dict(self, parameters, morphology):
"""Instantiate the object from its legacy dictionary form"""
Expand Down
23 changes: 18 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
from tests.utils import DATA, cwd


@pytest.fixture(scope="session")
def nrnivmodl(tmp_path_factory):
"""Compile the mechanisms only once per session."""
path = tmp_path_factory.mktemp("nrnivmodl_dir")
with cwd(path):
os.popen(f"nrnivmodl {DATA}/mechanisms").read()
return path


@pytest.fixture
def workspace(tmp_path):
"""Change the working directory to tmp_path.
Expand All @@ -35,7 +44,7 @@ def workspace(tmp_path):


@pytest.fixture
def emodel_dir(workspace):
def emodel_dir(workspace, nrnivmodl):
"""Copy the required files to workspace/emodel."""
dirs = ["config", "mechanisms", "morphology", "ephys_data"]
files = ["final.json"]
Expand All @@ -44,6 +53,7 @@ def emodel_dir(workspace):
shutil.copytree(DATA / name, dst / name)
for name in files:
shutil.copyfile(DATA / name, dst / name)
shutil.copytree(nrnivmodl / "x86_64", workspace / "x86_64")
yield dst


Expand Down Expand Up @@ -72,7 +82,10 @@ def db_restart(emodel_dir):


@pytest.fixture
def evaluator(db, emodel_dir):
os.popen(f"nrnivmodl {emodel_dir}/mechanisms").read()
db.get_mechanisms_directory = lambda: None
return get_evaluator_from_access_point(access_point=db)
def db_from_nexus(emodel_dir):
return get_access_point(
"local",
emodel="L5PC",
emodel_dir=emodel_dir,
recipes_path=emodel_dir / "config/recipes_nexus.json",
)
4 changes: 3 additions & 1 deletion tests/functional_tests/test_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
from numpy.testing import assert_allclose
from pandas.testing import assert_frame_equal

from bluepyemodel.evaluation.evaluation import get_evaluator_from_access_point

def test_protocols(db, evaluator, tmp_path):

def test_protocols(db, tmp_path):
logging.basicConfig(level=logging.DEBUG)
logging.getLogger().setLevel(logging.DEBUG)

params = db.get_emodel().parameters
evaluator = get_evaluator_from_access_point(access_point=db)

responses = evaluator.run_protocols(
protocols=evaluator.fitness_protocols.values(), param_values=params
Expand Down
Loading

0 comments on commit 8eb60d5

Please sign in to comment.