Ejemplo n.º 1
0
def parse_path(path: str):
    """Parse a path of form path.to.template_file.template_name or path/to/template_file/template_name,
    returning a tuple of (name, file, abspath)."""

    if "/" in path or "\\" in path:
        import os

        # relative or absolute path of form:
        # path/to/file/TemplateName
        file, template_name = os.path.split(path)
        dirs, file = os.path.split(file)
        abspath = os.path.abspath(dirs)
    elif "." in path:
        *modules, file, template_name = path.split(".")

        # look for pyrates library and return absolute path
        parentdir = ".".join(modules)
        # let Python figure out where to look for the module
        try:
            module = importlib.import_module(parentdir)
        except ModuleNotFoundError:
            raise PyRatesException(f"Could not find Python (module) directory associated to path "
                                   f"`{parentdir}` of Template `{path}`.")
        try:
            abspath = module.__path__  # __path__ returns a list[str] or _NamespacePath
            abspath = abspath[0] if type(abspath) is list else abspath._path[0]
        except TypeError:
            raise PyRatesException(f"Something is wrong with the given YAML template path `{path}`.")

    else:
        raise NotImplementedError(f"Was base specified in template '{path}', but left empty?")
        # this should only happen, if "base" is specified, but empty

    return template_name, file, abspath
Ejemplo n.º 2
0
def _separate_variables(variables: dict):
    """
    Return variable definitions and the respective values.

    Returns
    -------
    variables
    inputs
    output
    """
    # this part can be improved a lot with a proper expression parser

    for vname, properties in variables.items():

        # get shape parameter, default shape is scalar
        shape = properties.get("shape", (1,))

        # identify variable type and data type
        # note: this assume that a "default" must be given for every variable
        try:
            vtype, dtype, value = _parse_defaults(properties["default"])
        except KeyError:
            raise PyRatesException("Variables need to have a 'default' (variable type, data type and/or value) "
                                   "specified.")

        yield vname, vtype, dtype, shape, value
Ejemplo n.º 3
0
    def apply(self, return_key=False, values: dict = None):
        """Returns the non-editable but unique, cashed definition of the operator."""

        # key for global operator cache is template name.
        # ToDo: Consider replacing this cache with a separate by-circuit cache.
        key = self.name
        if values is None:
            values = {}

        try:
            instance, default_values = self.cache[key]

            for vname, value in default_values.items():
                if vname not in values:
                    values[vname] = value

        except KeyError:
            # get variable definitions and specified default values
            variables = []
            default_values = dict()
            inputs = []
            output = None
            for vname, vtype, dtype, shape, value in _separate_variables(self.variables):

                # if no new value is given for a variable, get the default
                if vname not in values:
                    values[vname] = value

                default_values[vname] = value

                if vtype == "input":
                    inputs.append(vname)  # = dict(sources=[], reduce_dim=True)  # default to True for now
                    vtype = "state_var"
                elif vtype == "output":
                    if output is None:
                        output = vname  # for now assume maximum one output is present
                    else:
                        raise PyRatesException("More than one output specification found in operator. "
                                               "Only one output per operator is supported.")
                    vtype = "state_var"
                # pack variable defining properties into tuple
                variables.append((vname, vtype, dtype, shape))

            # reduce order of ODE if necessary
            # this step is currently skipped to streamline equation interface
            # instead equations need to be given as either non-differential equations or first-order
            # linear differential equations
            # *equations, variables = cls._reduce_ode_order(template.equations, variables)
            equations = self.equations

            instance = self.target_ir(equations=equations, variables=variables,
                                      inputs=inputs, output=output,
                                      template=self)
            self.cache[key] = (instance, default_values)

        if return_key:
            return instance, values, key
        else:
            return instance, values
Ejemplo n.º 4
0
    def __init__(self,
                 operators: Dict[str, OperatorIR] = None,
                 template: str = ""):

        super().__init__()
        if operators is None:
            operators = {}

        # compute hash from incoming operators. Different order of operators in input might lead to different hash.
        self._h = hash(tuple(operators.values()))

        # collect all information about output variables of operators
        #############################################################
        all_outputs = {}  # type: Dict[str, List[str]]
        # op_inputs, op_outputs = set(), set()
        for key, operator in operators.items():

            # add operator as node to local operator_graph
            inputs = {
                var: dict(sources=set(), reduce_dim=True)
                for var in operator.inputs
            }
            self.add_node(key, operator=operator, inputs=inputs, label=key)

            # collect all output variables
            out_var = operator.output

            # check, if variable name exists in outputs and create empty list if it doesn't
            if out_var not in all_outputs:
                all_outputs[out_var] = []

            all_outputs[out_var].append(key)

        # link outputs to inputs
        ########################
        for label, data in self.nodes(data=True):
            op = data["operator"]
            for in_var in op.inputs:
                if in_var in all_outputs:
                    # link all collected outputs of given variable in inputs field of operator
                    for predecessor in all_outputs[in_var]:
                        # add predecessor output as source; this would also work for non-equal variable names
                        data["inputs"][in_var]["sources"].add(predecessor)
                        # adding into set means there will be no order, but also nothing can be added twice
                        self.add_edge(predecessor, label)
                else:
                    pass  # means, that 'source' will remain an empty list and no incoming edge will be added

        # check for cycle in operator graph
        ###################################
        try:
            find_cycle(self)
        except NetworkXNoCycle:
            pass
        else:
            raise PyRatesException(
                "Found cyclic operator graph. Cycles are not allowed for operators within one node "
                "or edge.")
Ejemplo n.º 5
0
    def output(self):
        """Detect output variable of edge, assuming only one output variable exists."""

        # try to find single output variable
        # noinspection PyTypeChecker
        out_op = [op for op, out_degree in self.op_graph.out_degree if out_degree == 0]  # type: List[str]

        # only one single output operator allowed
        if len(out_op) == 1:
            out_var = self[out_op[0]].output
            return f"{out_op[0]}/{out_var}"
        elif len(out_op) == 0:
            return None
        else:
            raise PyRatesException("Too many or too little output operators found. Exactly one output operator and "
                                   "associated output variable is required per edge.")
Ejemplo n.º 6
0
    def apply(self, values: dict = None):
        """ Apply template to gain a node or edge intermediate representation.

        Parameters
        ----------
        values
            dictionary with operator/variable as keys and values to update these variables as items.

        Returns
        -------

        """

        value_updates = {}
        if values:
            for key, value in values.items():
                op_name, var_name = key.split("/")
                if op_name not in value_updates:
                    value_updates[op_name] = {}
                value_updates[op_name][var_name] = value

        operators = {}
        all_values = {}  # # type: Dict[OperatorIR, Dict]

        for template, variations in self.operators.items():
            values_to_update = variations

            if values_to_update is None:
                values_to_update = {}
            # if a value for this particular variation has been passed, overwrite the previous value
            if template.name in value_updates:
                values_to_update.update(value_updates.pop(template.name))
            # apply operator template to get OperatorIR and associated default values and label
            operator, op_values, key = template.apply(return_key=True,
                                                      values=values_to_update)
            operators[key] = operator
            all_values[key] = op_values

        # fail gracefully, if any variables remain in `values` which means, that there is some typo
        if value_updates:
            raise PyRatesException(
                "Found value updates that did not fit any operator by name. This may be due to a "
                "typo in specifying the operator or variable to update. Remaining variables:"
                f"{value_updates}")
        return self.target_ir(operators=operators,
                              values=all_values,
                              template=self.path)
Ejemplo n.º 7
0
    def apply(self, label: str = None):
        """Create a Circuit graph instance based on the template"""

        if not label:
            label = self.label

        # reformat node templates to NodeIR instances
        nodes = {key: temp.apply() for key, temp in self.nodes.items()}

        # reformat edge templates to EdgeIR instances
        edges = []
        for (source, target, template, values) in self.edges:

            # get edge template and instantiate it
            values = deepcopy(values)
            weight = values.pop("weight", 1.)

            # get delay
            delay = values.pop("delay", None)

            # treat empty dummy edge templates as not existent templates
            if len(template.operators) == 0:
                template = None
            if template is None:
                edge_ir = None
                if values:
                    # should not happen. Putting this just in case.
                    raise PyRatesException(
                        "An empty edge IR was provided with additional values. "
                        "No way to figure out where to apply those values.")
            else:
                edge_ir = template.apply(
                    values=values)  # type: Optional[EdgeIR] # edge spec

            edges.append((
                source,
                target,  # edge_unique_key,
                {
                    "edge_ir": edge_ir,
                    "weight": weight,
                    "delay": delay
                }))

        return CircuitIR(label, self.circuits, nodes, edges, self.path)
Ejemplo n.º 8
0
def update_edges(base_edges: List[tuple], updates: List[Union[tuple, dict]]):
    """Add edges to list of edges. Removing or altering is currently not supported."""

    updated = deepcopy(base_edges)
    for edge in updates:
        if isinstance(edge, dict):
            if "variables" in edge:
                edge = [
                    edge["source"], edge["target"], edge["template"],
                    edge["variables"]
                ]
            else:
                edge = [edge["source"], edge["target"], edge["template"]]
        elif not 3 <= len(edge) <= 4:
            raise PyRatesException(
                "Wrong edge data type or not enough arguments")
        updated.append(edge)

    return updated
Ejemplo n.º 9
0
    def input(self):
        """Detect input variable of edge, assuming only one input variable exists. This also references the operator
        the variable belongs to."""
        # noinspection PyTypeChecker
        in_op = [
            op for op, in_degree in self.op_graph.in_degree if in_degree == 0
        ]  # type: List[str]

        # multiple input operations are possible, as long as they require the same singular input variable
        in_var = set()
        for op_key in in_op:
            for var in self[op_key].inputs:
                in_var.add(f"{op_key}/{var}")

        if len(in_var) == 1:
            return in_var.pop()
        elif len(in_var) == 0:
            return None
        else:
            raise PyRatesException(
                "Too many input variables found. Exactly one or zero input variables are "
                "required per edge.")
Ejemplo n.º 10
0
    def apply(self, label: str = None, node_values: dict = None):
        """Create a Circuit graph instance based on the template


        Parameters
        ----------
        label
            (optional) Assign a label that is saved as a sort of name to the circuit instance. This is particularly
            relevant, when adding multiple circuits to a bigger circuit. This way circuits can be identified by their
            given label.
        node_values
            (optional) Dictionary containing values (and possibly other variable properties) that overwrite defaults in
            specific nodes/operators/variables. Values must be given in the form: {'node/op/var': value}

        Returns
        -------

        """
        if not label:
            label = self.label

        # reformat node templates to NodeIR instances
        if node_values is None:
            nodes = {key: temp.apply() for key, temp in self.nodes.items()}
        else:
            values = dict()
            for key, value in node_values.items():
                node, op, var = key.split("/")
                if node not in values:
                    values[node] = dict()

                values[node]["/".join((op, var))] = value
            nodes = {
                key: temp.apply(values.get(key, None))
                for key, temp in self.nodes.items()
            }

        # reformat edge templates to EdgeIR instances
        edges = []
        for (source, target, template, values) in self.edges:

            # get edge template and instantiate it
            values = deepcopy(values)
            weight = values.pop("weight", 1.)

            # get delay
            delay = values.pop("delay", None)
            spread = values.pop("spread", None)

            # treat additional source assignments in values dictionary
            extra_sources = {}
            keys_to_remove = []
            for key, value in values.items():
                try:
                    _, _, _ = value.split("/")
                except AttributeError:
                    # not a string
                    continue
                except ValueError as e:
                    raise ValueError(
                        f"Wrong format of source specifier. Expected form: `node/op/var`. "
                        f"Actual form: {value}")
                else:
                    # was actually able to split that string? Then it is an additional source specifier.
                    # Let's treat it as such
                    try:
                        op, var = key.split("/")
                    except ValueError as e:
                        if e.args[0].startswith("not enough"):
                            # No operator specified: assume that it is a known input variable. If not, we will notice
                            # later.
                            var = key
                            # Need to remove the key, but need to wait until after the iteration finishes.
                            keys_to_remove.append(key)
                        else:
                            raise e
                    else:
                        # we know which variable in which operator, so let us set its type to "input"
                        values[key] = "input"

                    # assuming, the variable has been set to "input", we can omit any operator description and only
                    # pass the actual variable name
                    extra_sources[var] = value

            for key in keys_to_remove:
                values.pop(key)

            # treat empty dummy edge templates as not existent templates
            if template and len(template.operators) == 0:
                template = None
            if template is None:
                edge_ir = None
                if values:
                    # should not happen. Putting this just in case.
                    raise PyRatesException(
                        "An empty edge IR was provided with additional values. "
                        "No way to figure out where to apply those values.")

            else:
                edge_ir = template.apply(
                    values=values)  # type: Optional[EdgeIR] # edge spec

            # check whether multiple source variables are defined
            try:
                # if source is a dictionary, pass on its values as source_var
                source_var = list(source.values())[0]  # type: dict
                source = list(source.keys())[0]
            except AttributeError:
                # no dictionary? only singular source definition present. go on as planned.
                edge_dict = dict(edge_ir=edge_ir,
                                 weight=weight,
                                 delay=delay,
                                 spread=spread)

            else:
                # oh source was indeed a dictionary. go pass source information as separate entry
                edge_dict = dict(edge_ir=edge_ir,
                                 weight=weight,
                                 delay=delay,
                                 spread=spread,
                                 source_var=source_var)

            # now add extra sources, if there are some
            if extra_sources:
                edge_dict["extra_sources"] = extra_sources

            edges.append((
                source,
                target,  # edge_unique_key,
                edge_dict))

        return CircuitIR(label, self.circuits, nodes, edges, self.path)