diff --git a/bluepyemodel/access_point/forge_access_point.py b/bluepyemodel/access_point/forge_access_point.py index 2a4df26..72bfb86 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.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 @@ -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,9 @@ def register( # when EModelWorkflow resource is complete if type_ == "EModelWorkflow": type_ = "Entity" - schema_id = self.forge._model.schema_id(type_) + 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 a1e2dca..4ab80fe 100755 --- a/bluepyemodel/access_point/nexus.py +++ b/bluepyemodel/access_point/nexus.py @@ -1486,3 +1486,54 @@ 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.access_point.register( + base_payload, + filters_existence=None, + legacy_filters_existence=None, + replace=False, + distributions=None, + 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( { diff --git a/bluepyemodel/emodel_pipeline/simulatable_neuron.py b/bluepyemodel/emodel_pipeline/simulatable_neuron.py new file mode 100644 index 0000000..8f03c3c --- /dev/null +++ b/bluepyemodel/emodel_pipeline/simulatable_neuron.py @@ -0,0 +1,62 @@ +"""SimulatableNeuron class""" + +""" +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, + 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. + 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 + 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.name = name + 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)