diff --git a/lems/model/model.py b/lems/model/model.py index b70a7f7..34c55c8 100644 --- a/lems/model/model.py +++ b/lems/model/model.py @@ -9,9 +9,10 @@ from os.path import dirname # For python versions where typing isn't available at all try: - from typing import List, Dict + from typing import List, Dict, Union, Tuple except ImportError: pass +import copy from lems import __schema_location__, __schema_version__ from lems.base.base import LEMSBase @@ -104,6 +105,17 @@ def __init__(self, include_includes=True, fail_on_missing_includes=True): """ Whether to raise an Exception when a file in an element is not found :type: boolean """ + self.resolved_model = None + """ A resolved version of the model, generated by self.resolve() + :type: None or Model""" + + self.comp_ref_map = None + """ A map of the component references in the model, generated by + self.get_comp_reference_map + + :type: None or Map + """ + def add_target(self, target): """ Adds a simulation target to the model. @@ -338,10 +350,16 @@ def export_to_file(self, filepath, level_prefix = ' '): f.close() def resolve(self): + # type: () -> Model """ - Resolves references in this model. + Resolves references in this model and returns resolved model. + + :returns: resolved Model """ + if self.resolved_model: + return self.resolved_model + model = self.copy() for ct in model.component_types: @@ -356,7 +374,8 @@ def resolve(self): c2.numeric_value = model.get_numeric_value(c2.value, c2.dimension) model.add(c2) - return model + self.resolved_model = model + return self.resolved_model def resolve_component_type(self, component_type): """ @@ -855,9 +874,83 @@ def get_numeric_value(self, value_str, dimension = None): #print("Have converted %s to value: %s, dimension %s"%(value_str, numeric_value, dimension)) return numeric_value - def list_exposures(self, substring=None): - # type: (str) -> Dict[Component, Map] - """Get exposures from model. + def get_component_list(self, substring=""): + # type: (str) -> Dict[str, Component] + """Get all components whose id matches the given substring. + + Note that in PyLEMS, if a component does not have an id attribute, + PyLEMS uses the name of the component as its ID. + See the parser methods in LEMSFileParser. + + This function is required because the component and fat_component + attribute of the model class only holds lists of the top level + components and not its child/children elements. So we need to manually + fetch them. + + :param substring: substring to match components against + :type substring: str + :returns: Dict of components matching the substring of the form {'id' : Component } + + """ + comp_list = {} + ret_list = {} + # For each top level component, recursively get to all children + # There is no advantage of resolving the model and using fat_components + # here. They do store a component's "children" but as `Children` + # objects and one cannot easily recursively descend the tree using + # them. So it is easier to use (non-fat) components here, which hold + # children as `Component` objects. + for comp in self.components: + comp_list.update(self.get_nested_components(comp)) + + for c in comp_list.values(): + if substring in c.id: + ret_list.update({c.id: c}) + + return ret_list + + def get_fattened_component_list(self, substring=""): + # type: (str) -> Map + """Get a list of fattened components whose ids include the substring. + + A "fattened component" is one where all elements of the components have + been resolved. See lems.model.component.FatComponent. + + :param substring: substring to match components against + :type substring: str + :returns: Map of fattened components matching the substring + + """ + resolved_model = self.resolve() + fattened_comp_list = Map() + + comp_list = resolved_model.get_component_list(substring).values() + for comp in comp_list: + fattened_comp_list[comp.id] = resolved_model.fatten_component(comp) + + return fattened_comp_list + + def get_nested_components(self, comp): + # type: (Component) -> Dict[str, Component] + """Get all nested (child/children) components in the comp component + + :param comp: component to get all nested (child/children) components for + :type comp: Component + :returns: list of components + + """ + comp_list = {} + comp_list.update({comp.id: comp}) + + for c in comp.children: + comp_list.update(self.get_nested_components(c)) + + return comp_list + + def list_exposures(self, substring=""): + # type: (str) -> Dict[FatComponent, Map] + """Get exposures from model belonging to components which contain the + given substring. :param substring: substring to match for in component names :type substring: str @@ -870,15 +963,9 @@ def list_exposures(self, substring=None): """ exposures = {} - comp_list = [] - if substring: - for comp in self.components: - if substring in comp.id: - comp_list.append(comp) - else: - comp_list = self.components + comp_list = self.get_fattened_component_list(substring) - for comp in comp_list: + for comp in comp_list.values(): cur_type = self.component_types[comp.type] allexps = Map() # Add exposures of the component type itself @@ -901,35 +988,226 @@ def list_exposures(self, substring=None): return exposures - def get_recording_path_for_exposures(self, comp, exposures): - # type: (Component, List[Exposure]) -> List[str] - """Create recording path from a component and its list of exposures. + def get_full_comp_paths_with_comp_refs(self, comp, comptext=None): + # type: (FatComponent, Union[None, str]) -> None + """Get list of component paths with all component references also + resolved for the given component `comp`. + + This does not return a value, but fills in self.temp_vec with all the + possible paths as individual lists. These can then be passed to the + construct_path method to construct string paths. - :param comp: component + Additionally, note that this generates paths from the model + declaration, not from a built simulation instance of the model. + Therefore, it does not find paths that are constructed during build + time. + + XXX: should this be converted to a private method? + + :param comp: Component to get all paths for :type comp: Component - :param exposures: list of exposures - :type exposures: [Exposure] - :returns: list of strings, one for each recording path + :param comptext: text to use for component (used for generation of path strings) + :type comptext: str """ - retlist = [] - for exposure in exposures: - retlist.append(comp.id + "/" + exposure.name) - return retlist - - def list_recording_paths_for_exposures(self, substring=None): - # type: (str) -> List[str] - """Get the recording path strings for exposures of matching component - ids. + debug = False + # ref_map = self.get_comp_ref_map() + fat_components = self.get_fattened_component_list() + if debug: + print("Processing {}".format(comp.id)) + self.temp_vec.append(comp.id) + if comptext: + if debug: + print("for {}, text given: {}".format(comp.id, comptext)) + self.path_vec.append(comptext) + else: + self.path_vec.append(comp.id) + + # proceed further in the tree + nextchildren = [] + # check if any children components are used in the model for this comp + for ch in comp.children: + if ch.name in fat_components: + nextchildren.append(fat_components[ch.name]) + if debug: + print("children {} for {} added".format(fat_components[ch.name], comp.id)) + # check if any child components are used in the model for this comp + nextchild = [] + for cc in comp.child_components: + if cc.id in fat_components: + nextchild.append(cc) + if debug: + print("child {} for {} added".format(cc, comp.id)) + + nextattachment = [] + # attachments + # TODO: not sure what function these serve before build time + for at in comp.attachments: + if at.name in fat_components: + nextattachment.append(fat_components[at.name]) + if debug: + print("attachment {} for {} added".format(cc, comp.id)) + + # structure + # multi instantiates + nextmi = [] + for mi in comp.structure.multi_instantiates: + # replace comp with comp[*] for each multi instantiated comp + self.path_vec[-1] = "SKIP" + for mi_n in range(mi.number): + nextmi.append(mi.component) + if debug: + print("MI {} for {} added".format(mi.component.id, comp.id)) + # child instances + nextci = [] + for ci in comp.structure.child_instances: + nextci.append(ci.referenced_component) + if debug: + print("CI {} for {} added".format(ci.referenced_component.id, comp.id)) + # nothing to be done for Withs + # event connections: note: when the simulation is built, the event + # connections are processes and the sources attached to the necessary + # target instances. Since we are not building the simulation here, we + # cannot list the exposures from event connection inputs at the target + # instances. + nextec = [] + for ec in comp.structure.event_connections: + nextec.append(fat_components[ec.receiver.id]) + if debug: + print("EC {} appended for {}".format(ec.receiver.id, comp.id)) + + # a leaf node + if not len(nextchildren) and not len(nextchild) and not len(nextattachment) and not len(nextmi) and not len(nextci) and not len(nextec): + if debug: + print("{} is leaf".format(comp.id)) + print("Append {} to recording_paths".format(self.temp_vec)) + print("Append {} to recording_paths_text".format(self.path_vec)) + # append a copy since the path_vec is emptied out each time + self.recording_paths.append(copy.deepcopy(self.temp_vec)) + self.recording_paths_text.append(copy.deepcopy(self.path_vec)) + self.temp_vec.pop() + self.path_vec.pop() + return + + # process all next level nodes + for nextnode in (nextchildren + nextchild): + self.get_full_comp_paths_with_comp_refs(nextnode) + for nextnode in nextattachment: + self.get_full_comp_paths_with_comp_refs(nextnode, "SKIP") + i = 0 + for nextnode in nextmi: + self.get_full_comp_paths_with_comp_refs(nextnode, "{}[{}]".format(comp.id, i)) + i += 1 + for nextnode in nextci: + self.get_full_comp_paths_with_comp_refs(nextnode, "SKIP") + for nextnode in nextec: + self.get_full_comp_paths_with_comp_refs(nextnode) + + self.temp_vec.pop() + self.path_vec.pop() + + def construct_path(self, pathlist, skip=None): + # type: (List[str], str) -> str + """Construct path from a list. + + :param vec: list of text strings to generate path from + :type vec: list(str) + :param skip: text strings to skip + :type skip: str + :returns: generated path string - :param substring: substring to match component ids against + """ + # remove "", which are components we don't want to include in the path + if skip: + while skip in pathlist: + pathlist.remove(skip) + return '/'.join(pathlist) + + def list_recording_paths_for_exposures(self, substring="", target=""): + # (str, str) -> List[str] + """List recording paths for exposures in the model for components + matching the given substring, and for the given simulation target. + + This is a helper method that will generate *all* recording paths for + exposures in the provided LEMS model. Since a detailed model may + include many paths, it is suggested to use the `substring` parameter to + limit the list to necessary components only. + + Please note that this uses only the declared model, and not a built + instance of the model. Therefore, it returns a subset of all possible + paths. + + :param substring: substring to match component IDs against :type substring: str - :returns: list of recording paths - + :param target: simulation target whose components are to be analysed + :type target: str + :return: list of generated path strings """ - recording_paths = [] + if not len(target): + print("Please provide a target element.") + return [] + exposures = self.list_exposures(substring) + resolved_comps = self.get_fattened_component_list() + target_comp = self.get_fattened_component_list(target) + if len(target_comp) != 1: + print("Multiple targets found. Please use a unique target name") + return [] + + if self.debug: + print(resolved_comps) + # temporary for the depth first search + self.temp_vec = [] + self.path_vec = [] + # store our recording paths + # this stores the comp.ids + self.recording_paths = [] + self.recording_paths_text = [] + target_comp = resolved_comps[target] + self.get_full_comp_paths_with_comp_refs(target_comp) + + exp_paths = [] + for r in range(len(self.recording_paths)): + p = self.recording_paths[r] + t = self.recording_paths_text[r] + # go over each element, appending exposures where needed + for i in range(len(p)): + compid = p[i] + comppath = t[0:i+1] + for acomp, exps in exposures.items(): + if acomp.id == compid: + for exp in exps: + if self.debug: + print("full comppath is {}".format(comppath + [exp.name])) + newpath = self.construct_path(comppath + [exp.name], "SKIP") + if newpath not in exp_paths: + exp_paths.append(newpath) + else: + if self.debug: + print("No exposures for {}".format(p[i])) + pass + + exp_paths.sort() + if self.debug: + print("\n".join(exp_paths)) + return exp_paths + + def get_comp_ref_map(self): + # type () -> Map[str, List[FatComponent]] + """Get a Map of ComponentReferences in the model. - for comp, exps in exposures.items(): - recording_paths.extend(self.get_recording_path_for_exposures(comp, exps)) - return recording_paths + :returns: Map with target -> [source] entries + """ + if self.comp_ref_map: + return self.comp_ref_map + + fat_components = self.get_fattened_component_list() + + self.comp_ref_map = Map() + for fc in fat_components.values(): + for cr in fc.component_references.values(): + try: + self.comp_ref_map[cr.referenced_component.id].append(fc) + except KeyError: + self.comp_ref_map[cr.referenced_component.id] = [fc] + return self.comp_ref_map diff --git a/lems/model/structure.py b/lems/model/structure.py index 31fa2ee..4124ba7 100644 --- a/lems/model/structure.py +++ b/lems/model/structure.py @@ -124,8 +124,8 @@ def __init__(self, from_, to, :type: str """ self.receiver = receiver - """ Name of the proxy receiver component attached to the target component that actually receiving the event. - :type: str """ + """ Proxy receiver component attached to the target component that actually receiving the event. + :type: Component """ self.receiver_container = receiver_container """ Name of the child component grouping to add the receiver to. diff --git a/lems/test/test_exposure_listing.xml b/lems/test/test_exposure_listing.xml index dfb431c..c94c706 100644 --- a/lems/test/test_exposure_listing.xml +++ b/lems/test/test_exposure_listing.xml @@ -74,8 +74,8 @@ - - + + diff --git a/lems/test/test_misc.py b/lems/test/test_misc.py index e14fa26..301307f 100644 --- a/lems/test/test_misc.py +++ b/lems/test/test_misc.py @@ -30,6 +30,14 @@ def test_exposure_getters(self): if c.id == "example_iaf2_cell": self.assertTrue('v' in es) + paths = model.list_recording_paths_for_exposures(substring="", target="net1") + self.assertTrue("net1/p1[0]/v" in paths) + self.assertTrue("net1/p1[1]/v" in paths) + self.assertTrue("net1/p1[2]/v" in paths) + self.assertTrue("net1/p1[3]/v" in paths) + self.assertTrue("net1/p1[4]/v" in paths) + self.assertTrue("net1/p2[0]/v" in paths) + if __name__ == '__main__': unittest.main()