Skip to content
Snippets Groups Projects
signal_flow_graph.py 50.6 KiB
Newer Older
"""
B-ASIC Signal Flow Graph Module.
Oscar Gustafsson's avatar
Oscar Gustafsson committed
import itertools as it
import re
from collections import defaultdict, deque
from io import StringIO
Oscar Gustafsson's avatar
Oscar Gustafsson committed
from numbers import Number
Oscar Gustafsson's avatar
Oscar Gustafsson committed
from typing import (
    DefaultDict,
    Deque,
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    Dict,
    Iterable,
    List,
    MutableSet,
    Optional,
    Sequence,
    Set,
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    Tuple,
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    Union,
    cast,
Oscar Gustafsson's avatar
Oscar Gustafsson committed
)
Oscar Gustafsson's avatar
Oscar Gustafsson committed
from graphviz import Digraph

from b_asic.graph_component import (
    GraphComponent,
    GraphID,
    GraphIDNumber,
    Name,
    TypeName,
)
from b_asic.operation import (
    AbstractOperation,
    MutableDelayMap,
    MutableResultMap,
    Operation,
    ResultKey,
)
from b_asic.port import InputPort, OutputPort, SignalSourceProvider
from b_asic.signal import Signal
Oscar Gustafsson's avatar
Oscar Gustafsson committed
from b_asic.special_operations import Delay, Input, Output

DelayQueue = List[Tuple[str, ResultKey, OutputPort]]


class GraphIDGenerator:
    """Generates Graph IDs for objects."""

    _next_id_number: DefaultDict[TypeName, GraphIDNumber]

    def __init__(self, id_number_offset: GraphIDNumber = GraphIDNumber(0)):
        """Construct a GraphIDGenerator."""
        self._next_id_number = defaultdict(lambda: id_number_offset)

Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def next_id(
        self, type_name: TypeName, used_ids: MutableSet = set()
    ) -> GraphID:
        """Get the next graph id for a certain graph id type."""
        self._next_id_number[type_name] += 1
        new_id = type_name + str(self._next_id_number[type_name])
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        while new_id in used_ids:
            self._next_id_number[type_name] += 1
            new_id = type_name + str(self._next_id_number[type_name])
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        return GraphID(new_id)

    @property
    def id_number_offset(self) -> GraphIDNumber:
        """Get the graph id number offset of this generator."""
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        return GraphIDNumber(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            self._next_id_number.default_factory()
        )  # pylint: disable=not-callable


class SFG(AbstractOperation):
    """
    Signal flow graph.

    Contains a set of connected operations, forming a new operation.
    Used as a base for simulation, scheduling, etc.
    _components_by_id: Dict[GraphID, GraphComponent]
    _components_by_name: DefaultDict[Name, List[GraphComponent]]
    _components_dfs_order: List[GraphComponent]
    _operations_dfs_order: List[Operation]
    _operations_topological_order: List[Operation]
    _graph_id_generator: GraphIDGenerator
    _input_operations: List[Input]
    _output_operations: List[Output]
    _original_components_to_new: Dict[GraphComponent, GraphComponent]
    _original_input_signals_to_indices: Dict[Signal, int]
    _original_output_signals_to_indices: Dict[Signal, int]
    _precedence_list: Optional[List[List[OutputPort]]]
    _used_ids: Set[GraphID] = set()
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def __init__(
        self,
        inputs: Optional[Sequence[Input]] = None,
        outputs: Optional[Sequence[Output]] = None,
        input_signals: Optional[Sequence[Signal]] = None,
        output_signals: Optional[Sequence[Signal]] = None,
        id_number_offset: GraphIDNumber = GraphIDNumber(0),
        name: Name = Name(""),
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        input_sources: Optional[
            Sequence[Optional[SignalSourceProvider]]
        ] = None,
    ):
        """
        Construct an SFG given its inputs and outputs.

        Inputs/outputs may be specified using either Input/Output operations
        directly with the inputs/outputs parameters, or using signals with the
        input_signals/output_signals parameters. If signals are used, the
        corresponding Input/Output operations will be created automatically.

        The id_number_offset parameter specifies what number graph IDs will be
        offset by for each new graph component type. IDs start at 1 by default,
        so the default offset of 0 will result in IDs like "c1", "c2", etc.
        while an offset of 3 will result in "c4", "c5", etc.
        """

        input_signal_count = 0 if input_signals is None else len(input_signals)
        input_operation_count = 0 if inputs is None else len(inputs)
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        output_signal_count = (
            0 if output_signals is None else len(output_signals)
        )
        output_operation_count = 0 if outputs is None else len(outputs)
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        super().__init__(
            input_count=input_signal_count + input_operation_count,
            output_count=output_signal_count + output_operation_count,
            name=name,
            input_sources=input_sources,
        )
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        self._components_by_id = {}
        self._used_ids = set()
        self._components_by_name = defaultdict(list)
        self._components_dfs_order = []
        self._operations_dfs_order = []
        self._operations_topological_order = []
        self._graph_id_generator = GraphIDGenerator(
            GraphIDNumber(id_number_offset)
        )
        self._input_operations = []
        self._output_operations = []
        self._original_components_to_new = {}
        self._original_input_signals_to_indices = {}
        self._original_output_signals_to_indices = {}
        self._precedence_list = None

        # Setup input signals.
        if input_signals is not None:
            for input_index, signal in enumerate(input_signals):
                if signal in self._original_components_to_new:
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    raise ValueError(
                        f"Duplicate input signal {signal!r} in SFG"
                    )
                new_input_op = cast(
                    Input, self._add_component_unconnected_copy(Input())
                )
                new_signal = cast(
                    Signal, self._add_component_unconnected_copy(signal)
                )
                new_signal.set_source(new_input_op.output(0))
                self._input_operations.append(new_input_op)
                self._original_input_signals_to_indices[signal] = input_index
        # Setup input operations, starting from indices after input signals.
        if inputs is not None:
            for input_index, input_op in enumerate(inputs, input_signal_count):
                if input_op in self._original_components_to_new:
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    raise ValueError(
                        f"Duplicate input operation {input_op!r} in SFG"
                    )
                new_input_op = self._add_component_unconnected_copy(input_op)
                for signal in input_op.output(0).signals:
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    assert signal not in self._original_components_to_new, (
                        "Duplicate input signals connected to input ports"
                        " supplied to SFG constructor."
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    )
                    new_signal = self._add_component_unconnected_copy(signal)
                    new_signal.set_source(new_input_op.output(0))
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    self._original_input_signals_to_indices[
                        signal
                    ] = input_index
                self._input_operations.append(new_input_op)
        # Setup output signals.
        if output_signals is not None:
            for output_index, signal in enumerate(output_signals):
                new_output_op = cast(
                    Output, self._add_component_unconnected_copy(Output())
                )
                if signal in self._original_components_to_new:
                    # Signal was already added when setting up inputs.
                    new_signal = cast(
                        Signal, self._original_components_to_new[signal]
                    )
                    new_signal.set_destination(new_output_op.input(0))
                else:
                    # New signal has to be created.
                    new_signal = cast(
                        Signal, self._add_component_unconnected_copy(signal)
                    )
                    new_signal.set_destination(new_output_op.input(0))
                self._output_operations.append(new_output_op)
                self._original_output_signals_to_indices[signal] = output_index
        # Setup output operations, starting from indices after output signals.
        if outputs is not None:
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            for output_index, output_op in enumerate(
                outputs, output_signal_count
            ):
                if output_op in self._original_components_to_new:
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    raise ValueError(
                        f"Duplicate output operation {output_op!r} in SFG"
                    )
                new_output_op = cast(
                    Output, self._add_component_unconnected_copy(output_op)
                )
                for signal in output_op.input(0).signals:
                    if signal in self._original_components_to_new:
                        # Signal was already added when setting up inputs.
                        new_signal = self._original_components_to_new[signal]
                    else:
                        # New signal has to be created.
                        new_signal = self._add_component_unconnected_copy(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                            signal
                        )
                    new_signal.set_destination(new_output_op.input(0))
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    self._original_output_signals_to_indices[
                        signal
                    ] = output_index
                self._output_operations.append(new_output_op)

        output_operations_set = set(self._output_operations)

        # Search the graph inwards from each input signal.
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        for (
            signal,
            input_index,
        ) in self._original_input_signals_to_indices.items():
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            new_signal = cast(Signal, self._original_components_to_new[signal])
            if new_signal.destination is None:
                if signal.destination is None:
                    raise ValueError(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                        f"Input signal #{input_index} is missing destination"
                        " in SFG"
                    )
                if (
                    signal.destination.operation
                    not in self._original_components_to_new
                ):
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                        signal.destination.operation
                    )
            elif new_signal.destination.operation in output_operations_set:
                # Add directly connected input to output to ordered list.
                self._components_dfs_order.extend(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    [
                        new_signal.source.operation,
                        new_signal,
                        new_signal.destination.operation,
                    ]
                )
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    [
                        new_signal.source.operation,
                        new_signal.destination.operation,
                    ]
                )

        # Search the graph inwards from each output signal.
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        for (
            signal,
            output_index,
        ) in self._original_output_signals_to_indices.items():
            # Check if already added source.
            new_signal = self._original_components_to_new[signal]
            if new_signal.source is None:
                if signal.source is None:
                    raise ValueError(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                        f"Output signal #{output_index} is missing source"
                        " in SFG"
                    )
                if (
                    signal.source.operation
                    not in self._original_components_to_new
                ):
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                        signal.source.operation
                    )

    def __str__(self) -> str:
        """Get a string representation of this SFG."""
        string_io = StringIO()
        string_io.write(super().__str__() + "\n")
        string_io.write("Internal Operations:\n")
        line = "-" * 100 + "\n"
        string_io.write(line)

        for operation in self.get_operations_topological_order():
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            string_io.write(f"{operation}\n")
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def __call__(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        self, *src: Optional[SignalSourceProvider], name: Name = Name("")
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    ) -> "SFG":
        """
        Get a new independent SFG instance that is identical to this SFG
        except without any of its external connections.
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        return SFG(
            inputs=self._input_operations,
            outputs=self._output_operations,
            id_number_offset=self.id_number_offset,
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            name=Name(name),
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            input_sources=src if src else None,
        )
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        return TypeName("sfg")

    def evaluate(self, *args):
        result = self.evaluate_outputs(args)
        n = len(result)
        return None if n == 0 else result[0] if n == 1 else result

Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def evaluate_output(
        self,
        index: int,
        input_values: Sequence[Number],
        results: Optional[MutableResultMap] = None,
        delays: Optional[MutableDelayMap] = None,
        prefix: str = "",
        bits_override: Optional[int] = None,
        truncate: bool = True,
    ) -> Number:
        if index < 0 or index >= self.output_count:
            raise IndexError(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                "Output index out of range (expected"
                f" 0-{self.output_count - 1}, got {index})"
            )
        if len(input_values) != self.input_count:
            raise ValueError(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                "Wrong number of inputs supplied to SFG for evaluation"
                f" (expected {self.input_count}, got {len(input_values)})"
            )
        if results is None:
            results = {}
        if delays is None:
            delays = {}

        # Set the values of our input operations to the given input values.
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        for op, arg in zip(
            self._input_operations,
            self.truncate_inputs(input_values, bits_override)
            if truncate
            else input_values,
        ):
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        value = self._evaluate_source(
            self._output_operations[index].input(0).signals[0].source,
            results,
            delays,
            prefix,
            bits_override,
            truncate,
            deferred_delays,
        )
        while deferred_delays:
            new_deferred_delays = []
            for key_base, key, src in deferred_delays:
                self._do_evaluate_source(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                    key_base,
                    key,
                    src,
                    results,
                    delays,
                    prefix,
                    bits_override,
                    truncate,
                    new_deferred_delays,
                )
            deferred_delays = new_deferred_delays
        results[self.key(index, prefix)] = value
        return value

    def connect_external_signals_to_components(self) -> bool:
        """
        Connects any external signals to this SFG's internal operations.
        This SFG becomes unconnected to the SFG it is a component off,
        causing it to become invalid afterwards. Returns True if successful,
        False otherwise.
        if len(self.inputs) != len(self.input_operations):
            raise IndexError(
                f"Number of inputs ({len(self.inputs)}) does not match the"
                f" number of input_operations ({len(self.input_operations)})"
                " in SFG."
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            )
        if len(self.outputs) != len(self.output_operations):
            raise IndexError(
                f"Number of outputs ({len(self.outputs)}) does not match the"
                f" number of output_operations ({len(self.output_operations)})"
                " in SFG."
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            )
        if len(self.input_signals) == 0:
            return False
        if len(self.output_signals) == 0:
            return False

        # For each input_signal, connect it to the corresponding operation
        for port, input_operation in zip(self.inputs, self.input_operations):
            dest = input_operation.output(0).signals[0].destination
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            if dest is None:
                raise ValueError("Missing destination in signal.")
            dest.clear()
            port.signals[0].set_destination(dest)
        # For each output_signal, connect it to the corresponding operation
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        for port, output_operation in zip(
            self.outputs, self.output_operations
        ):
            src = output_operation.input(0).signals[0].source
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            if src is None:
                raise ValueError("Missing soruce in signal.")
            src.clear()
            port.signals[0].set_source(src)
        return True

    @property
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def input_operations(self) -> Sequence[Operation]:
        """
        Get the internal input operations in the same order as their respective input ports.
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def output_operations(self) -> Sequence[Operation]:
        """
        Get the internal output operations in the same order as their respective output ports.
        return self._output_operations

    def split(self) -> Iterable[Operation]:
        return self.operations

Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def to_sfg(self) -> "SFG":
        return self

    def inputs_required_for_output(self, output_index: int) -> Iterable[int]:
        if output_index < 0 or output_index >= self.output_count:
            raise IndexError(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                "Output index out of range (expected"
                f" 0-{self.output_count - 1}, got {output_index})"
            )

        input_indexes_required = []
        sfg_input_operations_to_indexes = {
Oscar Gustafsson's avatar
Oscar Gustafsson committed
            input_op: index
            for index, input_op in enumerate(self._input_operations)
        }
        output_op = self._output_operations[output_index]
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        queue: Deque[Operation] = deque([output_op])
        visited: Set[Operation] = {output_op}
        while queue:
            op = queue.popleft()
            if isinstance(op, Input):
                if op in sfg_input_operations_to_indexes:
                    input_indexes_required.append(
Oscar Gustafsson's avatar
Oscar Gustafsson committed
                        sfg_input_operations_to_indexes[op]
                    )
                    del sfg_input_operations_to_indexes[op]

            for input_port in op.inputs:
                for signal in input_port.signals:
                    if signal.source is not None:
                        new_op = signal.source.operation
                        if new_op not in visited:
                            queue.append(new_op)
                            visited.add(new_op)

        return input_indexes_required

    def copy_component(self, *args, **kwargs) -> GraphComponent:
Oscar Gustafsson's avatar
Oscar Gustafsson committed
        return super().copy_component(
            *args,
            **kwargs,
            inputs=self._input_operations,
            outputs=self._output_operations,
            id_number_offset=self.id_number_offset,
            name=self.name,
        )

    @property
    def id_number_offset(self) -> GraphIDNumber:
        """Get the graph id number offset of the graph id generator for this SFG.
        """
        return self._graph_id_generator.id_number_offset

    @property
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def components(self) -> List[GraphComponent]:
        """Get all components of this graph in depth-first order."""
        return self._components_dfs_order

    @property
Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def operations(self) -> List[Operation]:
        """Get all operations of this graph in depth-first order."""
        return self._operations_dfs_order

Oscar Gustafsson's avatar
Oscar Gustafsson committed
    def find_by_type_name(
        self, type_name: TypeName
    ) -> Sequence[GraphComponent]:
        """
        Find all components in this graph with the specified type name.
        Returns an empty sequence if no components were found.
        Parameters
        ==========
        type_name : TypeName
            The TypeName of the desired components.
Loading
Loading full blame...