From c0f76f4a2468b64e0288b458bf76aab07a6543c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= Date: Tue, 11 Mar 2025 15:11:12 +0100 Subject: [PATCH 1/3] adding simulatable neuron class and registering function --- .../access_point/forge_access_point.py | 9 ++- bluepyemodel/access_point/nexus.py | 36 ++++++++++++ .../emodel_pipeline/simulatable_neuron.py | 57 +++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 bluepyemodel/emodel_pipeline/simulatable_neuron.py diff --git a/bluepyemodel/access_point/forge_access_point.py b/bluepyemodel/access_point/forge_access_point.py index 2a4df26..4030361 100644 --- a/bluepyemodel/access_point/forge_access_point.py +++ b/bluepyemodel/access_point/forge_access_point.py @@ -37,6 +37,7 @@ from bluepyemodel.emodel_pipeline.emodel_settings import EModelPipelineSettings from bluepyemodel.emodel_pipeline.emodel_workflow import EModelWorkflow from bluepyemodel.emodel_pipeline.memodel import MEModel +from bluepyemodel.emodel_pipeline.simulatableneuron import SimulatableNeuron from bluepyemodel.evaluation.fitness_calculator_configuration import FitnessCalculatorConfiguration from bluepyemodel.model.distribution_configuration import DistributionConfiguration from bluepyemodel.model.neuron_model_configuration import NeuronModelConfiguration @@ -57,6 +58,7 @@ "EModelWorkflow": "EModelWorkflow", "EModelScript": "EModelScript", "MEModel": "MEModel", + "SimulatableNeuron": "SimulatableNeuron", } CLASS_TO_RESOURCE_NAME = { @@ -69,6 +71,7 @@ "EModelWorkflow": "EMW", "EModelScript": "EMS", "MEModel": "MEM", + "SimulatableNeuron": "SN" } NEXUS_TYPE_TO_CLASS = { @@ -81,6 +84,7 @@ "EModelWorkflow": EModelWorkflow, "EModelScript": EModelScript, "MEModel": MEModel, + "SimulatableNeuron": SimulatableNeuron } NEXUS_ENTRIES = [ @@ -437,7 +441,10 @@ def register( # when EModelWorkflow resource is complete if type_ == "EModelWorkflow": type_ = "Entity" - schema_id = self.forge._model.schema_id(type_) + if type_ == "SimulatableNeuron": + schema_id = None + else: + schema_id = self.forge._model.schema_id(type_) self.forge.register(resource, schema_id=schema_id) diff --git a/bluepyemodel/access_point/nexus.py b/bluepyemodel/access_point/nexus.py index a1e2dca..627d828 100755 --- a/bluepyemodel/access_point/nexus.py +++ b/bluepyemodel/access_point/nexus.py @@ -1486,3 +1486,39 @@ def sonata_exists(self, seed): return True except AccessPointException: return False + + def store_simulatable_neuron( + self, simulatable_neuron, is_analysis_suitable=False + ): + """Store a BPEM object on Nexus + + Args: + simulatable_neuron (SimulatableNeuron) + is_analysis_suitable (bool): Should be True only when managing metatada for resources + of type EModel, for which all data are complete (has FCC, ETC, EMC, etc.). + """ + + metadata_dict = self.emodel_metadata_ontology.for_resource( + is_analysis_suitable=is_analysis_suitable + ) + + type_ = "SimulatableNeuron" + + base_payload = simulatable_neuron.as_dict() + base_payload["type"] = ["Entity", type_] + if "subject" in metadata_dict: + base_payload["subject"] = metadata_dict["subject"] + if "brainLocation" in metadata_dict: + base_payload["brainLocation"] = metadata_dict["brainLocation"] + if "annotation" in metadata_dict: + base_payload["annotation"] = metadata_dict["annotation"] + + self.forge.register( + base_payload, + filters_existence=None, + legacy_filters_existence=None, + replace=False, + distributions=None, + images=None, + type_=type_, + ) diff --git a/bluepyemodel/emodel_pipeline/simulatable_neuron.py b/bluepyemodel/emodel_pipeline/simulatable_neuron.py new file mode 100644 index 0000000..37992c0 --- /dev/null +++ b/bluepyemodel/emodel_pipeline/simulatable_neuron.py @@ -0,0 +1,57 @@ +""" +Copyright 2025 Open Brain Institute + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + + +class SimulatableNeuron: + """Whole neuron model, including links to morphology, ion channels models and hoc.""" + + def __init__( + self, + description, + emodel_script_id, + mechanism_ids, + morphology_id, + holding_current=None, + threshold_current=None, + validated=False, + ): + """Init + + from_circuit and synaptomes, that might be present in the database, + are not implemented here. + name, eModel, eType, subject, brainRegion are automatically filled in by forge_access_point for now using emodel_metadata + + Args: + description (str): description of the model + emodel_script_id (str): ID of the hoc model in the database + mechanism_ids (list of str): IDs of the ion channel models in the database + morphology_id (str): ID of the morphology in the database + holding_current (float): holding current to use in protocols (in nA) + threshold_current (float): current at which the cell starts firing (in nA) + validated (bool): whether the model has been validated by user + """ + # check if brain region and subject are automatically added + self.description = description + self.emodel_script_id = emodel_script_id + self.mechanism_ids = mechanism_ids + self.morphology_id = morphology_id + self.holding_current = holding_current + self.threshold_current = threshold_current + self.validated = validated + + def as_dict(self): + """Used for the storage of the object""" + return vars(self) From 14fa2fbec781e9727d60e36fac4fee151cb3afc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= Date: Tue, 11 Mar 2025 15:31:27 +0100 Subject: [PATCH 2/3] minor fixes --- .../access_point/forge_access_point.py | 11 ++++---- bluepyemodel/access_point/nexus.py | 8 +++--- .../emodel_pipeline/simulatable_neuron.py | 27 +++++++++++-------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/bluepyemodel/access_point/forge_access_point.py b/bluepyemodel/access_point/forge_access_point.py index 4030361..72bfb86 100644 --- a/bluepyemodel/access_point/forge_access_point.py +++ b/bluepyemodel/access_point/forge_access_point.py @@ -37,7 +37,7 @@ from bluepyemodel.emodel_pipeline.emodel_settings import EModelPipelineSettings from bluepyemodel.emodel_pipeline.emodel_workflow import EModelWorkflow from bluepyemodel.emodel_pipeline.memodel import MEModel -from bluepyemodel.emodel_pipeline.simulatableneuron import SimulatableNeuron +from bluepyemodel.emodel_pipeline.simulatable_neuron import SimulatableNeuron from bluepyemodel.evaluation.fitness_calculator_configuration import FitnessCalculatorConfiguration from bluepyemodel.model.distribution_configuration import DistributionConfiguration from bluepyemodel.model.neuron_model_configuration import NeuronModelConfiguration @@ -71,7 +71,7 @@ "EModelWorkflow": "EMW", "EModelScript": "EMS", "MEModel": "MEM", - "SimulatableNeuron": "SN" + "SimulatableNeuron": "SN", } NEXUS_TYPE_TO_CLASS = { @@ -84,7 +84,7 @@ "EModelWorkflow": EModelWorkflow, "EModelScript": EModelScript, "MEModel": MEModel, - "SimulatableNeuron": SimulatableNeuron + "SimulatableNeuron": SimulatableNeuron, } NEXUS_ENTRIES = [ @@ -441,9 +441,8 @@ def register( # when EModelWorkflow resource is complete if type_ == "EModelWorkflow": type_ = "Entity" - if type_ == "SimulatableNeuron": - schema_id = None - else: + schema_id = None + if type_ != "SimulatableNeuron": schema_id = self.forge._model.schema_id(type_) self.forge.register(resource, schema_id=schema_id) diff --git a/bluepyemodel/access_point/nexus.py b/bluepyemodel/access_point/nexus.py index 627d828..c92bbce 100755 --- a/bluepyemodel/access_point/nexus.py +++ b/bluepyemodel/access_point/nexus.py @@ -1487,11 +1487,9 @@ def sonata_exists(self, seed): except AccessPointException: return False - def store_simulatable_neuron( - self, simulatable_neuron, is_analysis_suitable=False - ): + def store_simulatable_neuron(self, simulatable_neuron, is_analysis_suitable=False): """Store a BPEM object on Nexus - + Args: simulatable_neuron (SimulatableNeuron) is_analysis_suitable (bool): Should be True only when managing metatada for resources @@ -1513,7 +1511,7 @@ def store_simulatable_neuron( if "annotation" in metadata_dict: base_payload["annotation"] = metadata_dict["annotation"] - self.forge.register( + self.access_point.register( base_payload, filters_existence=None, legacy_filters_existence=None, diff --git a/bluepyemodel/emodel_pipeline/simulatable_neuron.py b/bluepyemodel/emodel_pipeline/simulatable_neuron.py index 37992c0..8f03c3c 100644 --- a/bluepyemodel/emodel_pipeline/simulatable_neuron.py +++ b/bluepyemodel/emodel_pipeline/simulatable_neuron.py @@ -1,3 +1,5 @@ +"""SimulatableNeuron class""" + """ Copyright 2025 Open Brain Institute @@ -19,22 +21,24 @@ class SimulatableNeuron: """Whole neuron model, including links to morphology, ion channels models and hoc.""" def __init__( - self, - description, - emodel_script_id, - mechanism_ids, - morphology_id, - holding_current=None, - threshold_current=None, - validated=False, - ): + self, + name, + description, + emodel_script_id, + mechanism_ids, + morphology_id, + holding_current=None, + threshold_current=None, + validated=False, + ): """Init from_circuit and synaptomes, that might be present in the database, are not implemented here. - name, eModel, eType, subject, brainRegion are automatically filled in by forge_access_point for now using emodel_metadata - + subject, brainRegion are coming emodel_metadata + Args: + name (str): name description (str): description of the model emodel_script_id (str): ID of the hoc model in the database mechanism_ids (list of str): IDs of the ion channel models in the database @@ -44,6 +48,7 @@ def __init__( validated (bool): whether the model has been validated by user """ # check if brain region and subject are automatically added + self.name = name self.description = description self.emodel_script_id = emodel_script_id self.mechanism_ids = mechanism_ids From 95019379058d2e02e5f2d244a067f1162ef992b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= Date: Wed, 12 Mar 2025 11:05:01 +0100 Subject: [PATCH 3/3] link simulatable neuron to emodel workflow --- bluepyemodel/access_point/nexus.py | 17 +++++++++++++++++ bluepyemodel/emodel_pipeline/emodel_workflow.py | 15 +++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/bluepyemodel/access_point/nexus.py b/bluepyemodel/access_point/nexus.py index c92bbce..4ab80fe 100755 --- a/bluepyemodel/access_point/nexus.py +++ b/bluepyemodel/access_point/nexus.py @@ -1520,3 +1520,20 @@ def store_simulatable_neuron(self, simulatable_neuron, is_analysis_suitable=Fals images=None, type_=type_, ) + + # update EMW if any + workflow, workflow_id = self.get_emodel_workflow() + + # wait for the object to be uploaded and fetchable + if workflow is not None and workflow_id is not None: + time.sleep(self.sleep_time) + + # fetch just uploaded simulatable neuron resource to get its id + type_ = "SimulatableNeuron" + filters = {"type": type_, **simulatable_neuron.as_dict()} + resource = self.access_point.fetch_one(filters, strict=True) + simulatable_neuron_id = resource.id + workflow.add_simulatable_neuron_id(simulatable_neuron_id) + + time.sleep(self.sleep_time) + self.store_or_update_emodel_workflow(workflow) diff --git a/bluepyemodel/emodel_pipeline/emodel_workflow.py b/bluepyemodel/emodel_pipeline/emodel_workflow.py index 017e867..9dd6e3d 100644 --- a/bluepyemodel/emodel_pipeline/emodel_workflow.py +++ b/bluepyemodel/emodel_pipeline/emodel_workflow.py @@ -32,6 +32,7 @@ def __init__( fitness_configuration_id=None, emodels=None, emodel_scripts_id=None, + simulatable_neuron_ids=None, state="not launched", ): """Init @@ -42,6 +43,8 @@ def __init__( emodel_configuration (str): NeuronModelConfiguration id fitness_configuration_id (str): FitnessCalculatorConfiguration id emodels (list): list of EModel ids + emodel_scripts_id (list): list of EModelScript ids + simulatable_neuron_ids (list): list of SimulatableNeuron ids state (str): can be "not launched", "running" or "done" """ self.targets_configuration_id = targets_configuration_id @@ -50,6 +53,7 @@ def __init__( self.fitness_configuration_id = fitness_configuration_id self.emodels = emodels if emodels else [] self.emodel_scripts_id = emodel_scripts_id if emodel_scripts_id else [] + self.simulatable_neuron_ids = simulatable_neuron_ids if simulatable_neuron_ids else [] self.state = state def add_emodel_id(self, emodel_id): @@ -57,9 +61,13 @@ def add_emodel_id(self, emodel_id): self.emodels.append(emodel_id) def add_emodel_script_id(self, emodel_script_id): - """Add an emodel id to the list of emodels""" + """Add an emodel script id to the list of emodel scripts""" self.emodel_scripts_id.append(emodel_script_id) + def add_simulatable_neuron_id(self, simulatable_neuron_id): + """Add an simulatable neuron id to the list of simulatable neurons""" + self.emodel_scripts_id.append(simulatable_neuron_id) + def get_configuration_ids(self): """Return all configuration id parameters""" ids = ( @@ -75,7 +83,10 @@ def get_configuration_ids(self): def get_related_nexus_ids(self): emodels_ids = [{"id": id_, "type": "EModel"} for id_ in self.emodels] emodel_scripts_ids = [{"id": id_, "type": "EModelScript"} for id_ in self.emodel_scripts_id] - generates = emodels_ids + emodel_scripts_ids + simulatable_neuron_ids = [ + {"id": id_, "type": "SimulatableNeuron"} for id_ in self.simulatable_neuron_ids + ] + generates = emodels_ids + emodel_scripts_ids + simulatable_neuron_ids if self.fitness_configuration_id: generates.append( {