Skip to content

Ported objects

PortedObject(name, input_ports=[], output_ports=[], variable_ports=[], parsing_locals={}, **kwargs)

Bases: ABC

Base class implementing ported objects. Cannot be instantiated directly.

PARAMETER DESCRIPTION
name

a string which must be unique for each PortedObject inside a common CompositePortedObject.

TYPE: str

input_ports

list of input ports to expose. See add_input_ports.

TYPE: list[InputPort | dict | tuple | str] DEFAULT: []

output_ports

list of output ports to expose. See add_output_ports.

TYPE: list[OutputPort | dict | str] DEFAULT: []

variable_ports

list of variable ports to expose. See add_variable_ports.

TYPE: list[VariablePort | dict | str] DEFAULT: []

parsing_locals

a dictionary mapping strings to sympy objects.

TYPE: dict DEFAULT: {}

**kwargs

arguments passed to super().init(). No user arguments should be supplied.

DEFAULT: {}

Source code in psymple/build/abstract.py
def __init__(
    self,
    name: str,
    input_ports: list[InputPort | dict | tuple | str] = [],
    output_ports: list[OutputPort | dict | str] = [],
    variable_ports: list[VariablePort | dict | str] = [],
    parsing_locals: dict = {},
    **kwargs,
):
    """
    Construct a PortedObject.

    Args:
        name: a string which must be unique for each `PortedObject` inside a common
            [`CompositePortedObject`][psymple.build.CompositePortedObject].
        input_ports: list of input ports to expose.
            See [add_input_ports][psymple.build.abstract.PortedObject.add_input_ports].
        output_ports: list of output ports to expose.
            See [add_output_ports][psymple.build.abstract.PortedObject.add_input_ports].
        variable_ports: list of variable ports to expose.
            See [add_variable_ports][psymple.build.abstract.PortedObject.add_variable_ports].
        parsing_locals: a dictionary mapping strings to `sympy` objects.
        **kwargs: arguments passed to super().__init__(). No user arguments should be supplied.
    """
    self.name = name
    # Ports exposed to the outside, indexed by their name
    self.variable_ports = {}
    self.input_ports = {}
    self.output_ports = {}

    self.parsing_locals = parsing_locals
    self.add_input_ports(*input_ports)
    self.add_output_ports(*output_ports)
    self.add_variable_ports(*variable_ports)

add_input_ports(*ports)

Add input ports to self.

PARAMETER DESCRIPTION
*ports

data specifying an input port, in the form of:

  • an InputPort instance;
  • a dictionary specifying "name", and optionally "description" and "default_value";
  • a tuple, with the first entry specifying the name, and the second the default value;
  • a string, specifying the name of the port.

Arguments can contain a mixture of the above data formats.

TYPE: InputPort | dict | tuple | str DEFAULT: ()

Examples:

Using an InputPort instance:

>>> from psymple.ported_objects import VariablePortedObject, InputPort
>>> X = VariablePortedObject(name="X")
>>> X.add_input_ports(InputPort("A", default_value=6))
>>> X._dump_input_ports()
[{'name': 'A', 'description': '', 'default_value': 6}]

Using a dictionary:

>>> X = VariablePortedObject(name="X")
>>> X.add_input_ports(dict(name = "A", description = "input port A", default_value=6))
>>> X._dump_input_ports()
[{'name': 'A', 'description': 'input port A', 'default_value': 6}]

Using a tuple:

>>> X = VariablePortedObject(name="X")
>>> X.add_input_ports(("A", 6, "input port A"))
>>> X._dump_input_ports()
[{'name': 'A', 'description': 'input port A', 'default_value': 6}]

Using a string (note that a description or default value cannot be specified):

>>> X = VariablePortedObject(name="X")
>>> X.add_input_ports("A")
>>> X._dump_input_ports()
[{'name': 'A', 'description': '', 'default_value': None}]
Source code in psymple/build/abstract.py
def add_input_ports(self, *ports: InputPort | dict | tuple | str):
    """
    Add input ports to self.

    Args:
        *ports: data specifying an input port, in the form of:

            - an `InputPort` instance;
            - a dictionary specifying "name", and optionally "description" and "default_value";
            - a tuple, with the first entry specifying the name, and the second the default value;
            - a string, specifying the name of the port.

            Arguments can contain a mixture of the above data formats.

    Examples:
        Using an InputPort instance:
        >>> from psymple.ported_objects import VariablePortedObject, InputPort
        >>> X = VariablePortedObject(name="X")
        >>> X.add_input_ports(InputPort("A", default_value=6))
        >>> X._dump_input_ports()
        [{'name': 'A', 'description': '', 'default_value': 6}]

        Using a dictionary:
        >>> X = VariablePortedObject(name="X")
        >>> X.add_input_ports(dict(name = "A", description = "input port A", default_value=6))
        >>> X._dump_input_ports()
        [{'name': 'A', 'description': 'input port A', 'default_value': 6}]

        Using a tuple:
        >>> X = VariablePortedObject(name="X")
        >>> X.add_input_ports(("A", 6, "input port A"))
        >>> X._dump_input_ports()
        [{'name': 'A', 'description': 'input port A', 'default_value': 6}]

        Using a string (note that a description or default value cannot be specified):
        >>> X = VariablePortedObject(name="X")
        >>> X.add_input_ports("A")
        >>> X._dump_input_ports()
        [{'name': 'A', 'description': '', 'default_value': None}]

    """
    for port_info in ports:
        port = self.parse_port_entry(port_info, InputPort)
        self._add_input_port(port)

add_output_ports(*ports)

Add input ports to self.

PARAMETER DESCRIPTION
*ports

data specifying an output port, in the form of:

  • an OutputPort instance;
  • a dictionary specifying "name", and optionally "description";
  • a string, specifying the name of the port.

Arguments can contain a mixture of the above data formats.

TYPE: OutputPort | dict | str DEFAULT: ()

Examples:

Using an OutputPort instance:

>>> from psymple.ported_objects import FunctionalPortedObject, OutputPort
>>> X = FunctionalPortedObject(name="X")
>>> X.add_output_ports(OutputPort("A", description="output port A"))
>>> X._dump_output_ports()
[{'name': 'A', 'description': 'output port A'}]

Using a dictionary:

>>> X = FunctionalPortedObject(name="X")
>>> X.add_output_ports(dict(name = "A", description = "output port A"))
>>> X._dump_output_ports()
[{'name': 'A', 'description': 'output port A'}]

Using a string (note that a description or default value cannot be specified):

>>> X = FunctionalPortedObject(name="X")
>>> X.add_output_ports("A")
>>> X._dump_output_ports()
[{'name': 'A', 'description': ''}]
Source code in psymple/build/abstract.py
def add_output_ports(self, *ports: OutputPort | dict | str):
    """
    Add input ports to self.

    Args:
        *ports: data specifying an output port, in the form of:

            - an `OutputPort` instance;
            - a dictionary specifying "name", and optionally "description";
            - a string, specifying the name of the port.

            Arguments can contain a mixture of the above data formats.

    Examples:
        Using an OutputPort instance:
        >>> from psymple.ported_objects import FunctionalPortedObject, OutputPort
        >>> X = FunctionalPortedObject(name="X")
        >>> X.add_output_ports(OutputPort("A", description="output port A"))
        >>> X._dump_output_ports()
        [{'name': 'A', 'description': 'output port A'}]

        Using a dictionary:
        >>> X = FunctionalPortedObject(name="X")
        >>> X.add_output_ports(dict(name = "A", description = "output port A"))
        >>> X._dump_output_ports()
        [{'name': 'A', 'description': 'output port A'}]

        Using a string (note that a description or default value cannot be specified):
        >>> X = FunctionalPortedObject(name="X")
        >>> X.add_output_ports("A")
        >>> X._dump_output_ports()
        [{'name': 'A', 'description': ''}]

    """
    for port_info in ports:
        port = self.parse_port_entry(port_info, OutputPort)
        self._add_output_port(port)

add_variable_ports(*ports)

Add input ports to self.

PARAMETER DESCRIPTION
*ports

data specifying a variable port, in the form of:

  • a VariablePort instance;
  • a dictionary specifying "name", and optionally "description";
  • a string, specifying the name of the port.

Arguments can contain a mixture of the above data formats.

TYPE: VariablePort | dict | str DEFAULT: ()

Examples:

Using an VariablePort instance:

>>> from psymple.ported_objects import VariablePortedObject, VariablePort
>>> X = VariablePortedObject(name="X")
>>> X.add_variable_ports(VariablePort("A", description="variable port A"))
>>> X._dump_variable_ports()
[{'name': 'A', 'description': 'variable port A'}]

Using a dictionary:

>>> X = VariablePortedObject(name="X")
>>> X.add_variable_ports(dict(name = "A", description = "variable port A"))
>>> X._dump_variable_ports()
[{'name': 'A', 'description': 'variable port A'}]

Using a string (note that a description or default value cannot be specified):

>>> X = VariablePortedObject(name="X")
>>> X.add_variable_ports("A")
>>> X._dump_variable_ports()
[{'name': 'A', 'description': ''}]
Source code in psymple/build/abstract.py
def add_variable_ports(self, *ports: VariablePort | dict | str):
    """
    Add input ports to self.

    Args:
        *ports: data specifying a variable port, in the form of:

            - a `VariablePort` instance;
            - a dictionary specifying "name", and optionally "description";
            - a string, specifying the name of the port.

            Arguments can contain a mixture of the above data formats.

    Examples:
        Using an VariablePort instance:
        >>> from psymple.ported_objects import VariablePortedObject, VariablePort
        >>> X = VariablePortedObject(name="X")
        >>> X.add_variable_ports(VariablePort("A", description="variable port A"))
        >>> X._dump_variable_ports()
        [{'name': 'A', 'description': 'variable port A'}]

        Using a dictionary:
        >>> X = VariablePortedObject(name="X")
        >>> X.add_variable_ports(dict(name = "A", description = "variable port A"))
        >>> X._dump_variable_ports()
        [{'name': 'A', 'description': 'variable port A'}]

        Using a string (note that a description or default value cannot be specified):
        >>> X = VariablePortedObject(name="X")
        >>> X.add_variable_ports("A")
        >>> X._dump_variable_ports()
        [{'name': 'A', 'description': ''}]

    """
    for port_info in ports:
        port = self.parse_port_entry(port_info, VariablePort)
        self._add_variable_port(port)

PortedObjectWithAssignments(**kwargs)

Bases: PortedObject

Abstract class to hold common functionality of VariablePortedObject and FunctionalPortedObject. Cannot be instantiated directly.

Source code in psymple/build/abstract.py
def __init__(self, **kwargs):
    super().__init__(**kwargs)
    self.assignments = {}

VariablePortedObject(name, input_ports=[], variable_ports=[], assignments=[], create_output_ports=True, create_input_ports=True, parsing_locals={}, **kwargs)

Bases: PortedObjectWithAssignments

A ported object containing a collection of ODEs (DifferentialAssignment instances).

Each ODE is associated to a variable, which may or may not be exposed as a variable port. Symbols on the RHS of the ODE should be either:

  • Variables defined in this ported object
  • parameters that have corresponding input ports
  • globally defined symbols
PARAMETER DESCRIPTION
name

a string which must be unique for each PortedObject inside a common CompositePortedObject.

TYPE: str

input_ports

list of input ports to expose. See add_input_ports.

TYPE: list[InputPort | dict | tuple | str] DEFAULT: []

variable_ports

list of variable ports to expose. See add_variable_ports.

TYPE: list[InputPort | dict | str] DEFAULT: []

assignments

list of differential assignments (ODEs). See add_variable_assignments

TYPE: list[DifferentialAssignment | dict | tuple] DEFAULT: []

create_output_ports

if True, automatically create an output port exposing each exposed variable to be read by directed wires. See Notes for more information.

TYPE: bool DEFAULT: True

create_input_ports

if True, automatically expose all parameters as input ports. See Notes for more information.

TYPE: bool DEFAULT: True

parsing_locals

a dictionary mapping strings to sympy objects.

TYPE: dict DEFAULT: {}

**kwargs

arguments passed to super().init(). No user arguments should be supplied.

DEFAULT: {}

Notes
  • By default, each variable (dependent variable of each ODE) is automatically exposed as a variable port. Alternatively, chosen variables can be exposed by specifying them in the list variable_ports.

  • By default, each exposed variable will be mirrored in the creation of an output port with the same name. This output port reads the value of the variable.

  • Parameters listed in input_ports are exposed and can be used in ODE expressions.

  • If create_input_ports=True (default), then each symbol appearing in an ODE which is not a variable or parameter defined in input_ports is also exposed as a parameter input port. The created parameter will have no default value, and must be otherwise specified or linked by a wire in a parent CompositePortedObject.

Source code in psymple/build/ported_objects.py
def __init__(
    self,
    name: str,
    input_ports: list[InputPort | dict | tuple | str] = [],
    variable_ports: list[InputPort | dict | str] = [],
    assignments: list[DifferentialAssignment | dict | tuple] = [],
    create_output_ports: bool = True,
    create_input_ports: bool = True,
    parsing_locals: dict = {},
    **kwargs,
):
    """
    Construct a VariablePortedObject.

    Args:
        name: a string which must be unique for each `PortedObject` inside a common
            [`CompositePortedObject`][psymple.build.CompositePortedObject].
        input_ports: list of input ports to expose.
            See [add_input_ports][psymple.build.abstract.PortedObject.add_input_ports].
        variable_ports: list of variable ports to expose.
            See [add_variable_ports][psymple.build.abstract.PortedObject.add_variable_ports].
        assignments: list of differential assignments (ODEs). See
            [add_variable_assignments][psymple.build.VariablePortedObject.add_variable_assignments]
        create_output_ports: if `True`, automatically create an output port exposing each exposed variable
            to be read by directed wires. See Notes for more information.
        create_input_ports: if `True`, automatically expose all parameters as input ports. See Notes for more
            information.
        parsing_locals: a dictionary mapping strings to `sympy` objects.
        **kwargs: arguments passed to super().__init__(). No user arguments should be supplied.

    info: Notes
        - By default, each variable (dependent variable of each ODE) is automatically exposed as a
            variable port. Alternatively, chosen variables can be exposed by specifying
            them in the list variable_ports.

        - By default, each exposed variable will be mirrored in the creation of an output port with
            the same name. This output port reads the value of the variable.

        - Parameters listed in input_ports are exposed and can be used in ODE expressions.

        - If `create_input_ports=True` (default), then each symbol appearing in an ODE which is not a
            variable or parameter defined in input_ports is also exposed as a parameter input port. The
            created parameter will have no default value, and must be otherwise specified or linked by
            a wire in a parent [`CompositePortedObject`][psymple.build.CompositePortedObject].
    """
    super().__init__(
        name=name,
        input_ports=input_ports,
        variable_ports=variable_ports,
        parsing_locals=parsing_locals,
        **kwargs,
    )
    # A dict of assignments indexed by the variable name
    self.internals = {}
    create_variable_ports = False if variable_ports else True
    self.add_variable_assignments(
        *assignments,
        create_variable_ports=create_variable_ports,
        create_output_ports=create_output_ports,
        create_input_ports=create_input_ports,
    )
    self.create_input_ports = create_input_ports
    self.create_output_ports = create_output_ports

add_variable_assignments(*assignments, create_variable_ports=True, create_output_ports=True, create_input_ports=True)

Add variable assignments to self.

PARAMETER DESCRIPTION
*assignments

data specifying a DifferentialAssignment. Each entry must be:

  • an instance of DifferentialAssignment;
  • a dict with keys "variable" and "expression" which can be passed to the DifferentialAssignment constructor
  • a tuple, whose first entry is passed to the "variable" argument and whose second is passed to the "expression" argument of the DifferentialAssignment constructor

TYPE: DifferentialAssignment | dict | tuple DEFAULT: ()

create_variable_ports

if True, variable ports exposing the variable of each assignment will automatically be created.

TYPE: bool DEFAULT: True

create_output_ports

if True, output ports mirroring each variable port will automatically be created.

TYPE: bool DEFAULT: True

create_input_ports

if True, input ports for each free symbol in the expression of each assignment will automatically be created.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

if an assignment with the same variable name is already defined in self

Source code in psymple/build/ported_objects.py
def add_variable_assignments(
    self,
    *assignments: DifferentialAssignment | dict | tuple,
    create_variable_ports: bool = True,
    create_output_ports: bool = True,
    create_input_ports: bool = True,
):
    """
    Add variable assignments to self.

    Args:
        *assignments: data specifying a [`DifferentialAssignment`][psymple.build.assignments.DifferentialAssignment].
            Each entry must be:

            - an instance of `DifferentialAssignment`;
            - a `dict` with keys `"variable"` and `"expression"` which can be passed to
                the `DifferentialAssignment` constructor
            - a `tuple`, whose first entry is passed to the `"variable"` argument and whose
                second is passed to the `"expression"` argument of the `DifferentialAssignment`
                constructor

        create_variable_ports: if `True`, variable ports exposing the variable of each assignment
            will automatically be created.
        create_output_ports: if `True`, output ports mirroring each variable port will
            automatically be created.
        create_input_ports: if `True`, input ports for each free symbol in the expression of each
            assignment will automatically be created.

    Raises:
        ValueError: if an assignment with the same variable name is already defined in self
    """
    for assignment_info in assignments:
        assignment = self.parse_assignment_entry(
            assignment_info, DifferentialAssignment
        )
        variable_name = assignment.symbol.name
        if variable_name in self.assignments:
            raise ValueError(
                f"Variable '{variable_name}' in VariablePortedObject '{self.name}' doubly defined."
            )
        self.assignments[variable_name] = assignment
        if create_variable_ports:
            self.add_variable_ports(variable_name)
        elif variable_name not in self.variable_ports:
            self.internals[variable_name] = assignment.variable

    if create_output_ports:
        self.add_output_ports(*self.variable_ports.keys())

    if create_input_ports:
        # Create input ports for all symbols that are not variables (exposed or internal) or already
        # specified as input ports.
        free_symbols = {
            symb.name
            for a in self.assignments.values()
            for symb in a.get_free_symbols()
        }
        internal_variables = set(self.internals.keys())
        variable_ports = set(self.variable_ports.keys())
        input_ports = set(self.input_ports.keys())
        undefined_ports = (
            free_symbols - internal_variables - variable_ports - input_ports
        )
        self.add_input_ports(*undefined_ports)

compile(prefix_names=False, global_symbols=set())

Generate a CompiledPortedObject with:

  • input ports generated from the input ports of self
  • variable ports exposing the variable and assignment of each assignment instance of self which have a corresponding variable port of self
  • internal variable assignments for each assignment instance of self which do not have a corresponding variable port of self
  • identity functional assignments at output ports corresponding to each exposed variable
PARAMETER DESCRIPTION
prefix_names

if True, all symbols in self will be prefixed with self.name

TYPE: bool DEFAULT: False

global_symbols

symbols to pass to _assert_no_undefined_symbols method

TYPE: set DEFAULT: set()

Source code in psymple/build/ported_objects.py
def compile(self, prefix_names: bool = False, global_symbols: set = set()):
    """
    Generate a [`CompiledPortedObject`][psymple.build.ported_objects.CompiledPortedObject] with:

    - input ports generated from the input ports of self
    - variable ports exposing the variable and assignment of each assignment
        instance of self which have a corresponding variable port of self
    - internal variable assignments for each assignment instance of self
        which do not have a corresponding variable port of self
    - identity functional assignments at output ports corresponding to each
        exposed variable

    Args:
        prefix_names: if `True`, all symbols in self will be prefixed with `self.name`
        global_symbols: symbols to pass to `_assert_no_undefined_symbols` method
    """
    self._assert_no_undefined_symbols(global_symbols | self.parsing_locals.keys())
    compiled = CompiledPortedObject(self.name, self.parsing_locals)
    for name, assignment in self.assignments.items():
        if isinstance(assignment, DifferentialAssignment):
            if name in self.variable_ports:
                compiled._add_variable_port(
                    CompiledVariablePort(self.variable_ports[name], assignment)
                )
            elif name in self.internals:
                compiled.internal_variable_assignments[name] = assignment

            if name in self.output_ports:
                compiled._add_output_port(
                    CompiledOutputPort(
                        self.output_ports[name],
                        FunctionalAssignment(assignment.symbol, assignment.symbol),
                    )
                )

    for input_port in self.input_ports.values():
        compiled._add_input_port(CompiledInputPort(input_port))

    if prefix_names:
        compiled._sub_prefixed_symbols()
    return compiled

to_data()

A dismantler method such that every instance X of VariablePortedObject can be recreated by calling X.to_data().to_ported_object()

RETURNS DESCRIPTION
data

a data object capturing the data of self

TYPE: PortedObjectData

Source code in psymple/build/ported_objects.py
def to_data(self) -> PortedObjectData:
    """
    A dismantler method such that every instance X of `VariablePortedObject`
    can be recreated by calling `X.to_data().to_ported_object()`

    Returns:
        data: a data object capturing the data of self
    """
    metadata = {
        "name": self.name,
        "type": "vpo",
    }
    object_data = {
        "input_ports": self._dump_input_ports(),
        "variable_ports": self._dump_variable_ports(),
        "assignments": self._dump_assignments(),
        "create_input_ports": self.create_input_ports,
        "create_output_ports": self.create_output_ports,
    }
    return PortedObjectData(metadata=metadata, object_data=object_data)

FunctionalPortedObject(name, input_ports=[], assignments=[], create_input_ports=True, parsing_locals={}, **kwargs)

Bases: PortedObjectWithAssignments

A PortedObject containing a multivariate function.

The function is defined by a set of ParameterAssignment instances.

The function arguments are the free symbols on the RHS of the assignments, and should be exposed as input ports. The function values are the LHS of the assignments, and are automatically exposed as output ports.

Function assignments whose expression references a parameter defined as the function value of another expression are not allowed.

PARAMETER DESCRIPTION
name

a string which must be unique for each PortedObject inside a common CompositePortedObject.

TYPE: str

input_ports

list of input ports to expose. See add_input_ports.

TYPE: list DEFAULT: []

assignments

list of functional assignments. See add_parameter_assignments.

TYPE: list[ParameterAssignment | tuple | dict] DEFAULT: []

create_input_ports

if True, automatically expose all parameters as input ports. See Notes for more information.

TYPE: bool DEFAULT: True

parsing_locals

a dictionary mapping strings to sympy objects.

TYPE: dict DEFAULT: {}

**kwargs

arguments passed to super().init(). No user arguments should be supplied.

DEFAULT: {}

Notes
  • The parameter of every created assignment is automatically exposed as an output port.

  • If create_input_ports=True (default), then each symbol appearing in a function which is not a parameter defined in input_ports is also exposed as a parameter input port. The created parameter will have no default value, and must be otherwise specified or linked by a wire in a parent CompositePortedObject.

Source code in psymple/build/ported_objects.py
def __init__(
    self,
    name: str,
    input_ports: list = [],
    assignments: list[ParameterAssignment | tuple | dict] = [],
    create_input_ports: bool = True,
    parsing_locals: dict = {},
    **kwargs,
):
    """
    Construct a FunctionalPortedObject.

    Args:
        name: a string which must be unique for each `PortedObject` inside a common
            [`CompositePortedObject`][psymple.build.CompositePortedObject].
        input_ports: list of input ports to expose.
            See [add_input_ports][psymple.build.abstract.PortedObject.add_input_ports].
        assignments: list of functional assignments. See
            [add_parameter_assignments][psymple.build.FunctionalPortedObject.add_parameter_assignments].
        create_input_ports: if `True`, automatically expose all parameters as input ports. See Notes for more
            information.
        parsing_locals: a dictionary mapping strings to `sympy` objects.
        **kwargs: arguments passed to super().__init__(). No user arguments should be supplied.

    info: Notes
        - The parameter of every created assignment is automatically exposed as an output port.

        - If `create_input_ports=True` (default), then each symbol appearing in a function which is not a
            parameter defined in input_ports is also exposed as a parameter input port. The
            created parameter will have no default value, and must be otherwise specified or linked by
            a wire in a parent [`CompositePortedObject`][psymple.build.CompositePortedObject].
    """
    # TODO: Functional ported objects should take lists of assignments to a list of output port
    super().__init__(
        name=name, input_ports=input_ports, parsing_locals=parsing_locals, **kwargs
    )
    self.add_parameter_assignments(
        *assignments, create_input_ports=create_input_ports
    )
    self.create_input_ports = create_input_ports

add_parameter_assignments(*assignments, create_input_ports=True)

Add parameter assignments to self.

PARAMETER DESCRIPTION
*assignments

data specifying a ParameterAssignment. Each entry must be:

  • an instance of ParameterAssignment;
  • a dict with keys "parameter" and "expression" which can be passed to the ParameterAssignment constructor
  • a tuple, whose first entry is passed to the "parameter" argument and whose second is passed to the "expression" argument of the ParameterAssignment constructor

TYPE: list[ParameterAssignment | dict | tuple] DEFAULT: ()

create_input_ports

if True, input ports for each free symbol in the expression of each assignment will automatically be created.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

if an assignment with the same variable name is already defined in self

ValueError

if an expression contains a symbol with no corresponding input port

Source code in psymple/build/ported_objects.py
def add_parameter_assignments(
    self,
    *assignments: list[ParameterAssignment | dict | tuple],
    create_input_ports: bool = True,
):
    """
    Add parameter assignments to self.

    Args:
        *assignments: data specifying a [`ParameterAssignment`][psymple.build.assignments.ParameterAssignment].
            Each entry must be:

            - an instance of `ParameterAssignment`;
            - a `dict` with keys `"parameter"` and `"expression"` which can be passed to
                the `ParameterAssignment` constructor
            - a `tuple`, whose first entry is passed to the `"parameter"` argument and whose
                second is passed to the `"expression"` argument of the `ParameterAssignment`
                constructor

        create_input_ports: if `True`, input ports for each free symbol in the expression of each
            assignment will automatically be created.

    Raises:
        ValueError: if an assignment with the same variable name is already defined in self
        ValueError: if an expression contains a symbol with no corresponding input port
    """
    for assignment_info in assignments:
        assignment = self.parse_assignment_entry(
            assignment_info, FunctionalAssignment
        )
        parameter_name = assignment.parameter.name
        if parameter_name in self.assignments:
            raise ValueError(
                f"Variable '{parameter_name}' in FunctionalPortedObject '{self.name}' doubly defined."
            )
        free_symbols = assignment.get_free_symbols()
        for symbol in free_symbols:
            name = symbol.name
            if name not in self.input_ports:
                if create_input_ports:
                    self.add_input_ports(name)
                else:
                    raise ValueError(
                        f"Expression contains symbol {name} but there is no "
                        "corresponding input port."
                    )
        self.assignments[parameter_name] = assignment
        self.output_ports[parameter_name] = OutputPort(parameter_name)

compile(prefix_names=False)

Generate a CompiledPortedObject with:

  • input ports generated from input ports of self
  • output ports exposing the parameter and assignment of each assignment instance of self
PARAMETER DESCRIPTION
prefix_names

if True, all symbols in self will be prefixed with self.name

TYPE: bool DEFAULT: False

Source code in psymple/build/ported_objects.py
def compile(self, prefix_names: bool = False):
    """
    Generate a [`CompiledPortedObject`][psymple.build.ported_objects.CompiledPortedObject] with:

    - input ports generated from input ports of self
    - output ports exposing the parameter and assignment of each assignment
        instance of self

    Args:
        prefix_names: if `True`, all symbols in self will be prefixed with `self.name`
    """
    compiled = CompiledPortedObject(self.name, self.parsing_locals)
    # Pass input ports of self through to input ports of compiled object
    for name, input_port in self.input_ports.items():
        compiled._add_input_port(CompiledInputPort(input_port))
    # Create an output port of compiled holding each assignment of self
    for name, output_port in self.output_ports.items():
        assignment = self.assignments[name]
        compiled._add_output_port(CompiledOutputPort(output_port, assignment))
    # Prefix names  for objects compiled as children
    if prefix_names:
        compiled._sub_prefixed_symbols()
    return compiled

to_data()

A dismantler method such that every instance X of VariablePortedObject can be recreated by calling X.to_data().to_ported_object()

RETURNS DESCRIPTION
data

a data object capturing the data of self

TYPE: PortedObjectData

Source code in psymple/build/ported_objects.py
def to_data(self) -> PortedObjectData:
    """
    A dismantler method such that every instance X of `VariablePortedObject`
    can be recreated by calling `X.to_data().to_ported_object()`

    Returns:
        data: a data object capturing the data of self
    """
    metadata = {
        "name": self.name,
        "type": "fpo",
    }
    object_data = {
        "input_ports": self._dump_input_ports(),
        "assignments": self._dump_assignments(),
        "create_input_ports": self.create_input_ports,
    }
    return PortedObjectData(metadata=metadata, object_data=object_data)

CompositePortedObject(name, children=[], input_ports=[], output_ports=[], variable_ports=[], variable_wires=[], directed_wires=[], parsing_locals={}, **kwargs)

Bases: PortedObject

A ported object containing other ported object instances whose ports are connected by directed wires and variable wires.

Directed wires

Directed wires connect:

  • an input port of self to input ports of children, or,
  • an output port of a child to input ports of children and/or upto one output port of self, or,
  • a variable port of a child to input ports of children.

These wires capture functional composition. In the following example, a CompositePortedObject instance X contains FunctionalPortedObject instances A and B. Object A specifies the assignment \( x = f(y) \) and B specifies the assignment \( r = g(u,v) \). Connecting output port x of A (accessed by "A.x") to input port u of B (accessed by "B.u") with a directed wire represents the composite assignment \( r = g(f(y), v) \).

Examples:

>>> from psymple.build import FunctionalPortedObject, CompositePortedObject
>>> A = FunctionalPortedObject(name="A", assignments=[("x", "f(y)")])
>>> B = FunctionalPortedObject(name="B", assignments=[("y", "g(u,v)")])
>>> X = CompositePortedObject(name="X", children=[A,B], directed_wires=[("A.x", "B.u")])

See add_wires for the syntax to specify directed wires.

Variable wires

Variable wires connect variable ports of children to upto one variable port of self.

These wires capture ODE aggregation:

ODE aggregation

The aggregation of the ODEs \( dx/dt = f(x,t,a) \) and \( dy/dt = g(y,t,b) \), identifying \( (x,y) \longrightarrow z \), is the ODE \( dz/dt = f(z,t,a) + g(z,t,b) \).

In the following example, a CompositePortedObject instance X contains VariablePortedObject instances A and B. Object A specifies the ODE \( dx/dt = f(x,t,a) \) and B specifies the ODE \( dy/dt = g(y,t,b) \). Aggregating variable port x of A (accessed by "A.x") and variable port y of B (accessed by "B.y") and exposing at variable port z of X (identifying \( (x,y) \longrightarrow z \)) represents the ODE \( dz/dt = f(z,t,a) + g(z,t,b) \).

Examples:

>>> from psymple.build import FunctionalPortedObject, CompositePortedObject
>>> A = FunctionalPortedObject(name="A", assignments=[("x", "f(x,t,a)")])
>>> B = FunctionalPortedObject(name="B", assignments=[("y", "g(y,t,b)")])
>>> X = CompositePortedObject(name="X", children=[A,B], variable_ports = ["z"], variable_wires=[(["A.x", "B.u"], "z")])

See add_wires for the syntax to specify directed wires.

Requirements
  • Every input port of self should be the source of at least one directed wire

  • Every output port of self must be the destination of exactly one directed wire

  • Every variable port of self must be the destination of at most one variable wire

  • Every input port of a child must either have a default value or a directed wire connected to it

  • Every output port of a child should have a directed wire going out of it

  • Every variable port of a child should have a variable wire connected to it

  • The directed wires should have no cycles (when contracting child ported objects into nodes of a graph)

PARAMETER DESCRIPTION
name

a string which must be unique for each PortedObject inside a common CompositePortedObject.

TYPE: str

children

list of children to add. See add_children.

TYPE: list[PortedObject | PortedObjectData] DEFAULT: []

input_ports

list of input ports to expose. See add_input_ports.

TYPE: list[InputPort | dict | tuple | str] DEFAULT: []

output_ports

list of output ports to expose. See add_output_ports.

TYPE: list[OutputPort | dict | str] DEFAULT: []

variable_ports

list of variable ports to expose. See add_variable_ports.

TYPE: list[VariablePort | dict | str] DEFAULT: []

variable_wires

list of variable wires to create. See add_wires.

TYPE: list[dict | tuple] DEFAULT: []

directed_wires

list of directed wires to create. See add_wires.

TYPE: list[dict | tuple] DEFAULT: []

parsing_locals

a dictionary mapping strings to sympy objects.

TYPE: dict DEFAULT: {}

**kwargs

arguments passed to super().init(). No user arguments should be supplied.

DEFAULT: {}

Note

There is no automatic creation of ports in a CompositePortedObject

Source code in psymple/build/ported_objects.py
def __init__(
    self,
    name: str,
    children: list[PortedObject | PortedObjectData] = [],
    input_ports: list[InputPort | dict | tuple | str] = [],
    output_ports: list[OutputPort | dict | str] = [],
    variable_ports: list[VariablePort | dict | str] = [],
    variable_wires: list[dict | tuple] = [],
    directed_wires: list[dict | tuple] = [],
    parsing_locals: dict = {},
    **kwargs,
):
    """
    Construct a CompositePortedObject.

    Args:
        name: a string which must be unique for each `PortedObject` inside a common
            [`CompositePortedObject`][psymple.build.CompositePortedObject].
        children: list of children to add.
            See [add_children][psymple.build.CompositePortedObject.add_children].
        input_ports: list of input ports to expose.
            See [add_input_ports][psymple.build.abstract.PortedObject.add_input_ports].
        output_ports: list of output ports to expose.
            See [add_output_ports][psymple.build.abstract.PortedObject.add_input_ports].
        variable_ports: list of variable ports to expose.
            See [add_variable_ports][psymple.build.abstract.PortedObject.add_variable_ports].
        variable_wires: list of variable wires to create. See
            [add_wires][psymple.build.CompositePortedObject.add_wires].
        directed_wires: list of directed wires to create. See
            [add_wires][psymple.build.CompositePortedObject.add_wires].
        parsing_locals: a dictionary mapping strings to `sympy` objects.
        **kwargs: arguments passed to super().__init__(). No user arguments should be supplied.

    info: Note
        There is no automatic creation of ports in a `CompositePortedObject`
    """
    super().__init__(
        name=name,
        input_ports=input_ports,
        output_ports=output_ports,
        variable_ports=variable_ports,
        parsing_locals=parsing_locals,
        **kwargs,
    )
    self.children = {}
    self.variable_aggregation_wiring = []
    self.directed_wires = []
    self.add_children(*children)
    self.add_wires(variable_wires=variable_wires, directed_wires=directed_wires)

add_children(*children)

Add children to self. A child is a PortedObject instance whose ports and assignments become available to self.

PARAMETER DESCRIPTION
*children

instance of PortedObject or PortedObjectData specifying a ported object. Entries can be a mixture of types.

TYPE: PortedObjectData | PortedObject DEFAULT: ()

Source code in psymple/build/ported_objects.py
def add_children(self, *children: PortedObjectData | PortedObject):
    """
    Add children to `self`. A child is a `PortedObject` instance whose ports and assignments
    become available to `self`.

    Args:
        *children: instance of `PortedObject` or `PortedObjectData` specifying a
            ported object. Entries can be a mixture of types.
    """
    for data in children:
        # Attempt to coerce dictionary into PortedObjectData
        if isinstance(data, dict):
            if not type(data) == PortedObjectData:
                data = PortedObjectData(**data)
            self._build_child(data)
        elif isinstance(data, PortedObject):
            self._add_child(data)

add_directed_wire(source_name, destination_names)

Add a directed wire to self.

PARAMETER DESCRIPTION
source_name

a string identifying the source port

TYPE: str

destination_names

a string or a list of strings identifying destination port(s)

TYPE: str | list[str]

RAISES DESCRIPTION
WiringError

if the provided ports cannot be found or are of incorrect type.

Note

It is recommended to use the add_wires method for additional entry options and to add multiple wires at the same time.

Source code in psymple/build/ported_objects.py
def add_directed_wire(self, source_name: str, destination_names: str | list[str]):
    """
    Add a directed wire to self.

    Args:
        source_name: a string identifying the source port
        destination_names: a string or a list of strings identifying destination port(s)

    Raises:
        WiringError: if the provided ports cannot be found or are of incorrect type.

    info: Note
        It is recommended to use the [add_wires][psymple.build.CompositePortedObject.add_wires]
        method for additional entry options and to add multiple wires at the same time.
    """
    source_port = self._get_port_by_name(source_name, "parameter")
    error_text = (
        f"directed wire from {source_name} to {destination_names} in {self.name}"
    )
    if source_port is None:
        raise WiringError(
            f"Error adding {error_text}. "
            f"Source port '{source_name}' does not exist."
        )
    if (
        source_name in self.output_ports
        or type(source_port) is VariablePort
        or (source_name not in self.input_ports and type(source_port) is InputPort)
    ):
        # Source must be: own input, or child output
        raise WiringError(
            f"Error adding {error_text}. Source port '{source_name}' "
            "must be an input port or a child output port."
        )
    # If a singular destination is specified, coerce it into a list
    if isinstance(destination_names, str):
        destination_names = [destination_names]
    for destination_name in destination_names:
        destination_port = self._get_port_by_name(destination_name, "parameter")
        if destination_port is None:
            raise WiringError(
                f"Error adding {error_text}. "
                f"Destination port '{destination_name}' does not exist."
            )
        if (
            destination_name in self.input_ports
            or type(source_port) is VariablePort
            or (
                destination_name not in self.output_ports
                and type(destination_port) is OutputPort
            )
        ):
            # Destination must be: own output, or child input
            raise WiringError(
                f"Error adding {error_text}. "
                f"Destination port '{destination_name}' must be "
                "an output port or a child input port."
            )

    wire = DirectedWire(source_name, destination_names)
    self.directed_wires.append(wire)

add_variable_wire(child_ports, parent_port=None, output_name=None)

Add a variable wire to self.

PARAMETER DESCRIPTION
child_ports

a list of strings identifying variable ports to aggregate

TYPE: list[str]

parent_port

a string identifying the parent variable port of self to identify with

TYPE: str DEFAULT: None

output_name

a string identifying the aggregation internally if a parent port is not specified.

TYPE: str DEFAULT: None

RAISES DESCRIPTION
WiringError

if the provided ports cannot be found or are of incorrect type.

Note

It is recommended to use the add_wires method for additional entry options and to add multiple wires at the same time.

Source code in psymple/build/ported_objects.py
def add_variable_wire(
    self,
    child_ports: list[str],
    parent_port: str = None,
    output_name: str = None,
):
    """
    Add a variable wire to self.

    Args:
        child_ports: a list of strings identifying variable ports to aggregate
        parent_port: a string identifying the parent variable port of self to identify with
        output_name: a string identifying the aggregation internally if a parent port
            is not specified.

    Raises:
        WiringError: if the provided ports cannot be found or are of incorrect type.

    info: Note
        It is recommended to use the [add_wires][psymple.build.CompositePortedObject.add_wires]
        method for additional entry options and to add multiple wires at the same time.
    """
    error_text = f"variable wire with {f'parent_port {parent_port}' if parent_port else f'output_name {output_name}'} and children {child_ports} in {self.name}"
    if parent_port is not None:
        port = self._get_port_by_name(parent_port, "variable")
        if (
            port is None
            or not type(port) is VariablePort
            or not self._is_own_port(parent_port)
        ):
            WiringError(
                f"Error adding {error_text}. "
                f"Parent port '{parent_port}' must be a variable port "
                "of the ported object itself."
            )
    # Child ports should be ports of children
    for child_port in child_ports:
        port = self._get_port_by_name(child_port, "variable")
        if (
            port is None
            or not type(port) is VariablePort
            or self._is_own_port(child_port)
        ):
            WiringError(
                f"Error adding {error_text}. "
                f"Child port '{child_port}' must be a variable port of a child."
            )
    wiring = VariableAggregationWiring(child_ports, parent_port, output_name)
    self.variable_aggregation_wiring.append(wiring)

add_wires(variable_wires=[], directed_wires=[])

Add wires to self.

Variable wires aggregate a set of child variable ports, and either

  • expose the result as a variable port of self, or,
  • store the result internally.

Either a parent port or internal name must be provided. Specifying a parent port will override the internal name.

Directed wires connect

  • an input port of self to input ports of children, or,
  • an output port of a child to input ports of children and/or upto one output port of self, or,
  • a variable port of a child to input ports of children.
PARAMETER DESCRIPTION
variable_wires

a list of either:

  • a dictionary specifying "child_ports" (list[str]), and either "parent_port" (str), or "output_name" (str);
  • a tuple which maps to the above dictionary, which must either be of the form (child_ports, parent_port) or (child_ports, None, output_name).

TYPE: list DEFAULT: []

directed_wires

a list of either:

  • a dictionary specifying "source" (str) and either "destinations" (list[str]) or "destination" (str);
  • a tuple, which maps to the above dictionary, which must be of the form (source, destinations) or (source, destination).

TYPE: list DEFAULT: []

RAISES DESCRIPTION
ValidationError

if the provided data cannot be parsed correctly.

Source code in psymple/build/ported_objects.py
def add_wires(self, variable_wires: list = [], directed_wires: list = []):
    """
    Add wires to self.

    Variable wires aggregate a set of child variable ports, and either

    - expose the result as a variable port of self, or,
    - store the result internally.

    Either a parent port or internal name must be provided. Specifying a parent port will
    override the internal name.

    Directed wires connect

    - an input port of self to input ports of children, or,
    - an output port of a child to input ports of children and/or upto one output port of self, or,
    - a variable port of a child to input ports of children.

    Args:
        variable_wires: a list of either:

            - a dictionary specifying `"child_ports"` (`list[str]`), and either `"parent_port"` (`str`),
                or `"output_name"` (`str`);
            - a tuple which maps to the above dictionary, which must either be of the form
                `(child_ports, parent_port)` or `(child_ports, None, output_name)`.

        directed_wires: a list of either:

            - a dictionary specifying `"source"` (`str`) and either `"destinations"` (`list[str]`)
                or `"destination"` (`str`);
            - a tuple, which maps to the above dictionary, which must be of the form
                `(source, destinations)` or `(source, destination)`.

    Raises:
        ValidationError: if the provided data cannot be parsed correctly.
    """
    for wire_info in variable_wires:
        if isinstance(wire_info, dict):
            keys = wire_info.keys()
            if "child_ports" in keys and (
                "parent_port" in keys or "output_name" in keys
            ):
                self.add_variable_wire(**wire_info)
            else:
                raise ValidationError(
                    f"The dictionary {wire_info} must at least specify keys "
                    f'"child_ports" and either "parent_port" or "output_name.'
                )
        elif isinstance(wire_info, tuple):
            self.add_variable_wire(
                child_ports=wire_info[0],
                parent_port=wire_info[1] if len(wire_info) >= 2 else None,
                output_name=wire_info[2] if len(wire_info) == 3 else None,
            )
        else:
            raise ValidationError(
                f"The information {wire_info} is not a dictionary or tuple"
            )

    for wire_info in directed_wires:
        if isinstance(wire_info, dict):
            keys = wire_info.keys()
            if keys == {"source", "destinations"} or keys == {
                "source",
                "destination",
            }:
                self.add_directed_wire(*wire_info.values())
            else:
                raise ValidationError(
                    f'The dictionary {wire_info} must contain keys "source" and either "destination" or "destinations".'
                )
        elif isinstance(wire_info, tuple):
            self.add_directed_wire(*wire_info)
        else:
            raise ValidationError(
                f"The element {wire_info} is not a dictionary or tuple"
            )

compile(prefix_names=False)

Generate a CompiledPortedObject with:

  • input ports generated from input ports of self
  • output ports generated from output ports of self, with assignments exposed by directed wires
  • variable ports generated from variable ports of self, with assignments exposed by variable wires
  • internal variable assignments generated from variable assignments of children not exposed to variable ports
  • internal parameter assignments generated from parameter assignments of children not exposed to output ports
PARAMETER DESCRIPTION
prefix_names

if True, all symbols in self will be prefixed with self.name

TYPE: bool DEFAULT: False

Source code in psymple/build/ported_objects.py
def compile(self, prefix_names: bool = False):
    """
    Generate a [`CompiledPortedObject`][psymple.build.ported_objects.CompiledPortedObject] with:

    - input ports generated from input ports of self
    - output ports generated from output ports of self, with assignments exposed by directed wires
    - variable ports generated from variable ports of self, with assignments exposed by variable wires
    - internal variable assignments generated from variable assignments of children not exposed to
        variable ports
    - internal parameter assignments generated from parameter assignments of children not exposed to
        output ports

    Args:
        prefix_names: if `True`, all symbols in self will be prefixed with `self.name`
    """
    # Approch:
    #   - Compile each child of self recursively
    #   - Compile input ports of self
    #   - Collect input ports, variable ports and internal assignments of children
    #   - Process directed wires, producing compiled output ports, symbol identifications,
    #       or internal parameter assignments, as neccesary
    #   - Process variable wires, performing variable aggregations and producing compiled
    #       variable ports or internal variable assignments, as necessary
    #   - Perform any symbol identifications
    #   - Perform consistency remapping and prefixing inside self, as necessary

    internal_symbol_identifications = []
    output_symbol_identifications = []
    compiled = CompiledPortedObject(self.name, self.parsing_locals)
    compiled.children = {
        name: child.compile(prefix_names=True)
        for name, child in self.children.items()
    }

    # Compile own input ports. Not much happening for input ports.
    for name, input_port in self.input_ports.items():
        # compiled.input_ports[name] = CompiledInputPort(input_port)
        compiled._add_input_port(CompiledInputPort(input_port))

    for name, variable_port in self.variable_ports.items():
        compiled._add_variable_port(CompiledVariablePort(variable_port, 0))

    # For each child input port, we have to ensure it's
    # connected or has a default value

    unconnected_child_input_ports = {}
    unconnected_child_variable_ports = {}
    for child_name, child in compiled.children.items():
        # Collect all child input ports
        for name, port in child.input_ports.items():
            new_name = HIERARCHY_SEPARATOR.join([child_name, name])
            unconnected_child_input_ports[new_name] = port
        # Collect all child variable ports
        for name, port in child.variable_ports.items():
            new_name = HIERARCHY_SEPARATOR.join([child_name, name])
            unconnected_child_variable_ports[new_name] = port
        # Pass forward internal variable/parameter assignments. Their keys are remapped
        # at this point to prevent duplication.
        compiled.internal_variable_assignments.update(
            {
                assg.name: assg
                for assg in child.internal_variable_assignments.values()
            }
        )
        compiled.internal_parameter_assignments.update(
            {
                assg.name: assg
                for assg in child.internal_parameter_assignments.values()
            }
        )
        # Pass forward assignments from output ports. Assignments may later be exposed
        # at an output port by a directed wire.
        # TODO: Unconnected output ports are an indication that something may be wrong
        # If an output port is not connected, we could consider discarding it
        for name, port in child.output_ports.items():
            if assg := port.assignment:
                compiled.internal_parameter_assignments[assg.name] = assg

    # Process directed wires.
    for wire in self.directed_wires:

        # Directed wires connect:
        # - an input port to child input ports, or;
        # - a child output port to child input ports and at most one output port.
        # We take cases on the number of output ports a directed wire connects to.
        outputs = [
            port for port in self.output_ports if port in wire.destination_ports
        ]
        num_outputs = len(outputs)
        if num_outputs > 1:
            # No wire can point to more than one output port
            raise WiringError(
                f"Incorrect wiring in '{self.name}'. "
                f"The directed wire from port {wire.source_port} to {wire.destination_ports}"
                "is connected to two different output ports. "
            )
        elif num_outputs == 1:
            # A wire ending at an output port can only start at a child output port.
            source = compiled._get_port_by_name(wire.source_port, "parameter")
            if type(source) is not CompiledOutputPort:
                raise WiringError(
                    f"Incorrect wiring in '{self.name}'. "
                    f"The directed wire from port {wire.source_port} to {wire.destination_ports}"
                    f"starts at {wire.source_port}, which is not a child output port."
                )

        source = compiled._get_port_by_name(wire.source_port, "parameter")
        # Now we perform the identifications. In the process we check which child ports
        # don't have an incoming wire using unconnected_child_input_ports.
        for destination_port in wire.destination_ports:
            if destination_port in unconnected_child_input_ports:
                # Goes from own input or child output port to child input port.
                # In all of these cases, the ports have been pre-compiled
                destination = compiled._get_port_by_name(
                    destination_port, "parameter"
                )
                assert type(destination) is CompiledInputPort

                # Substitute the destination symbol for the wire symbol
                symb_id = SymbolIdentification(source.symbol, destination.symbol)
                internal_symbol_identifications.append(symb_id)
                unconnected_child_input_ports.pop(destination_port)
            elif destination_port in self.output_ports:
                # We can only be in this case if the source is a child output port,
                # which has already been compiled
                source = compiled._get_port_by_name(wire.source_port, "parameter")
                destination = self._get_port_by_name(destination_port, "parameter")
                assert type(destination) is OutputPort

                # If the source port is an output port, pass the parameter assignment
                # which was at the child output port through to the destination port,
                # and identify the source symbol to the destination symbol.
                if type(source) is CompiledOutputPort:
                    # Substitute the source symbol for the output port symbol
                    symb_id = SymbolIdentification(
                        destination.symbol, source.symbol
                    )
                    output_symbol_identifications.append(symb_id)

                    # Pass forward the assignment at source, currently stored as an
                    # internal parameter assignment, to the output port.
                    source_assg = compiled.internal_parameter_assignments.pop(
                        source.name
                    )
                    compiled._add_output_port(
                        CompiledOutputPort(
                            destination,
                            source_assg,
                        )
                    )
            else:
                raise WiringError(
                    f"Incorrect wiring in '{self.name}'. "
                    "Directed wire destination should be output port "
                    f"or child input port but is {destination_port}"
                )

    # Find unconnected child input ports and check for default values
    bad_input_ports = []
    for name, port in unconnected_child_input_ports.items():
        if port.default_value is None:
            bad_input_ports.append(name)
        else:
            # Initialize their parameters with default values
            assg = DefaultParameterAssignment(
                port.symbol, port.default_value, self.parsing_locals
            )
            compiled.internal_parameter_assignments[name] = assg
    if bad_input_ports:
        raise WiringError(
            f"Incorrect wiring in '{self.name}'. "
            "The following child input ports are unconnected "
            f"and have no default value: {bad_input_ports}"
        )

    # Process variable aggregation wiring
    # TODO: Warn if there is a variable port that is not connected to children
    compiled.variable_ports = {}
    for wiring in self.variable_aggregation_wiring:
        # Collect child ports
        child_ports = []
        for port_name in wiring.child_ports:
            unconnected_child_variable_ports.pop(port_name)
            port = compiled._get_port_by_name(port_name, "variable")
            child_ports.append(port)

        if wiring.parent_port is not None:
            new_var_name = wiring.parent_port
        elif wiring.output_name is not None:
            new_var_name = wiring.output_name
        else:
            raise WiringError(
                f"For VariableAggregationWiring, either parent_port "
                "or output_name need to be provided"
            )
        # Combine RHSs of each child variable, and set child variables equal
        assg = DifferentialAssignment(new_var_name, "0")
        for child in child_ports:
            assg.combine(child.assignment)
            assert isinstance(child.assignment.symbol, sym.Symbol)
            internal_symbol_identifications.append(
                SymbolIdentification(assg.symbol, child.assignment.symbol)
            )
        if wiring.parent_port is not None:
            parent = self._get_port_by_name(wiring.parent_port, "variable")
            new_port = CompiledVariablePort(parent, assg)
            compiled._add_variable_port(new_port)
            # compiled.variable_ports[parent.name] = new_port
        else:
            compiled.internal_variable_assignments[new_var_name] = assg

    # Make unconnected child ports into unexposed variables
    for name, port in unconnected_child_variable_ports.items():
        compiled.internal_variable_assignments[name] = port.assignment

    compiled._sub_symbol_identifications(
        internal_symbol_identifications, output_symbol_identifications
    )

    # Align the dictionary keys with the names of the symbols
    # whose assignments the dictionary is storing.
    # This has to happen after all the wiring compilation,
    # because the wires refer to the child + port name within the child,
    # so the child name cannot be part of the dictionary key while
    # the wiring is compiled. For internal assignments pulled up from
    # children, this remapping has already happened and those dictionaries
    # are unaffected.
    compiled._remap_dict_keys()

    if prefix_names:
        # After this, all variables/parameters appearing everywhere
        # are prefixed by the name of the ported object.
        # This, however, does not apply to the dictionary keys,
        # see above for the reasoning
        compiled._sub_prefixed_symbols()

    return compiled

to_data()

A dismantler method such that every instance X of CompositePortedObject can be recreated by calling X.to_data().to_ported_object()

RETURNS DESCRIPTION
data

a data object capturing the data of self

TYPE: PortedObjectData

Source code in psymple/build/ported_objects.py
def to_data(self) -> PortedObjectData:
    """
    A dismantler method such that every instance X of `CompositePortedObject`
    can be recreated by calling `X.to_data().to_ported_object()`

    Returns:
        data: a data object capturing the data of self
    """
    metadata = {
        "name": self.name,
        "type": "cpo",
    }
    object_data = {
        "children": self._dump_children(),
        "input_ports": self._dump_input_ports(),
        "output_ports": self._dump_output_ports(),
        "variable_ports": self._dump_variable_ports(),
        "directed_wires": self._dump_directed_wires(),
        "variable_wires": self._dump_variable_wires(),
    }
    return PortedObjectData(metadata=metadata, object_data=object_data)