diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index 5973f97..5fa4e1b 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -13,9 +13,6 @@ from .generatedscollector import GdsCollector -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - class GeneratedsSuperSuper(object): """Super class for GeneratedsSuper. @@ -23,6 +20,9 @@ class GeneratedsSuperSuper(object): Any bits that must go into every libNeuroML class should go here. """ + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): """Generic function to allow easy addition of a new member to a NeuroML object. Without arguments, when `obj=None`, it simply calls the `info()` method @@ -37,7 +37,14 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): calling (parent) component type object. :param obj: member object or class type (neuroml.NeuroMLDocument) or - name of class type ("NeuroMLDocument"), or None + name of class type ("NeuroMLDocument"), a string, or None + + If the string that is passed is not the name of a NeuroML + component type, it will be added as a string if the parent + component type allows "any" elements. This does not mean that one + can add anything, since it must still be valid XML/LEMS. The + only usecase for this currently is to add RDF strings to the + Annotation component type. :type obj: str or type or None :param hint: member name to add to when there are multiple members that `obj` can be added to :type hint: string @@ -58,6 +65,24 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): if type(obj) is type or isinstance(obj, str): obj = self.component_factory(obj, validate=validate, **kwargs) + # if obj is still a string, it is a general string, but to confirm that + # this is what the user intends, ask them to provide a hint. + if isinstance(obj, str): + # no hint, no pass + if not hint: + e = Exception( + "Received a text object to add. " + + 'Please pass `hint="__ANY__"` to confirm that this is what you intend. ' + + "I will then try to add this to an __ANY__ member in the object." + ) + raise e + # hint confirmed, try to add it below + else: + self.logger.warning("Received a text object to add.") + self.logger.warning( + "Trying to add this to an __ANY__ member in the object." + ) + # getattr only returns the value of the provided member but one cannot # then use this to modify the member. Using `vars` also allows us to # modify the value @@ -65,13 +90,19 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): all_members = self._get_members() for member in all_members: # get_data_type() returns the type as a string, e.g.: 'IncludeType' + + # handle "__ANY__" + if isinstance(obj, str) and member.get_data_type() == "__ANY__": + targets.append(member) + break + if member.get_data_type() == type(obj).__name__: targets.append(member) if len(targets) == 0: # no targets found e = Exception( - """A member object of {} type could not be found in NeuroML class {}.\n{} + """A member object of '{}' type could not be found in NeuroML class {}.\n{} """.format(type(obj).__name__, type(self).__name__, self.info()) ) raise e @@ -97,7 +128,14 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): if neuroml.build_time_validation.ENABLED and validate: self.validate() else: - logger.warning("Build time validation is disabled.") + if not neuroml.build_time_validation.ENABLED: + self.logger.debug( + f"Build time validation is globally disabled: adding new {obj.__class__.__name__}." + ) + else: + self.logger.debug( + f"Build time validation is locally disabled: adding new {obj.__class__.__name__}." + ) return obj @classmethod @@ -128,12 +166,16 @@ def component_factory(cls, component_type, validate=True, **kwargs): Note that when providing the class type, one will need to import it, e.g.: `import NeuroMLDocument`, to ensure that it is defined, whereas this will not be required when using the string. + + If the value passed for component_type is a general string (with + spaces), it is simply returned :type component_type: str/type :param validate: toggle validation (default: True) :type validate: bool :param kwargs: named arguments to be passed to ComponentType constructor :type kwargs: named arguments - :returns: new Component (object) of provided ComponentType + :returns: new Component (object) of provided ComponentType, or + unprocessed string with spaces if given for component_type :rtype: object :raises ValueError: if validation/checks fail @@ -141,12 +183,28 @@ def component_factory(cls, component_type, validate=True, **kwargs): module_object = sys.modules[cls.__module__] if isinstance(component_type, str): - comp_type_class = getattr(module_object, component_type) + # class names do not have spaces, so if we find a space, we treat + # it as a general string and just return it. + if " " in component_type: + cls.logger.warning( + "Component Type appears to be a generic string: nothing to do." + ) + return component_type + else: + comp_type_class = getattr(module_object, component_type) else: comp_type_class = getattr(module_object, component_type.__name__) comp = comp_type_class(**kwargs) + # handle component types that support __ANY__ + try: + anytypevalue = kwargs["__ANY__"] + # append value to anytypeobjs_ list + comp.anytypeobjs_.append(anytypevalue) + except KeyError: + pass + # additional setups where required if comp_type_class.__name__ == "Cell": comp.setup_nml_cell() @@ -156,7 +214,14 @@ def component_factory(cls, component_type, validate=True, **kwargs): if neuroml.build_time_validation.ENABLED and validate: comp.validate() else: - logger.warning("Build time validation is disabled.") + if not neuroml.build_time_validation.ENABLED: + cls.logger.debug( + f"Build time validation is globally disabled: creating new {comp_type_class.__name__}." + ) + else: + cls.logger.debug( + f"Build time validation is locally disabled: creating new {comp_type_class.__name__}." + ) return comp def __add(self, obj, member, force=False): @@ -170,38 +235,49 @@ def __add(self, obj, member, force=False): :type force: bool """ - import warnings - - # A single value, not a list: - if member.get_container() == 0: - if force: - vars(self)[member.get_name()] = obj - else: - if vars(self)[member.get_name()]: - warnings.warn( - """{} has already been assigned. Use `force=True` to overwrite. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( - member.get_name() - ) + # handle __ANY__ which is to be stored in anytypeobjs_ + if member.get_name() == "__ANY__": + self.logger.debug("__ANY__ member, appending to anytypeobjs_.") + vars(self)["anytypeobjs_"].append(obj) + else: + # A single value, not a list: + self.logger.debug("Not a container member, assigning.") + if member.get_container() == 0: + if force: + vars(self)[member.get_name()] = obj + self.logger.warning( + """Overwriting member '{}'.""".format(member.get_name()) ) else: - vars(self)[member.get_name()] = obj - # List - else: - if force: - vars(self)[member.get_name()].append(obj) - else: - # "obj in .." checks by identity and value. - # In XML, two children with same values are identical. - # There is no use case where the same child would be added - # twice to a component. - if obj in vars(self)[member.get_name()]: - warnings.warn( - """{} already exists in {}. Use `force=True` to force readdition. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( - obj, member.get_name() + if vars(self)[member.get_name()]: + self.logger.debug( + """Member '{}' has already been assigned. Use `force=True` to overwrite. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( + member.get_name() + ) ) + else: + vars(self)[member.get_name()] = obj + # List + else: + self.logger.debug("Container member, appending.") + if force: + vars(self)[member.get_name()].append(obj) + self.logger.warning( + """Force appending to member '{}'""".format(member.get_name()) ) else: - vars(self)[member.get_name()].append(obj) + # "obj in .." checks by identity and value. + # In XML, two children with same values are identical. + # There is no use case where the same child would be added + # twice to a component. + if obj in vars(self)[member.get_name()]: + self.logger.warning( + """{} already exists in {}. Use `force=True` to force readdition.""".format( + obj, member.get_name() + ) + ) + else: + vars(self)[member.get_name()].append(obj) @classmethod def _get_members(cls): @@ -616,3 +692,55 @@ def get_nml2_class_hierarchy(cls): schema = sys.modules[cls.__module__] cls.__nml_hier = schema.NeuroMLDocument.get_class_hierarchy() return cls.__nml_hier + + def get_by_id(self, id_): + """Get a component or attribute by its ID, or type, or attribute name + + :param id_: id of component ("biophys"), or its type + ("BiophysicalProperties"), or its attribute name + ("biophysical_properties") + + + When the attribute name or type are given, this simply returns the + associated object, which could be a list. + + :type id_: str + :returns: component if found, else None + """ + all_ids = [] + all_members = self._get_members() + for member in all_members: + # check name: biophysical_properties + if member.get_name() == id_: + return getattr(self, member.get_name()) + # check type: BiophysicalProperties + elif member.get_data_type() == id_: + return getattr(self, member.get_name()) + + # check contents + + # not a list + if member.get_container() == 0: + # check contents for id: biophysical_properties.id + contents = getattr(self, member.get_name(), None) + if hasattr(contents, "id"): + if contents.id == id_: + return contents + else: + all_ids.append(member.get_name()) + else: + # container: iterate over each item + contents = getattr(self, member.get_name(), []) + for m in contents: + if hasattr(m, "id"): + if m.id == id_: + return m + else: + all_ids.append(m.id) + + try: + self.logger.warning(f"Id '{id_}' not found in {self.__name__}") + except AttributeError: + self.logger.warning(f"Id '{id_}' not found in {self.id}") + self.logger.warning(f"All ids: {sorted(all_ids)}") + return None diff --git a/neuroml/nml/helper_methods.py b/neuroml/nml/helper_methods.py index 3948d6a..aa477af 100644 --- a/neuroml/nml/helper_methods.py +++ b/neuroml/nml/helper_methods.py @@ -877,39 +877,6 @@ def summary(self, show_includes=True, show_non_network=True): return info - warn_count = 0 - - def get_by_id(self, id: str) -> typing.Optional[typing.Any]: - """Get a component by specifying its ID. - - :param id: id of Component to get - :type id: str - :returns: Component with given ID or None if no Component with provided ID was found - """ - if len(id)==0: - callframe = inspect.getouterframes(inspect.currentframe(), 2) - print('Method: '+ callframe[1][3] + ' is asking for an element with no id...') - - return None - all_ids = [] - for ms in self.member_data_items_: - mlist = getattr(self, ms.get_name()) - # TODO: debug why this is required - if mlist is None: - continue - for m in mlist: - if hasattr(m,"id"): - if m.id == id: - return m - else: - all_ids.append(m.id) - if self.warn_count<10: - neuroml.print_("Id "+id+" not found in element. All ids: "+str(sorted(all_ids))) - self.warn_count+=1 - elif self.warn_count==10: - neuroml.print_(" - Suppressing further warnings about id not found...") - return None - def append(self, element): """Append an element @@ -924,47 +891,19 @@ def append(self, element): METHOD_SPECS += (nml_doc_summary,) -network_get_by_id = MethodSpec( - name="get_by_id", - source='''\ - - warn_count = 0 - def get_by_id(self, id: str) -> typing.Optional[typing.Any]: - """Get a component by its ID - - :param id: ID of component to find - :type id: str - :returns: component with specified ID or None if no component with specified ID found - """ - all_ids = [] - for ms in self.member_data_items_: - mlist = getattr(self, ms.get_name()) - # TODO: debug why this is required - if mlist is None: - continue - for m in mlist: - if hasattr(m,"id"): - if m.id == id: - return m - else: - all_ids.append(m.id) - if self.warn_count<10: - neuroml.print_("Id "+id+" not found in element. All ids: "+str(sorted(all_ids))) - self.warn_count+=1 - elif self.warn_count==10: - neuroml.print_(" - Suppressing further warnings about id not found...") - return None - +network_str = MethodSpec( + name="str", + source="""\ def __str__(self): return "Network "+str(self.id)+" with "+str(len(self.populations))+" population(s)" - ''', + """, class_names=("Network"), ) -METHOD_SPECS += (network_get_by_id,) +METHOD_SPECS += (network_str,) cell_methods = MethodSpec( @@ -1551,13 +1490,13 @@ def add_segment( else: p = None except IndexError as e: - print("{}: prox must be a list of 4 elements".format(e)) + raise ValueError("{}: prox must be a list of 4 elements".format(e)) try: d = self.component_factory( "Point3DWithDiam", x=dist[0], y=dist[1], z=dist[2], diameter=dist[3] ) except IndexError as e: - print("{}: dist must be a list of 4 elements".format(e)) + raise ValueError("{}: dist must be a list of 4 elements".format(e)) segid = len(self.morphology.segments) if segid > 0 and parent is None: @@ -1578,7 +1517,7 @@ def add_segment( seg = None seg = self.get_segment(seg_id) if seg: - raise ValueError(f"A segment with provided id {seg_id} already exists") + raise ValueError(f"A segment with provided id '{seg_id}' already exists") except ValueError: # a segment with this ID does not already exist pass @@ -1596,8 +1535,8 @@ def add_segment( try: seg_group = self.get_segment_group(group_id) except ValueError as e: - print("Warning: {}".format(e)) - print(f"Warning: creating Segment Group with id {group_id}") + self.logger.warning("{}".format(e)) + self.logger.warning(f"Creating Segment Group with id '{group_id}'") seg_group = self.add_segment_group( group_id=group_id ) @@ -1685,7 +1624,7 @@ def add_segment_group(self, group_id, neuro_lex_id=None, notes=None): notes=notes, validate=False ) else: - print(f"Warning: Segment group {seg_group.id} already exists.") + self.logger.warning(f"Segment group '{seg_group.id}' already exists.") return seg_group @@ -2024,6 +1963,8 @@ def setup_default_segment_groups(self, use_convention=True, default_groups=["all :type default_groups: list of strings :returns: list of created segment groups (or empty list if none created) :rtype: list + + :raises ValueError: if a group other than the standard groups are provided """ new_groups = [] if use_convention: @@ -2044,8 +1985,7 @@ def setup_default_segment_groups(self, use_convention=True, default_groups=["all neuro_lex_id=None notes="Default segment group for all segments in the cell" else: - print(f"Error: only 'all', 'soma_group', 'dendrite_group', and 'axon_group' are supported. Received {grp}") - return [] + raise ValueError(f"Only 'all', 'soma_group', 'dendrite_group', and 'axon_group' are supported. Received {grp}") seg_group = self.add_segment_group(group_id=grp, neuro_lex_id=neuro_lex_id, notes=notes) new_groups.append(seg_group) @@ -2214,7 +2154,7 @@ def __sectionise(self, root_segment_id, seg_group, morph_tree): :returns: TODO """ - # print(f"Processing element: {root_segment_id}") + self.logger.debug(f"Processing element: {root_segment_id}") try: children = morph_tree[root_segment_id] @@ -2281,7 +2221,7 @@ def get_segment_adjacency_list(self): child_lists[parent] = [] child_lists[parent].append(segment.id) except AttributeError: - print(f"Warning: Segment: {segment} has no parent") + self.logger.warning(f"Segment: {segment} has no parent") self.adjacency_list = child_lists return child_lists @@ -2388,7 +2328,7 @@ def get_segments_at_distance(self, distance, src_seg = 0): frac_along = ((distance - dist) / self.get_segment_length(tgt)) except ZeroDivisionError: # ignore zero length segments - print(f"Warning: encountered zero length segment: {tgt}") + self.logger.warning(f"Encountered zero length segment: {tgt}") continue if frac_along > 1.0: diff --git a/neuroml/nml/nml.py b/neuroml/nml/nml.py index 7c8834b..33447cd 100644 --- a/neuroml/nml/nml.py +++ b/neuroml/nml/nml.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -# Generated Wed Aug 14 13:56:38 2024 by generateDS.py version 2.44.1. +# Generated Tue Aug 20 10:23:13 2024 by generateDS.py version 2.44.1. # Python 3.11.9 (main, Apr 17 2024, 00:00:00) [GCC 14.0.1 20240411 (Red Hat 14.0.1-0)] # # Command line options: @@ -12468,39 +12468,6 @@ def _buildChildren( obj_.original_tagname_ = "inputList" super(Network, self)._buildChildren(child_, node, nodeName_, True) - warn_count = 0 - - def get_by_id(self, id: str) -> typing.Optional[typing.Any]: - """Get a component by its ID - - :param id: ID of component to find - :type id: str - :returns: component with specified ID or None if no component with specified ID found - """ - all_ids = [] - for ms in self.member_data_items_: - mlist = getattr(self, ms.get_name()) - # TODO: debug why this is required - if mlist is None: - continue - for m in mlist: - if hasattr(m, "id"): - if m.id == id: - return m - else: - all_ids.append(m.id) - if self.warn_count < 10: - neuroml.print_( - "Id " - + id - + " not found in element. All ids: " - + str(sorted(all_ids)) - ) - self.warn_count += 1 - elif self.warn_count == 10: - neuroml.print_(" - Suppressing further warnings about id not found...") - return None - def __str__(self): return ( "Network " @@ -42532,46 +42499,6 @@ def summary(self, show_includes=True, show_non_network=True): return info - warn_count = 0 - - def get_by_id(self, id: str) -> typing.Optional[typing.Any]: - """Get a component by specifying its ID. - - :param id: id of Component to get - :type id: str - :returns: Component with given ID or None if no Component with provided ID was found - """ - if len(id) == 0: - callframe = inspect.getouterframes(inspect.currentframe(), 2) - print( - "Method: " + callframe[1][3] + " is asking for an element with no id..." - ) - - return None - all_ids = [] - for ms in self.member_data_items_: - mlist = getattr(self, ms.get_name()) - # TODO: debug why this is required - if mlist is None: - continue - for m in mlist: - if hasattr(m, "id"): - if m.id == id: - return m - else: - all_ids.append(m.id) - if self.warn_count < 10: - neuroml.print_( - "Id " - + id - + " not found in element. All ids: " - + str(sorted(all_ids)) - ) - self.warn_count += 1 - elif self.warn_count == 10: - neuroml.print_(" - Suppressing further warnings about id not found...") - return None - def append(self, element): """Append an element @@ -49111,13 +49038,13 @@ def add_segment( else: p = None except IndexError as e: - print("{}: prox must be a list of 4 elements".format(e)) + raise ValueError("{}: prox must be a list of 4 elements".format(e)) try: d = self.component_factory( "Point3DWithDiam", x=dist[0], y=dist[1], z=dist[2], diameter=dist[3] ) except IndexError as e: - print("{}: dist must be a list of 4 elements".format(e)) + raise ValueError("{}: dist must be a list of 4 elements".format(e)) segid = len(self.morphology.segments) if segid > 0 and parent is None: @@ -49139,7 +49066,7 @@ def add_segment( seg = self.get_segment(seg_id) if seg: raise ValueError( - f"A segment with provided id {seg_id} already exists" + f"A segment with provided id '{seg_id}' already exists" ) except ValueError: # a segment with this ID does not already exist @@ -49160,8 +49087,8 @@ def add_segment( try: seg_group = self.get_segment_group(group_id) except ValueError as e: - print("Warning: {}".format(e)) - print(f"Warning: creating Segment Group with id {group_id}") + self.logger.warning("{}".format(e)) + self.logger.warning(f"Creating Segment Group with id '{group_id}'") seg_group = self.add_segment_group(group_id=group_id) seg_group.members.append(Member(segments=segment.id)) @@ -49255,7 +49182,7 @@ def add_segment_group(self, group_id, neuro_lex_id=None, notes=None): validate=False, ) else: - print(f"Warning: Segment group {seg_group.id} already exists.") + self.logger.warning(f"Segment group '{seg_group.id}' already exists.") return seg_group @@ -49589,6 +49516,8 @@ def setup_default_segment_groups( :type default_groups: list of strings :returns: list of created segment groups (or empty list if none created) :rtype: list + + :raises ValueError: if a group other than the standard groups are provided """ new_groups = [] if use_convention: @@ -49609,10 +49538,9 @@ def setup_default_segment_groups( neuro_lex_id = None notes = "Default segment group for all segments in the cell" else: - print( - f"Error: only 'all', 'soma_group', 'dendrite_group', and 'axon_group' are supported. Received {grp}" + raise ValueError( + f"Only 'all', 'soma_group', 'dendrite_group', and 'axon_group' are supported. Received {grp}" ) - return [] seg_group = self.add_segment_group( group_id=grp, neuro_lex_id=neuro_lex_id, notes=notes @@ -49799,7 +49727,7 @@ def __sectionise(self, root_segment_id, seg_group, morph_tree): :returns: TODO """ - # print(f"Processing element: {root_segment_id}") + self.logger.debug(f"Processing element: {root_segment_id}") try: children = morph_tree[root_segment_id] @@ -49865,7 +49793,7 @@ def get_segment_adjacency_list(self): child_lists[parent] = [] child_lists[parent].append(segment.id) except AttributeError: - print(f"Warning: Segment: {segment} has no parent") + self.logger.warning(f"Segment: {segment} has no parent") self.adjacency_list = child_lists return child_lists @@ -49973,7 +49901,7 @@ def get_segments_at_distance(self, distance, src_seg=0): frac_along = (distance - dist) / self.get_segment_length(tgt) except ZeroDivisionError: # ignore zero length segments - print(f"Warning: encountered zero length segment: {tgt}") + self.logger.warning(f"Encountered zero length segment: {tgt}") continue if frac_along > 1.0: diff --git a/neuroml/test/test_nml.py b/neuroml/test/test_nml.py index f2ffd85..6c71041 100644 --- a/neuroml/test/test_nml.py +++ b/neuroml/test/test_nml.py @@ -73,10 +73,6 @@ def test_generic_add_single(self): # Success: returns object self.assertIsNotNone(doc.add(cell)) - # Already added, so throw exception - with self.assertWarns(UserWarning): - doc.add(cell) - # Success self.assertIsNotNone(doc.add(cell1)) @@ -164,9 +160,6 @@ def test_add_to_container(self): pop3 = neuroml.Population(id="unique") network.add(pop3) - # warning because this is already added - with self.assertWarns(UserWarning): - network.add(pop3) # Note that for Python, this is a new object # So we can add it again @@ -199,6 +192,18 @@ def test_get_by_id(self): test_pop = network.get_by_id("pop0") self.assertIs(test_pop, pop) + def test_get_by_id2(self): + """Test the get_by_id method, but for attribute""" + channel = neuroml.IonChannel(id="test", species="k", conductance="10pS") + species = channel.get_by_id("species") + self.assertEqual(species, "k") + conductance = channel.get_by_id("conductance") + self.assertEqual(conductance, "10pS") + + # can now be edited + conductance = "20pS" + self.assertEqual(conductance, "20pS") + def test_component_validate(self): """Test validate function""" network = neuroml.Network() @@ -836,6 +841,35 @@ def test_class_hierarchy(self): print() print_hierarchy(hier) + def test_adding_any_exception(self): + """Test adding things to __ANY__ attributes exception raise""" + newdoc = neuroml.NeuroMLDocument(id="lol") + annotation = newdoc.add(neuroml.Annotation) + + # without hint="__ANY__", we raise an exception + with self.assertRaises(Exception) as cm: + annotation.add(" some_string") + + self.assertEqual( + """Received a text object to add. Please pass `hint="__ANY__"` to confirm that this is what you intend. I will then try to add this to an __ANY__ member in the object.""", + str(cm.exception), + ) + + def test_adding_any(self): + """Test adding things to __ANY__ attributes""" + newdoc = neuroml.NeuroMLDocument(id="lol") + annotation = newdoc.add(neuroml.Annotation) + # valid NeuroML, but not valid LEMS + # space required to distinguish it from the name of a component type, + # which will not have spaces + annotation.add(" some_string", hint="__ANY__") + + # remove all spaces to test the string + annotation_text = str(annotation) + annotation_text = "".join(annotation_text.split()) + print(annotation_text) + self.assertEqual("some_string", annotation_text) + if __name__ == "__main__": ta = TestNML() diff --git a/neuroml/utils.py b/neuroml/utils.py index 3033ff7..af26761 100644 --- a/neuroml/utils.py +++ b/neuroml/utils.py @@ -15,7 +15,7 @@ import networkx import neuroml.nml.nml as schema -from neuroml import NeuroMLDocument +from neuroml import BiophysicalProperties, Morphology, NeuroMLDocument from . import loaders @@ -234,7 +234,7 @@ def component_factory( Please see `GeneratedsSuperSuper.component_factory` for more information. """ new_obj = schema.NeuroMLDocument().component_factory( - component_type, validate, **kwargs + component_type=component_type, validate=validate, **kwargs ) return new_obj @@ -316,7 +316,10 @@ def get_relative_component_path( def fix_external_morphs_biophys_in_cell( - nml2_doc: NeuroMLDocument, overwrite: bool = True + nml2_doc: NeuroMLDocument, + overwrite: bool = True, + load_morphology: bool = True, + load_biophysical_properties: bool = True, ) -> NeuroMLDocument: """Handle externally referenced morphologies and biophysics in cells. @@ -338,18 +341,17 @@ def fix_external_morphs_biophys_in_cell( :param overwrite: toggle whether the document is overwritten or a deep copy created :type overwrite: bool + :param load_morphology: whether morphologies should be loaded + :type load_morphology: bool + :param load_biophysical_properties: whether biophysical_properties should be loaded + :type load_biophysical_properties: bool :returns: neuroml document :raises KeyError: if referenced morphologies/biophysics cannot be found """ - if overwrite is False: - newdoc = copy.deepcopy(nml2_doc) - else: - newdoc = nml2_doc - # get a list of morph/biophys ids being referred to by cells referenced_ids = [] - for cell in newdoc.cells: - if cell.morphology_attr is not None: + for cell in nml2_doc.cells: + if load_morphology is True and cell.morphology_attr is not None: if cell.morphology is None: referenced_ids.append(cell.morphology_attr) else: @@ -357,7 +359,10 @@ def fix_external_morphs_biophys_in_cell( f"Cell ({cell}) already contains a Morphology, ignoring reference." ) logger.warning("Please check/correct your cell description") - if cell.biophysical_properties_attr is not None: + if ( + load_biophysical_properties is True + and cell.biophysical_properties_attr is not None + ): if cell.biophysical_properties is None: referenced_ids.append(cell.biophysical_properties_attr) else: @@ -366,30 +371,77 @@ def fix_external_morphs_biophys_in_cell( ) logger.warning("Please check/correct your cell description") + if len(referenced_ids) == 0: + logger.debug("No externally referenced morphologies or biophysics") + return nml2_doc + + if overwrite is False: + newdoc = copy.deepcopy(nml2_doc) + else: + newdoc = nml2_doc + # load referenced ids from included files and store them in dicts - ext_morphs = {} - ext_biophys = {} - for inc in newdoc.includes: - incdoc = loaders.read_neuroml2_file(inc.href, verbose=False, optimized=True) - for morph in incdoc.morphology: + found = False + ext_morphs: Dict[str, Morphology] = {} + ext_biophys: Dict[str, BiophysicalProperties] = {} + + # first check the same document + if not found and load_morphology is True and newdoc.morphology: + for morph in newdoc.morphology: if morph.id in referenced_ids: ext_morphs[morph.id] = morph - for biophys in incdoc.biophysical_properties: + + if (len(ext_morphs) + len(ext_biophys)) == len(referenced_ids): + logger.debug("Found all references") + found = True + break + + if ( + not found + and load_biophysical_properties is True + and newdoc.biophysical_properties + ): + for biophys in newdoc.biophysical_properties: if biophys.id in referenced_ids: ext_biophys[biophys.id] = biophys - # also include morphs/biophys that are in the same document - for morph in newdoc.morphology: - if morph.id in referenced_ids: - ext_morphs[morph.id] = morph - for biophys in newdoc.biophysical_properties: - if biophys.id in referenced_ids: - ext_biophys[biophys.id] = biophys + if (len(ext_morphs) + len(ext_biophys)) == len(referenced_ids): + logger.debug("Found all references") + found = True + break + + # now check included files (they need to be loaded and parsed, so this can + # be computationally intensive) + # includes/morphology/biophysical_properties should generally be empty + # lists and not None, but there may be cases where these have been removed + # after the document was loaded + if not found and newdoc.includes: + for inc in newdoc.includes: + incdoc = loaders.read_neuroml2_file(inc.href, verbose=False, optimized=True) + + if load_morphology is True and incdoc.morphology: + for morph in incdoc.morphology: + if morph.id in referenced_ids: + ext_morphs[morph.id] = morph + + if load_biophysical_properties is True and incdoc.biophysical_properties: + for biophys in incdoc.biophysical_properties: + if biophys.id in referenced_ids: + ext_biophys[biophys.id] = biophys + + if (len(ext_morphs) + len(ext_biophys)) == len(referenced_ids): + logger.debug("Found all references") + found = True + break # update cells by placing the morphology/biophys in them: # if referenced ids are not found, throw errors for cell in newdoc.cells: - if cell.morphology_attr is not None and cell.morphology is None: + if ( + load_morphology is True + and cell.morphology_attr is not None + and cell.morphology is None + ): try: # TODO: do we need a deepcopy here? cell.morphology = copy.deepcopy(ext_morphs[cell.morphology_attr]) @@ -401,7 +453,8 @@ def fix_external_morphs_biophys_in_cell( raise e if ( - cell.biophysical_properties_attr is not None + load_biophysical_properties is True + and cell.biophysical_properties_attr is not None and cell.biophysical_properties is None ): try: