Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • da/B-ASIC
  • lukja239/B-ASIC
  • robal695/B-ASIC
3 results
Select Git revision
Show changes
Showing
with 2321 additions and 641 deletions
b_asic/GUI/operation_icons/sqrt_grey.png

2.87 KiB

b_asic/GUI/operation_icons/sub.png

1.13 KiB

b_asic/GUI/operation_icons/sub_grey.png

1.18 KiB

import sys
from PySide2.QtWidgets import QPushButton, QMenu
from PySide2.QtCore import Qt, Signal
class PortButton(QPushButton):
connectionRequested = Signal(QPushButton)
moved = Signal()
def __init__(self, name, operation, port, window, parent=None):
super(PortButton, self).__init__(name, operation, parent)
self.pressed = False
self._window = window
self.port = port
self.operation = operation
self.clicked = 0
self._m_drag = False
self._m_press = False
self.setStyleSheet("background-color: white")
self.connectionRequested.connect(self._window._connect_button)
def contextMenuEvent(self, event):
menu = QMenu()
menu.addAction("Connect", lambda: self.connectionRequested.emit(self))
menu.exec_(self.cursor().pos())
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.select_port(event.modifiers())
super(PortButton, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
super(PortButton, self).mouseReleaseEvent(event)
def _toggle_port(self, pressed=False):
self.pressed = not pressed
self.setStyleSheet(f"background-color: {'white' if not self.pressed else 'grey'}")
def select_port(self, modifiers=None):
if modifiers != Qt.ControlModifier:
for port in self._window.pressed_ports:
port._toggle_port(port.pressed)
self._toggle_port(self.pressed)
self._window.pressed_ports = [self]
else:
self._toggle_port(self.pressed)
if self in self._window.pressed_ports:
self._window.pressed_ports.remove(self)
else:
self._window.pressed_ports.append(self)
for signal in self._window.signalList:
signal.update()
from PySide2.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout,\
QLabel, QCheckBox, QGridLayout
from PySide2.QtCore import Qt
from PySide2.QtGui import QIntValidator
class PropertiesWindow(QDialog):
def __init__(self, operation, main_window):
super(PropertiesWindow, self).__init__()
self.operation = operation
self._window = main_window
self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
self.setWindowTitle("Properties")
self.name_layout = QHBoxLayout()
self.name_layout.setSpacing(50)
self.name_label = QLabel("Name:")
self.edit_name = QLineEdit(self.operation.operation_path_name)
self.name_layout.addWidget(self.name_label)
self.name_layout.addWidget(self.edit_name)
self.latency_fields = dict()
self.vertical_layout = QVBoxLayout()
self.vertical_layout.addLayout(self.name_layout)
if self.operation.operation_path_name == "c":
self.constant_layout = QHBoxLayout()
self.constant_layout.setSpacing(50)
self.constant_value = QLabel("Constant:")
self.edit_constant = QLineEdit(str(self.operation.operation.value))
self.only_accept_int = QIntValidator()
self.edit_constant.setValidator(self.only_accept_int)
self.constant_layout.addWidget(self.constant_value)
self.constant_layout.addWidget(self.edit_constant)
self.vertical_layout.addLayout(self.constant_layout)
self.show_name_layout = QHBoxLayout()
self.check_show_name = QCheckBox("Show name?")
if self.operation.is_show_name:
self.check_show_name.setChecked(1)
else:
self.check_show_name.setChecked(0)
self.check_show_name.setLayoutDirection(Qt.RightToLeft)
self.check_show_name.setStyleSheet("spacing: 170px")
self.show_name_layout.addWidget(self.check_show_name)
self.vertical_layout.addLayout(self.show_name_layout)
if self.operation.operation.input_count > 0:
self.latency_layout = QHBoxLayout()
self.latency_label = QLabel("Set Latency For Input Ports (-1 for None):")
self.latency_layout.addWidget(self.latency_label)
self.vertical_layout.addLayout(self.latency_layout)
input_grid = QGridLayout()
x, y = 0, 0
for i in range(self.operation.operation.input_count):
input_layout = QHBoxLayout()
input_layout.addStretch()
if i % 2 == 0 and i > 0:
x += 1
y = 0
input_label = QLabel("in" + str(i))
input_layout.addWidget(input_label)
input_value = QLineEdit()
try:
input_value.setPlaceholderText(str(self.operation.operation.latency))
except ValueError:
input_value.setPlaceholderText("-1")
int_valid = QIntValidator()
int_valid.setBottom(-1)
input_value.setValidator(int_valid)
input_value.setFixedWidth(50)
self.latency_fields["in" + str(i)] = input_value
input_layout.addWidget(input_value)
input_layout.addStretch()
input_layout.setSpacing(10)
input_grid.addLayout(input_layout, x, y)
y += 1
self.vertical_layout.addLayout(input_grid)
if self.operation.operation.output_count > 0:
self.latency_layout = QHBoxLayout()
self.latency_label = QLabel("Set Latency For Output Ports (-1 for None):")
self.latency_layout.addWidget(self.latency_label)
self.vertical_layout.addLayout(self.latency_layout)
input_grid = QGridLayout()
x, y = 0, 0
for i in range(self.operation.operation.output_count):
input_layout = QHBoxLayout()
input_layout.addStretch()
if i % 2 == 0 and i > 0:
x += 1
y = 0
input_label = QLabel("out" + str(i))
input_layout.addWidget(input_label)
input_value = QLineEdit()
try:
input_value.setPlaceholderText(str(self.operation.operation.latency))
except ValueError:
input_value.setPlaceholderText("-1")
int_valid = QIntValidator()
int_valid.setBottom(-1)
input_value.setValidator(int_valid)
input_value.setFixedWidth(50)
self.latency_fields["out" + str(i)] = input_value
input_layout.addWidget(input_value)
input_layout.addStretch()
input_layout.setSpacing(10)
input_grid.addLayout(input_layout, x, y)
y += 1
self.vertical_layout.addLayout(input_grid)
self.ok = QPushButton("OK")
self.ok.clicked.connect(self.save_properties)
self.vertical_layout.addWidget(self.ok)
self.setLayout(self.vertical_layout)
def save_properties(self):
self._window.logger.info(f"Saving properties of operation: {self.operation.name}.")
self.operation.name = self.edit_name.text()
self.operation.label.setPlainText(self.operation.name)
if self.operation.operation_path_name == "c":
self.operation.operation.value = int(self.edit_constant.text())
if self.check_show_name.isChecked():
self.operation.label.setOpacity(1)
self.operation.is_show_name = True
else:
self.operation.label.setOpacity(0)
self.operation.is_show_name = False
self.operation.operation.set_latency_offsets({port: int(self.latency_fields[port].text()) if self.latency_fields[port].text() and int(self.latency_fields[port].text()) > 0 else None for port in self.latency_fields})
self.reject()
\ No newline at end of file
from PySide2.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout,\
QLabel, QCheckBox, QSpinBox, QGroupBox, QFrame, QFormLayout, QGridLayout, QSizePolicy, QFileDialog, QShortcut, QComboBox
from PySide2.QtCore import Qt, Signal
from PySide2.QtGui import QIntValidator, QKeySequence
from matplotlib.backends import qt_compat
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
class SelectSFGWindow(QDialog):
ok = Signal()
def __init__(self, window):
super(SelectSFGWindow, self).__init__()
self._window = window
self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
self.setWindowTitle("Select SFG")
self.dialog_layout = QVBoxLayout()
self.ok_btn = QPushButton("Ok")
self.ok_btn.clicked.connect(self.save_properties)
self.dialog_layout.addWidget(self.ok_btn)
self.combo_box = QComboBox()
self.sfg = None
self.setLayout(self.dialog_layout)
self.add_sfgs_to_layout()
def add_sfgs_to_layout(self):
for sfg in self._window.sfg_dict:
self.combo_box.addItem(sfg)
self.dialog_layout.addWidget(self.combo_box)
def save_properties(self):
self.sfg = self._window.sfg_dict[self.combo_box.currentText()]
self.accept()
self.ok.emit()
from PySide2.QtWidgets import QDialog, QPushButton, QVBoxLayout, QCheckBox,\
QFrame, QFormLayout
from PySide2.QtCore import Qt, Signal
from b_asic import SFG
class ShowPCWindow(QDialog):
pc = Signal()
def __init__(self, window):
super(ShowPCWindow, self).__init__()
self._window = window
self.check_box_dict = dict()
self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
self.setWindowTitle("Show PC")
self.dialog_layout = QVBoxLayout()
self.pc_btn = QPushButton("Show PC")
self.pc_btn.clicked.connect(self.show_precedence_graph)
self.dialog_layout.addWidget(self.pc_btn)
self.setLayout(self.dialog_layout)
def add_sfg_to_dialog(self):
self.sfg_layout = QVBoxLayout()
self.options_layout = QFormLayout()
for sfg in self._window.sfg_dict:
check_box = QCheckBox()
self.options_layout.addRow(sfg, check_box)
self.check_box_dict[check_box] = sfg
self.sfg_layout.addLayout(self.options_layout)
frame = QFrame()
frame.setFrameShape(QFrame.HLine)
frame.setFrameShadow(QFrame.Sunken)
self.dialog_layout.addWidget(frame)
self.dialog_layout.addLayout(self.sfg_layout)
def show_precedence_graph(self):
for check_box, sfg in self.check_box_dict.items():
if check_box.isChecked():
self._window.logger.info(f"Creating a precedence chart from sfg with name: {sfg}.")
self._window.sfg_dict[sfg].show_precedence_graph()
self.accept()
self.pc.emit()
\ No newline at end of file
from PySide2.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout,\
QLabel, QCheckBox, QSpinBox, QGroupBox, QFrame, QFormLayout, QGridLayout, QSizePolicy, QFileDialog, QShortcut
from PySide2.QtCore import Qt, Signal
from PySide2.QtGui import QIntValidator, QKeySequence
from matplotlib.backends import qt_compat
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
class SimulateSFGWindow(QDialog):
simulate = Signal()
def __init__(self, window):
super(SimulateSFGWindow, self).__init__()
self._window = window
self.properties = dict()
self.sfg_to_layout = dict()
self.input_fields = dict()
self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
self.setWindowTitle("Simulate SFG")
self.dialog_layout = QVBoxLayout()
self.simulate_btn = QPushButton("Simulate")
self.simulate_btn.clicked.connect(self.save_properties)
self.dialog_layout.addWidget(self.simulate_btn)
self.setLayout(self.dialog_layout)
def add_sfg_to_dialog(self, sfg):
sfg_layout = QVBoxLayout()
options_layout = QFormLayout()
name_label = QLabel(f"{sfg.name}")
sfg_layout.addWidget(name_label)
spin_box = QSpinBox()
spin_box.setRange(0, 2147483647)
options_layout.addRow("Iteration Count: ", spin_box)
check_box_plot = QCheckBox()
options_layout.addRow("Plot Results: ", check_box_plot)
check_box_all = QCheckBox()
options_layout.addRow("Get All Results: ", check_box_all)
sfg_layout.addLayout(options_layout)
self.input_fields[sfg] = {
"iteration_count": spin_box,
"show_plot": check_box_plot,
"all_results": check_box_all,
"input_values": []
}
if sfg.input_count > 0:
input_label = QHBoxLayout()
input_label = QLabel("Input Values:")
options_layout.addRow(input_label)
input_grid = QGridLayout()
x, y = 0, 0
for i in range(sfg.input_count):
input_layout = QHBoxLayout()
input_layout.addStretch()
if i % 2 == 0 and i > 0:
x += 1
y = 0
input_label = QLabel("in" + str(i))
input_layout.addWidget(input_label)
input_value = QLineEdit()
input_value.setPlaceholderText("0")
input_value.setValidator(QIntValidator())
input_value.setFixedWidth(50)
input_layout.addWidget(input_value)
input_layout.addStretch()
input_layout.setSpacing(10)
input_grid.addLayout(input_layout, x, y)
self.input_fields[sfg]["input_values"].append(input_value)
y += 1
sfg_layout.addLayout(input_grid)
frame = QFrame()
frame.setFrameShape(QFrame.HLine)
frame.setFrameShadow(QFrame.Sunken)
self.dialog_layout.addWidget(frame)
self.sfg_to_layout[sfg] = sfg_layout
self.dialog_layout.addLayout(sfg_layout)
def save_properties(self):
for sfg, properties in self.input_fields.items():
self.properties[sfg] = {
"iteration_count": self.input_fields[sfg]["iteration_count"].value(),
"show_plot": self.input_fields[sfg]["show_plot"].isChecked(),
"all_results": self.input_fields[sfg]["all_results"].isChecked(),
"input_values": [int(widget.text()) if widget.text() else 0 for widget in self.input_fields[sfg]["input_values"]]
}
# If we plot we should also print the entire data, since you can't really interact with the graph.
if self.properties[sfg]["show_plot"]:
self.properties[sfg]["all_results"] = True
self.accept()
self.simulate.emit()
class Plot(FigureCanvas):
def __init__(self, simulation, sfg, window, parent=None, width=5, height=4, dpi=100):
self.simulation = simulation
self.sfg = sfg
self.dpi = dpi
self._window = window
fig = Figure(figsize=(width, height), dpi=dpi)
fig.suptitle(sfg.name, fontsize=20)
self.axes = fig.add_subplot(111)
FigureCanvas.__init__(self, fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.save_figure = QShortcut(QKeySequence("Ctrl+S"), self)
self.save_figure.activated.connect(self._save_plot_figure)
self._plot_values_sfg()
def _save_plot_figure(self):
self._window.logger.info(f"Saving plot of figure: {self.sfg.name}.")
file_choices = "PNG (*.png)|*.png"
path, ext = QFileDialog.getSaveFileName(self, "Save file", "", file_choices)
path = path.encode("utf-8")
if not path[-4:] == file_choices[-4:].encode("utf-8"):
path += file_choices[-4:].encode("utf-8")
if path:
self.print_figure(path.decode(), dpi=self.dpi)
self._window.logger.info(f"Saved plot: {self.sfg.name} to path: {path}.")
def _plot_values_sfg(self):
x_axis = list(range(len(self.simulation.results["0"])))
for _output in range(self.sfg.output_count):
y_axis = self.simulation.results[str(_output)]
self.axes.plot(x_axis, y_axis)
from PySide2.QtWidgets import QErrorMessage
from traceback import format_exc
def handle_error(fn):
def wrapper(self, *args, **kwargs):
try:
return fn(self, *args, **kwargs)
except Exception as e:
self._window.logger.error(f"Unexpected error: {format_exc()}")
QErrorMessage(self._window).showMessage(f"Unexpected error: {format_exc()}")
return wrapper
def decorate_class(decorator):
def decorate(cls):
for attr in cls.__dict__:
if callable(getattr(cls, attr)):
setattr(cls, attr, decorator(getattr(cls, attr)))
return cls
return decorate
\ No newline at end of file
...@@ -2,13 +2,14 @@ ...@@ -2,13 +2,14 @@
Better ASIC Toolbox. Better ASIC Toolbox.
TODO: More info. TODO: More info.
""" """
from _b_asic import *
from b_asic.core_operations import * from b_asic.core_operations import *
from b_asic.graph_component import * from b_asic.graph_component import *
from b_asic.graph_id import *
from b_asic.operation import * from b_asic.operation import *
from b_asic.precedence_chart import *
from b_asic.port import * from b_asic.port import *
from b_asic.schema import *
from b_asic.signal_flow_graph import * from b_asic.signal_flow_graph import *
from b_asic.signal import * from b_asic.signal import *
from b_asic.simulation import * from b_asic.simulation import *
from b_asic.special_operations import *
from b_asic.save_load_structure import *
from b_asic.schema import *
...@@ -4,43 +4,39 @@ TODO: More info. ...@@ -4,43 +4,39 @@ TODO: More info.
""" """
from numbers import Number from numbers import Number
from typing import Any from typing import Optional, Dict
from numpy import conjugate, sqrt, abs as np_abs from numpy import conjugate, sqrt, abs as np_abs
from b_asic.port import InputPort, OutputPort
from b_asic.graph_id import GraphIDType from b_asic.port import SignalSourceProvider, InputPort, OutputPort
from b_asic.operation import AbstractOperation from b_asic.operation import AbstractOperation
from b_asic.graph_component import Name, TypeName from b_asic.graph_component import Name, TypeName
class Input(AbstractOperation):
"""Input operation.
TODO: More info.
"""
# TODO: Implement all functions.
@property
def type_name(self) -> TypeName:
return "in"
class Constant(AbstractOperation): class Constant(AbstractOperation):
"""Constant value operation. """Constant value operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, value: Number = 0, name: Name = ""): def __init__(self, value: Number = 0, name: Name = ""):
super().__init__(name) super().__init__(input_count=0, output_count=1, name=name)
self.set_param("value", value)
self._output_ports = [OutputPort(0, self)] @classmethod
self._parameters["value"] = value def type_name(cls) -> TypeName:
return "c"
def evaluate(self): def evaluate(self):
return self.param("value") return self.param("value")
@property @property
def type_name(self) -> TypeName: def value(self) -> Number:
return "c" """Get the constant value of this operation."""
return self.param("value")
@value.setter
def value(self, value: Number) -> None:
"""Set the constant value of this operation."""
return self.set_param("value", value)
class Addition(AbstractOperation): class Addition(AbstractOperation):
...@@ -48,134 +44,86 @@ class Addition(AbstractOperation): ...@@ -48,134 +44,86 @@ class Addition(AbstractOperation):
TODO: More info. TODO: More info.
""" """
def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
latency=latency, latency_offsets=latency_offsets)
self._input_ports = [InputPort(0, self), InputPort(1, self)]
self._output_ports = [OutputPort(0, self)]
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
if source2 is not None: return "add"
self._input_ports[1].connect(source2)
def evaluate(self, a, b): def evaluate(self, a, b):
return a + b return a + b
@property
def type_name(self) -> TypeName:
return "add"
class Subtraction(AbstractOperation): class Subtraction(AbstractOperation):
"""Binary subtraction operation. """Binary subtraction operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
self._input_ports = [InputPort(0, self), InputPort(1, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
if source2 is not None: return "sub"
self._input_ports[1].connect(source2)
def evaluate(self, a, b): def evaluate(self, a, b):
return a - b return a - b
@property
def type_name(self) -> TypeName:
return "sub"
class Multiplication(AbstractOperation): class Multiplication(AbstractOperation):
"""Binary multiplication operation. """Binary multiplication operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
self._input_ports = [InputPort(0, self), InputPort(1, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
if source2 is not None: return "mul"
self._input_ports[1].connect(source2)
def evaluate(self, a, b): def evaluate(self, a, b):
return a * b return a * b
@property
def type_name(self) -> TypeName:
return "mul"
class Division(AbstractOperation): class Division(AbstractOperation):
"""Binary division operation. """Binary division operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
self._input_ports = [InputPort(0, self), InputPort(1, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
if source2 is not None: return "div"
self._input_ports[1].connect(source2)
def evaluate(self, a, b): def evaluate(self, a, b):
return a / b return a / b
@property
def type_name(self) -> TypeName:
return "div"
class SquareRoot(AbstractOperation):
"""Unary square root operation.
TODO: More info.
"""
def __init__(self, source1: OutputPort = None, name: Name = ""):
super().__init__(name)
self._input_ports = [InputPort(0, self)]
self._output_ports = [OutputPort(0, self)]
if source1 is not None:
self._input_ports[0].connect(source1)
def evaluate(self, a):
return sqrt((complex)(a))
@property
def type_name(self) -> TypeName:
return "sqrt"
class Min(AbstractOperation):
class ComplexConjugate(AbstractOperation): """Binary min operation.
"""Unary complex conjugate operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, source1: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
self._input_ports = [InputPort(0, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
return "min"
def evaluate(self, a):
return conjugate(a)
@property def evaluate(self, a, b):
def type_name(self) -> TypeName: assert not isinstance(a, complex) and not isinstance(b, complex), \
return "conj" ("core_operations.Min does not support complex numbers.")
return a if a < b else b
class Max(AbstractOperation): class Max(AbstractOperation):
...@@ -183,155 +131,129 @@ class Max(AbstractOperation): ...@@ -183,155 +131,129 @@ class Max(AbstractOperation):
TODO: More info. TODO: More info.
""" """
def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
self._input_ports = [InputPort(0, self), InputPort(1, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
if source2 is not None: return "max"
self._input_ports[1].connect(source2)
def evaluate(self, a, b): def evaluate(self, a, b):
assert not isinstance(a, complex) and not isinstance(b, complex), \ assert not isinstance(a, complex) and not isinstance(b, complex), \
("core_operations.Max does not support complex numbers.") ("core_operations.Max does not support complex numbers.")
return a if a > b else b return a if a > b else b
@property
def type_name(self) -> TypeName:
return "max"
class SquareRoot(AbstractOperation):
class Min(AbstractOperation): """Unary square root operation.
"""Binary min operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0],
self._input_ports = [InputPort(0, self), InputPort(1, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
if source1 is not None:
self._input_ports[0].connect(source1)
if source2 is not None:
self._input_ports[1].connect(source2)
def evaluate(self, a, b): @classmethod
assert not isinstance(a, complex) and not isinstance(b, complex), \ def type_name(cls) -> TypeName:
("core_operations.Min does not support complex numbers.") return "sqrt"
return a if a < b else b
@property def evaluate(self, a):
def type_name(self) -> TypeName: return sqrt(complex(a))
return "min"
class Absolute(AbstractOperation): class ComplexConjugate(AbstractOperation):
"""Unary absolute value operation. """Unary complex conjugate operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, source1: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0],
self._input_ports = [InputPort(0, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
return "conj"
def evaluate(self, a): def evaluate(self, a):
return np_abs(a) return conjugate(a)
@property
def type_name(self) -> TypeName:
return "abs"
class ConstantMultiplication(AbstractOperation): class Absolute(AbstractOperation):
"""Unary constant multiplication operation. """Unary absolute value operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0],
self._input_ports = [InputPort(0, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
self._parameters["coefficient"] = coefficient
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
return "abs"
def evaluate(self, a): def evaluate(self, a):
return a * self.param("coefficient") return np_abs(a)
@property
def type_name(self) -> TypeName:
return "cmul"
class ConstantAddition(AbstractOperation): class ConstantMultiplication(AbstractOperation):
"""Unary constant addition operation. """Unary constant multiplication operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""): def __init__(self, value: Number = 0, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0],
self._input_ports = [InputPort(0, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)] self.set_param("value", value)
self._parameters["coefficient"] = coefficient
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
return "cmul"
def evaluate(self, a): def evaluate(self, a):
return a + self.param("coefficient") return a * self.param("value")
@property @property
def type_name(self) -> TypeName: def value(self) -> Number:
return "cadd" """Get the constant value of this operation."""
return self.param("value")
@value.setter
def value(self, value: Number) -> None:
"""Set the constant value of this operation."""
return self.set_param("value", value)
class ConstantSubtraction(AbstractOperation): class Butterfly(AbstractOperation):
"""Unary constant subtraction operation. """Butterfly operation that returns two outputs.
The first output is a + b and the second output is a - b.
TODO: More info. TODO: More info.
""" """
def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=2, output_count=2, name=name, input_sources=[src0, src1],
self._input_ports = [InputPort(0, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
self._parameters["coefficient"] = coefficient
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
return "bfly"
def evaluate(self, a): def evaluate(self, a, b):
return a - self.param("coefficient") return a + b, a - b
@property
def type_name(self) -> TypeName:
return "csub"
class ConstantDivision(AbstractOperation): class MAD(AbstractOperation):
"""Unary constant division operation. """Multiply-and-add operation.
TODO: More info. TODO: More info.
""" """
def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""): def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, src2: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(input_count=3, output_count=1, name=name, input_sources=[src0, src1, src2],
self._input_ports = [InputPort(0, self)] latency=latency, latency_offsets=latency_offsets)
self._output_ports = [OutputPort(0, self)]
self._parameters["coefficient"] = coefficient
if source1 is not None: @classmethod
self._input_ports[0].connect(source1) def type_name(cls) -> TypeName:
return "mad"
def evaluate(self, a): def evaluate(self, a, b, c):
return a / self.param("coefficient") return a * b + c
@property
def type_name(self) -> TypeName:
return "cdiv"
...@@ -4,10 +4,15 @@ TODO: More info. ...@@ -4,10 +4,15 @@ TODO: More info.
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import NewType from collections import deque
from copy import copy, deepcopy
from typing import NewType, Any, Dict, Mapping, Iterable, Generator
Name = NewType("Name", str) Name = NewType("Name", str)
TypeName = NewType("TypeName", str) TypeName = NewType("TypeName", str)
GraphID = NewType("GraphID", str)
GraphIDNumber = NewType("GraphIDNumber", int)
class GraphComponent(ABC): class GraphComponent(ABC):
...@@ -15,35 +20,94 @@ class GraphComponent(ABC): ...@@ -15,35 +20,94 @@ class GraphComponent(ABC):
TODO: More info. TODO: More info.
""" """
@property @classmethod
@abstractmethod @abstractmethod
def type_name(self) -> TypeName: def type_name(cls) -> TypeName:
"""Return the type name of the graph component""" """Get the type name of this graph component"""
raise NotImplementedError raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def name(self) -> Name: def name(self) -> Name:
"""Return the name of the graph component.""" """Get the name of this graph component."""
raise NotImplementedError raise NotImplementedError
@name.setter @name.setter
@abstractmethod @abstractmethod
def name(self, name: Name) -> None: def name(self, name: Name) -> None:
"""Set the name of the graph component to the entered name.""" """Set the name of this graph component to the given name."""
raise NotImplementedError
@property
@abstractmethod
def graph_id(self) -> GraphID:
"""Get the graph id of this graph component."""
raise NotImplementedError
@graph_id.setter
@abstractmethod
def graph_id(self, graph_id: GraphID) -> None:
"""Set the graph id of this graph component to the given id.
Note that this id will be ignored if this component is used to create a new graph,
and that a new local id will be generated for it instead."""
raise NotImplementedError
@property
@abstractmethod
def params(self) -> Mapping[str, Any]:
"""Get a dictionary of all parameter values."""
raise NotImplementedError
@abstractmethod
def param(self, name: str) -> Any:
"""Get the value of a parameter.
Returns None if the parameter is not defined.
"""
raise NotImplementedError
@abstractmethod
def set_param(self, name: str, value: Any) -> None:
"""Set the value of a parameter.
Adds the parameter if it is not already defined.
"""
raise NotImplementedError
@abstractmethod
def copy_component(self, *args, **kwargs) -> "GraphComponent":
"""Get a new instance of this graph component type with the same name, id and parameters."""
raise NotImplementedError
@property
@abstractmethod
def neighbors(self) -> Iterable["GraphComponent"]:
"""Get all components that are directly connected to this operation."""
raise NotImplementedError
@abstractmethod
def traverse(self) -> Generator["GraphComponent", None, None]:
"""Get a generator that recursively iterates through all components that are connected to this operation,
as well as the ones that they are connected to.
"""
raise NotImplementedError raise NotImplementedError
class AbstractGraphComponent(GraphComponent): class AbstractGraphComponent(GraphComponent):
"""Abstract Graph Component class which is a component of a signal flow graph. """Abstract Graph Component class which is a component of a signal flow graph.
TODO: More info. TODO: More info.
""" """
_name: Name _name: Name
_graph_id: GraphID
_parameters: Dict[str, Any]
def __init__(self, name: Name = ""): def __init__(self, name: Name = ""):
self._name = name self._name = name
self._graph_id = ""
self._parameters = {}
def __str__(self):
return f"id: {self.graph_id if self.graph_id else 'no_id'}, \tname: {self.name if self.name else 'no_name'}" + \
"".join((f", \t{key}: {str(param)}" for key, param in self._parameters.items()))
@property @property
def name(self) -> Name: def name(self) -> Name:
...@@ -52,3 +116,41 @@ class AbstractGraphComponent(GraphComponent): ...@@ -52,3 +116,41 @@ class AbstractGraphComponent(GraphComponent):
@name.setter @name.setter
def name(self, name: Name) -> None: def name(self, name: Name) -> None:
self._name = name self._name = name
@property
def graph_id(self) -> GraphID:
return self._graph_id
@graph_id.setter
def graph_id(self, graph_id: GraphID) -> None:
self._graph_id = graph_id
@property
def params(self) -> Mapping[str, Any]:
return self._parameters.copy()
def param(self, name: str) -> Any:
return self._parameters.get(name)
def set_param(self, name: str, value: Any) -> None:
self._parameters[name] = value
def copy_component(self, *args, **kwargs) -> GraphComponent:
new_component = self.__class__(*args, **kwargs)
new_component.name = copy(self.name)
new_component.graph_id = copy(self.graph_id)
for name, value in self.params.items():
new_component.set_param(copy(name), deepcopy(value)) # pylint: disable=no-member
return new_component
def traverse(self) -> Generator[GraphComponent, None, None]:
# Breadth first search.
visited = {self}
fontier = deque([self])
while fontier:
component = fontier.popleft()
yield component
for neighbor in component.neighbors:
if neighbor not in visited:
visited.add(neighbor)
fontier.append(neighbor)
"""@package docstring
B-ASIC Graph ID module for handling IDs of different objects in a graph.
TODO: More info
"""
from collections import defaultdict
from typing import NewType, DefaultDict
GraphID = NewType("GraphID", str)
GraphIDType = NewType("GraphIDType", str)
GraphIDNumber = NewType("GraphIDNumber", int)
class GraphIDGenerator:
"""A class that generates Graph IDs for objects."""
_next_id_number: DefaultDict[GraphIDType, GraphIDNumber]
def __init__(self):
self._next_id_number = defaultdict(lambda: 1) # Initalises every key element to 1
def get_next_id(self, graph_id_type: GraphIDType) -> GraphID:
"""Return the next graph id for a certain graph id type."""
graph_id = graph_id_type + str(self._next_id_number[graph_id_type])
self._next_id_number[graph_id_type] += 1 # Increase the current id number
return graph_id
...@@ -3,92 +3,241 @@ B-ASIC Operation Module. ...@@ -3,92 +3,241 @@ B-ASIC Operation Module.
TODO: More info. TODO: More info.
""" """
from b_asic.signal import Signal
from b_asic.port import SignalSourceProvider, InputPort, OutputPort
from b_asic.graph_component import GraphComponent, AbstractGraphComponent, Name
import itertools as it
from math import trunc
import collections
from abc import abstractmethod from abc import abstractmethod
from numbers import Number from numbers import Number
from typing import List, Dict, Optional, Any, Set, TYPE_CHECKING from typing import NewType, List, Dict, Sequence, Iterable, Mapping, MutableMapping, Optional, Any, Set, Union
from collections import deque
from b_asic.graph_component import GraphComponent, AbstractGraphComponent, Name
from b_asic.simulation import SimulationState, OperationState
from b_asic.signal import Signal
if TYPE_CHECKING:
from b_asic.port import InputPort, OutputPort
ResultKey = NewType("ResultKey", str)
ResultMap = Mapping[ResultKey, Optional[Number]]
MutableResultMap = MutableMapping[ResultKey, Optional[Number]]
DelayMap = Mapping[ResultKey, Number]
MutableDelayMap = MutableMapping[ResultKey, Number]
class Operation(GraphComponent): class Operation(GraphComponent, SignalSourceProvider):
"""Operation interface. """Operation interface.
TODO: More info. TODO: More info.
""" """
@abstractmethod @abstractmethod
def inputs(self) -> "List[InputPort]": def __add__(self, src: Union[SignalSourceProvider, Number]) -> "Addition":
"""Get a list of all input ports.""" """Overloads the addition operator to make it return a new Addition operation
object that is connected to the self and other objects.
"""
raise NotImplementedError
@abstractmethod
def __radd__(self, src: Union[SignalSourceProvider, Number]) -> "Addition":
"""Overloads the addition operator to make it return a new Addition operation
object that is connected to the self and other objects.
"""
raise NotImplementedError
@abstractmethod
def __sub__(self, src: Union[SignalSourceProvider, Number]) -> "Subtraction":
"""Overloads the subtraction operator to make it return a new Subtraction operation
object that is connected to the self and other objects.
"""
raise NotImplementedError
@abstractmethod
def __rsub__(self, src: Union[SignalSourceProvider, Number]) -> "Subtraction":
"""Overloads the subtraction operator to make it return a new Subtraction operation
object that is connected to the self and other objects.
"""
raise NotImplementedError
@abstractmethod
def __mul__(self, src: Union[SignalSourceProvider, Number]) -> "Union[Multiplication, ConstantMultiplication]":
"""Overloads the multiplication operator to make it return a new Multiplication operation
object that is connected to the self and other objects. If other is a number then
returns a ConstantMultiplication operation object instead.
"""
raise NotImplementedError
@abstractmethod
def __rmul__(self, src: Union[SignalSourceProvider, Number]) -> "Union[Multiplication, ConstantMultiplication]":
"""Overloads the multiplication operator to make it return a new Multiplication operation
object that is connected to the self and other objects. If other is a number then
returns a ConstantMultiplication operation object instead.
"""
raise NotImplementedError
@abstractmethod
def __truediv__(self, src: Union[SignalSourceProvider, Number]) -> "Division":
"""Overloads the division operator to make it return a new Division operation
object that is connected to the self and other objects.
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def outputs(self) -> "List[OutputPort]": def __rtruediv__(self, src: Union[SignalSourceProvider, Number]) -> "Division":
"""Get a list of all output ports.""" """Overloads the division operator to make it return a new Division operation
object that is connected to the self and other objects.
"""
raise NotImplementedError
@abstractmethod
def __lshift__(self, src: SignalSourceProvider) -> Signal:
"""Overloads the left shift operator to make it connect the provided signal source
to this operation's input, assuming it has exactly 1 input port.
Returns the new signal.
"""
raise NotImplementedError raise NotImplementedError
@property
@abstractmethod @abstractmethod
def input_count(self) -> int: def input_count(self) -> int:
"""Get the number of input ports.""" """Get the number of input ports."""
raise NotImplementedError raise NotImplementedError
@property
@abstractmethod @abstractmethod
def output_count(self) -> int: def output_count(self) -> int:
"""Get the number of output ports.""" """Get the number of output ports."""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def input(self, i: int) -> "InputPort": def input(self, index: int) -> InputPort:
"""Get the input port at index i.""" """Get the input port at the given index."""
raise NotImplementedError
@abstractmethod
def output(self, index: int) -> OutputPort:
"""Get the output port at the given index."""
raise NotImplementedError
@property
@abstractmethod
def inputs(self) -> Sequence[InputPort]:
"""Get all input ports."""
raise NotImplementedError
@property
@abstractmethod
def outputs(self) -> Sequence[OutputPort]:
"""Get all output ports."""
raise NotImplementedError
@property
@abstractmethod
def input_signals(self) -> Iterable[Signal]:
"""Get all the signals that are connected to this operation's input ports,
in no particular order.
"""
raise NotImplementedError
@property
@abstractmethod
def output_signals(self) -> Iterable[Signal]:
"""Get all the signals that are connected to this operation's output ports,
in no particular order.
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def output(self, i: int) -> "OutputPort": def key(self, index: int, prefix: str = "") -> ResultKey:
"""Get the output port at index i.""" """Get the key used to access the output of a certain output of this operation
from the output parameter passed to current_output(s) or evaluate_output(s).
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def params(self) -> Dict[str, Optional[Any]]: def current_output(self, index: int, delays: Optional[DelayMap] = None, prefix: str = "") -> Optional[Number]:
"""Get a dictionary of all parameter values.""" """Get the current output at the given index of this operation, if available.
The delays parameter will be used for lookup.
The prefix parameter will be used as a prefix for the key string when looking for delays.
See also: current_outputs, evaluate_output, evaluate_outputs.
"""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def param(self, name: str) -> Optional[Any]: 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:
"""Get the value of a parameter. """Evaluate the output at the given index of this operation with the given input values.
Returns None if the parameter is not defined. The results parameter will be used to store any results (including intermediate results) for caching.
The delays parameter will be used to get the current value of any intermediate delays that are encountered, and be updated with their new values.
The prefix parameter will be used as a prefix for the key string when storing results/delays.
The bits_override parameter specifies a word length override when truncating inputs which ignores the word length specified by the input signal.
The truncate parameter specifies whether input truncation should be enabled in the first place. If set to False, input values will be used driectly without any bit truncation.
See also: evaluate_outputs, current_output, current_outputs.
""" """
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def set_param(self, name: str, value: Any) -> None: def current_outputs(self, delays: Optional[DelayMap] = None, prefix: str = "") -> Sequence[Optional[Number]]:
"""Set the value of a parameter. """Get all current outputs of this operation, if available.
The parameter must be defined. See current_output for more information.
""" """
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def evaluate_outputs(self, state: "SimulationState") -> List[Number]: def evaluate_outputs(self, input_values: Sequence[Number], results: Optional[MutableResultMap] = None, delays: Optional[MutableDelayMap] = None, prefix: str = "", bits_override: Optional[int] = None, truncate: bool = True) -> Sequence[Number]:
"""Simulate the circuit until its iteration count matches that of the simulation state, """Evaluate all outputs of this operation given the input values.
then return the resulting output vector. See evaluate_output for more information.
""" """
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def split(self) -> "List[Operation]": def split(self) -> Iterable["Operation"]:
"""Split the operation into multiple operations. """Split the operation into multiple operations.
If splitting is not possible, this may return a list containing only the operation itself. If splitting is not possible, this may return a list containing only the operation itself.
""" """
raise NotImplementedError raise NotImplementedError
@abstractmethod
def inputs_required_for_output(self, output_index: int) -> Iterable[int]:
"""Get the input indices of all inputs in this operation whose values are required in order to evaluate the output at the given output index."""
raise NotImplementedError
@abstractmethod
def truncate_input(self, index: int, value: Number, bits: int) -> Number:
"""Truncate the value to be used as input at the given index to a certain bit length."""
raise NotImplementedError
@abstractmethod
def to_sfg(self) -> "SFG":
"""Convert the operation into its corresponding SFG.
If the operation is composed by multiple operations, the operation will be split.
"""
raise NotImplementedError
@property
@abstractmethod
def latency(self) -> int:
"""Get the latency of the operation, which is the longest time it takes from one of
the operations inputport to one of the operations outputport.
"""
raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def neighbors(self) -> "List[Operation]": def latency_offsets(self) -> Sequence[Sequence[int]]:
"""Return all operations that are connected by signals to this operation. """Get a nested list with all the operations ports latency-offsets, the first list contains the
If no neighbors are found, this returns an empty list. latency-offsets of the operations input ports, the second list contains the latency-offsets of
the operations output ports.
"""
raise NotImplementedError
@abstractmethod
def set_latency(self, latency: int) -> None:
"""Sets the latency of the operation to the specified integer value by setting the
latency-offsets of operations input ports to 0 and the latency-offsets of the operations
output ports to the specified value. The latency cannot be a negative integers.
"""
raise NotImplementedError
@abstractmethod
def set_latency_offsets(self, latency_offsets: Dict[str, int]) -> None:
"""Sets the latency-offsets for the operations ports specified in the latency_offsets dictionary.
The latency offsets dictionary should be {'in0': 2, 'out1': 4} if you want to set the latency offset
for the inport port with index 0 to 2, and the latency offset of the output port with index 1 to 4.
""" """
raise NotImplementedError raise NotImplementedError
...@@ -98,171 +247,345 @@ class AbstractOperation(Operation, AbstractGraphComponent): ...@@ -98,171 +247,345 @@ class AbstractOperation(Operation, AbstractGraphComponent):
TODO: More info. TODO: More info.
""" """
_input_ports: List["InputPort"] _input_ports: List[InputPort]
_output_ports: List["OutputPort"] _output_ports: List[OutputPort]
_parameters: Dict[str, Optional[Any]]
def __init__(self, name: Name = ""): def __init__(self, input_count: int, output_count: int, name: Name = "", input_sources: Optional[Sequence[Optional[SignalSourceProvider]]] = None, latency: int = None, latency_offsets: Dict[str, int] = None):
super().__init__(name) super().__init__(name)
self._input_ports = []
self._output_ports = [] self._input_ports = [InputPort(self, i) for i in range(input_count)]
self._parameters = {} self._output_ports = [OutputPort(self, i) for i in range(output_count)]
# Connect given input sources, if any.
if input_sources is not None:
source_count = len(input_sources)
if source_count != input_count:
raise ValueError(
f"Wrong number of input sources supplied to Operation (expected {input_count}, got {source_count})")
for i, src in enumerate(input_sources):
if src is not None:
self._input_ports[i].connect(src.source)
ports_without_latency_offset = set(([f"in{i}" for i in range(self.input_count)] +
[f"out{i}" for i in range(self.output_count)]))
if latency_offsets is not None:
self.set_latency_offsets(latency_offsets)
if latency is not None:
# Set the latency of the rest of ports with no latency_offset.
assert latency >= 0, "Negative latency entered"
for inp in self.inputs:
if inp.latency_offset is None:
inp.latency_offset = 0
for outp in self.outputs:
if outp.latency_offset is None:
outp.latency_offset = latency
@abstractmethod @abstractmethod
def evaluate(self, *inputs) -> Any: # pylint: disable=arguments-differ def evaluate(self, *inputs) -> Any: # pylint: disable=arguments-differ
"""Evaluate the operation and generate a list of output values given a """Evaluate the operation and generate a list of output values given a list of input values."""
list of input values.
"""
raise NotImplementedError raise NotImplementedError
def inputs(self) -> List["InputPort"]: def __add__(self, src: Union[SignalSourceProvider, Number]) -> "Addition":
return self._input_ports.copy() # Import here to avoid circular imports.
from b_asic.core_operations import Constant, Addition
return Addition(self, Constant(src) if isinstance(src, Number) else src)
def __radd__(self, src: Union[SignalSourceProvider, Number]) -> "Addition":
# Import here to avoid circular imports.
from b_asic.core_operations import Constant, Addition
return Addition(Constant(src) if isinstance(src, Number) else src, self)
def __sub__(self, src: Union[SignalSourceProvider, Number]) -> "Subtraction":
# Import here to avoid circular imports.
from b_asic.core_operations import Constant, Subtraction
return Subtraction(self, Constant(src) if isinstance(src, Number) else src)
def __rsub__(self, src: Union[SignalSourceProvider, Number]) -> "Subtraction":
# Import here to avoid circular imports.
from b_asic.core_operations import Constant, Subtraction
return Subtraction(Constant(src) if isinstance(src, Number) else src, self)
def __mul__(self, src: Union[SignalSourceProvider, Number]) -> "Union[Multiplication, ConstantMultiplication]":
# Import here to avoid circular imports.
from b_asic.core_operations import Multiplication, ConstantMultiplication
return ConstantMultiplication(src, self) if isinstance(src, Number) else Multiplication(self, src)
def __rmul__(self, src: Union[SignalSourceProvider, Number]) -> "Union[Multiplication, ConstantMultiplication]":
# Import here to avoid circular imports.
from b_asic.core_operations import Multiplication, ConstantMultiplication
return ConstantMultiplication(src, self) if isinstance(src, Number) else Multiplication(src, self)
def __truediv__(self, src: Union[SignalSourceProvider, Number]) -> "Division":
# Import here to avoid circular imports.
from b_asic.core_operations import Constant, Division
return Division(self, Constant(src) if isinstance(src, Number) else src)
def outputs(self) -> List["OutputPort"]: def __rtruediv__(self, src: Union[SignalSourceProvider, Number]) -> "Division":
return self._output_ports.copy() # Import here to avoid circular imports.
from b_asic.core_operations import Constant, Division
return Division(Constant(src) if isinstance(src, Number) else src, self)
def __lshift__(self, src: SignalSourceProvider) -> Signal:
if self.input_count != 1:
diff = "more" if self.input_count > 1 else "less"
raise TypeError(
f"{self.__class__.__name__} cannot be used as a destination because it has {diff} than 1 input")
return self.input(0).connect(src)
def __str__(self):
inputs_dict = dict()
for i, port in enumerate(self.inputs):
if port.signal_count == 0:
inputs_dict[i] = '-'
break
dict_ele = []
for signal in port.signals:
if signal.source:
if signal.source.operation.graph_id:
dict_ele.append(signal.source.operation.graph_id)
else:
dict_ele.append("no_id")
else:
if signal.graph_id:
dict_ele.append(signal.graph_id)
else:
dict_ele.append("no_id")
inputs_dict[i] = dict_ele
outputs_dict = dict()
for i, port in enumerate(self.outputs):
if port.signal_count == 0:
outputs_dict[i] = '-'
break
dict_ele = []
for signal in port.signals:
if signal.destination:
if signal.destination.operation.graph_id:
dict_ele.append(signal.destination.operation.graph_id)
else:
dict_ele.append("no_id")
else:
if signal.graph_id:
dict_ele.append(signal.graph_id)
else:
dict_ele.append("no_id")
outputs_dict[i] = dict_ele
return super().__str__() + f", \tinputs: {str(inputs_dict)}, \toutputs: {str(outputs_dict)}"
@property
def input_count(self) -> int: def input_count(self) -> int:
return len(self._input_ports) return len(self._input_ports)
@property
def output_count(self) -> int: def output_count(self) -> int:
return len(self._output_ports) return len(self._output_ports)
def input(self, i: int) -> "InputPort": def input(self, index: int) -> InputPort:
return self._input_ports[i] return self._input_ports[index]
def output(self, i: int) -> "OutputPort": def output(self, index: int) -> OutputPort:
return self._output_ports[i] return self._output_ports[index]
def params(self) -> Dict[str, Optional[Any]]:
return self._parameters.copy()
def param(self, name: str) -> Optional[Any]:
return self._parameters.get(name)
def set_param(self, name: str, value: Any) -> None:
assert name in self._parameters # TODO: Error message.
self._parameters[name] = value
def evaluate_outputs(self, state: SimulationState) -> List[Number]:
# TODO: Check implementation.
input_count: int = self.input_count()
output_count: int = self.output_count()
assert input_count == len(self._input_ports) # TODO: Error message.
assert output_count == len(self._output_ports) # TODO: Error message.
self_state: OperationState = state.operation_states[self]
while self_state.iteration < state.iteration:
input_values: List[Number] = [0] * input_count
for i in range(input_count):
source: Signal = self._input_ports[i].signal
input_values[i] = source.operation.evaluate_outputs(state)[
source.port_index]
self_state.output_values = self.evaluate(input_values)
# TODO: Error message.
assert len(self_state.output_values) == output_count
self_state.iteration += 1
for i in range(output_count):
for signal in self._output_ports[i].signals():
destination: Signal = signal.destination
destination.evaluate_outputs(state)
return self_state.output_values
def split(self) -> List[Operation]:
# TODO: Check implementation.
results = self.evaluate(self._input_ports)
if all(isinstance(e, Operation) for e in results):
return results
return [self]
@property @property
def neighbors(self) -> List[Operation]: def inputs(self) -> Sequence[InputPort]:
neighbors: List[Operation] = [] return self._input_ports
for port in self._input_ports:
for signal in port.signals:
neighbors.append(signal.source.operation)
for port in self._output_ports: @property
for signal in port.signals: def outputs(self) -> Sequence[OutputPort]:
neighbors.append(signal.destination.operation) return self._output_ports
return neighbors @property
def input_signals(self) -> Iterable[Signal]:
def traverse(self) -> Operation: result = []
"""Traverse the operation tree and return a generator with start point in the operation.""" for p in self.inputs:
return self._breadth_first_search() for s in p.signals:
result.append(s)
def _breadth_first_search(self) -> Operation: return result
"""Use breadth first search to traverse the operation tree."""
visited: Set[Operation] = {self}
queue = deque([self])
while queue:
operation = queue.popleft()
yield operation
for n_operation in operation.neighbors:
if n_operation not in visited:
visited.add(n_operation)
queue.append(n_operation)
def __add__(self, other):
"""Overloads the addition operator to make it return a new Addition operation
object that is connected to the self and other objects. If other is a number then
returns a ConstantAddition operation object instead.
"""
# Import here to avoid circular imports.
from b_asic.core_operations import Addition, ConstantAddition
if isinstance(other, Operation): @property
return Addition(self.output(0), other.output(0)) def output_signals(self) -> Iterable[Signal]:
elif isinstance(other, Number): result = []
return ConstantAddition(other, self.output(0)) for p in self.outputs:
for s in p.signals:
result.append(s)
return result
def key(self, index: int, prefix: str = "") -> ResultKey:
key = prefix
if self.output_count != 1:
if key:
key += "."
key += str(index)
elif not key:
key = str(index)
return key
def current_output(self, index: int, delays: Optional[DelayMap] = None, prefix: str = "") -> Optional[Number]:
return None
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(
f"Output index out of range (expected 0-{self.output_count - 1}, got {index})")
if len(input_values) != self.input_count:
raise ValueError(f"Wrong number of input values supplied to operation (expected {self.input_count}, got {len(input_values)})")
values = self.evaluate(*(self.truncate_inputs(input_values, bits_override) if truncate else input_values))
if isinstance(values, collections.abc.Sequence):
if len(values) != self.output_count:
raise RuntimeError(
f"Operation evaluated to incorrect number of outputs (expected {self.output_count}, got {len(values)})")
elif isinstance(values, Number):
if self.output_count != 1:
raise RuntimeError(
f"Operation evaluated to incorrect number of outputs (expected {self.output_count}, got 1)")
values = (values,)
else: else:
raise TypeError("Other type is not an Operation or a Number.") raise RuntimeError(
f"Operation evaluated to invalid type (expected Sequence/Number, got {values.__class__.__name__})")
def __sub__(self, other): if results is not None:
"""Overloads the subtraction operator to make it return a new Subtraction operation for i in range(self.output_count):
object that is connected to the self and other objects. If other is a number then results[self.key(i, prefix)] = values[i]
returns a ConstantSubtraction operation object instead. return values[index]
"""
# Import here to avoid circular imports.
from b_asic.core_operations import Subtraction, ConstantSubtraction
if isinstance(other, Operation): def current_outputs(self, delays: Optional[DelayMap] = None, prefix: str = "") -> Sequence[Optional[Number]]:
return Subtraction(self.output(0), other.output(0)) return [self.current_output(i, delays, prefix) for i in range(self.output_count)]
elif isinstance(other, Number):
return ConstantSubtraction(other, self.output(0))
else:
raise TypeError("Other type is not an Operation or a Number.")
def __mul__(self, other): def evaluate_outputs(self, input_values: Sequence[Number], results: Optional[MutableResultMap] = None, delays: Optional[MutableDelayMap] = None, prefix: str = "", bits_override: Optional[int] = None, truncate: bool = True) -> Sequence[Number]:
"""Overloads the multiplication operator to make it return a new Multiplication operation return [self.evaluate_output(i, input_values, results, delays, prefix, bits_override, truncate) for i in range(self.output_count)]
object that is connected to the self and other objects. If other is a number then
returns a ConstantMultiplication operation object instead.
"""
# Import here to avoid circular imports.
from b_asic.core_operations import Multiplication, ConstantMultiplication
if isinstance(other, Operation): def split(self) -> Iterable[Operation]:
return Multiplication(self.output(0), other.output(0)) # Import here to avoid circular imports.
elif isinstance(other, Number): from b_asic.special_operations import Input
return ConstantMultiplication(other, self.output(0)) try:
else: result = self.evaluate(*([Input()] * self.input_count))
raise TypeError("Other type is not an Operation or a Number.") if isinstance(result, collections.Sequence) and all(isinstance(e, Operation) for e in result):
return result
if isinstance(result, Operation):
return [result]
except TypeError:
pass
except ValueError:
pass
return [self]
def __truediv__(self, other): def to_sfg(self) -> "SFG":
"""Overloads the division operator to make it return a new Division operation
object that is connected to the self and other objects. If other is a number then
returns a ConstantDivision operation object instead.
"""
# Import here to avoid circular imports. # Import here to avoid circular imports.
from b_asic.core_operations import Division, ConstantDivision from b_asic.special_operations import Input, Output
from b_asic.signal_flow_graph import SFG
inputs = [Input() for i in range(self.input_count)]
try:
last_operations = self.evaluate(*inputs)
if isinstance(last_operations, Operation):
last_operations = [last_operations]
outputs = [Output(o) for o in last_operations]
except TypeError:
operation_copy = self.copy_component()
inputs = []
for i in range(self.input_count):
_input = Input()
operation_copy.input(i).connect(_input)
inputs.append(_input)
outputs = [Output(operation_copy)]
return SFG(inputs=inputs, outputs=outputs)
def copy_component(self, *args, **kwargs) -> Operation:
new_component = super().copy_component(*args, **kwargs)
for i, inp in enumerate(self.inputs):
new_component.input(i).latency_offset = inp.latency_offset
for i, outp in enumerate(self.outputs):
new_component.output(i).latency_offset = outp.latency_offset
return new_component
def inputs_required_for_output(self, output_index: int) -> Iterable[int]:
if output_index < 0 or output_index >= self.output_count:
raise IndexError(f"Output index out of range (expected 0-{self.output_count - 1}, got {output_index})")
return [i for i in range(self.input_count)] # By default, assume each output depends on all inputs.
if isinstance(other, Operation): @property
return Division(self.output(0), other.output(0)) def neighbors(self) -> Iterable[GraphComponent]:
elif isinstance(other, Number): return list(self.input_signals) + list(self.output_signals)
return ConstantDivision(other, self.output(0))
else: @property
raise TypeError("Other type is not an Operation or a Number.") def preceding_operations(self) -> Iterable[Operation]:
"""Returns an Iterable of all Operations that are connected to this Operations input ports."""
return [signal.source.operation for signal in self.input_signals if signal.source]
@property
def subsequent_operations(self) -> Iterable[Operation]:
"""Returns an Iterable of all Operations that are connected to this Operations output ports."""
return [signal.destination.operation for signal in self.output_signals if signal.destination]
@property
def source(self) -> OutputPort:
if self.output_count != 1:
diff = "more" if self.output_count > 1 else "less"
raise TypeError(
f"{self.__class__.__name__} cannot be used as an input source because it has {diff} than 1 output")
return self.output(0)
def truncate_input(self, index: int, value: Number, bits: int) -> Number:
return int(value) & ((2 ** bits) - 1)
def truncate_inputs(self, input_values: Sequence[Number], bits_override: Optional[int] = None) -> Sequence[Number]:
"""Truncate the values to be used as inputs to the bit lengths specified by the respective signals connected to each input."""
args = []
for i, input_port in enumerate(self.inputs):
value = input_values[i]
bits = bits_override
if bits_override is None and input_port.signal_count >= 1:
bits = input_port.signals[0].bits
if bits_override is not None:
if isinstance(value, complex):
raise TypeError("Complex value cannot be truncated to {bits} bits as requested by the signal connected to input #{i}")
value = self.truncate_input(i, value, bits)
args.append(value)
return args
@property
def latency(self) -> int:
if None in [inp.latency_offset for inp in self.inputs] or None in [outp.latency_offset for outp in self.outputs]:
raise ValueError("All native offsets have to set to a non-negative value to calculate the latency.")
return max(((outp.latency_offset - inp.latency_offset) for outp, inp in it.product(self.outputs, self.inputs)))
def set_latency(self, latency: int) -> None:
assert latency >= 0, "Negative latency entered."
for inport in self.inputs:
inport.latency_offset = 0
for outport in self.outputs:
outport.latency_offset = latency
@property
def latency_offsets(self) -> Sequence[Sequence[int]]:
latency_offsets = dict()
for i, inp in enumerate(self.inputs):
latency_offsets["in" + str(i)] = inp.latency_offset
for i, outp in enumerate(self.outputs):
latency_offsets["out" + str(i)] = outp.latency_offset
return latency_offsets
def set_latency_offsets(self, latency_offsets: Dict[str, int]) -> None:
for port_str, latency_offset in latency_offsets.items():
port_str = port_str.lower()
if port_str.startswith("in"):
index_str = port_str[2:]
assert index_str.isdigit(), "Incorrectly formatted index in string, expected 'in' + index"
self.input(int(index_str)).latency_offset = latency_offset
elif port_str.startswith("out"):
index_str = port_str[3:]
assert index_str.isdigit(), "Incorrectly formatted index in string, expected 'out' + index"
self.output(int(index_str)).latency_offset = latency_offset
else:
raise ValueError("Incorrectly formatted string, expected 'in' + index or 'out' + index")
...@@ -4,12 +4,15 @@ TODO: More info. ...@@ -4,12 +4,15 @@ TODO: More info.
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import NewType, Optional, List from copy import copy
from typing import Optional, List, Iterable, TYPE_CHECKING
from b_asic.operation import Operation
from b_asic.signal import Signal from b_asic.signal import Signal
from b_asic.graph_component import Name
if TYPE_CHECKING:
from b_asic.operation import Operation
PortIndex = NewType("PortIndex", int)
class Port(ABC): class Port(ABC):
"""Port Interface. """Port Interface.
...@@ -19,59 +22,45 @@ class Port(ABC): ...@@ -19,59 +22,45 @@ class Port(ABC):
@property @property
@abstractmethod @abstractmethod
def operation(self) -> Operation: def operation(self) -> "Operation":
"""Return the connected operation.""" """Return the connected operation."""
raise NotImplementedError raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def index(self) -> PortIndex: def index(self) -> int:
"""Return the unique PortIndex.""" """Return the index of the port."""
raise NotImplementedError raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def signals(self) -> List[Signal]: def latency_offset(self) -> int:
"""Return a list of all connected signals.""" """Get the latency_offset of the port."""
raise NotImplementedError raise NotImplementedError
@latency_offset.setter
@abstractmethod @abstractmethod
def signal(self, i: int = 0) -> Signal: def latency_offset(self, latency_offset: int) -> None:
"""Return the connected signal at index i. """Set the latency_offset of the port to the integer specified value."""
Keyword argumens:
i: integer index of the signal requsted.
"""
raise NotImplementedError raise NotImplementedError
@property @property
@abstractmethod
def connected_ports(self) -> List["Port"]:
"""Return a list of all connected Ports."""
raise NotImplementedError
@abstractmethod @abstractmethod
def signal_count(self) -> int: def signal_count(self) -> int:
"""Return the number of connected signals.""" """Return the number of connected signals."""
raise NotImplementedError raise NotImplementedError
@property
@abstractmethod @abstractmethod
def connect(self, port: "Port") -> Signal: def signals(self) -> Iterable[Signal]:
"""Create and return a signal that is connected to this port and the entered """Return all connected signals."""
port and connect this port to the signal and the entered port to the signal."""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def add_signal(self, signal: Signal) -> None: def add_signal(self, signal: Signal) -> None:
"""Connect this port to the entered signal. If the entered signal isn't connected to """Connect this port to the entered signal. If the entered signal isn't connected to
this port then connect the entered signal to the port aswell.""" this port then connect the entered signal to the port aswell.
raise NotImplementedError """
@abstractmethod
def disconnect(self, port: "Port") -> None:
"""Disconnect the entered port from the port by removing it from the ports signal.
If the entered port is still connected to this ports signal then disconnect the entered
port from the signal aswell."""
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
...@@ -97,21 +86,43 @@ class AbstractPort(Port): ...@@ -97,21 +86,43 @@ class AbstractPort(Port):
Handles functionality for port id and saves the connection to the parent operation. Handles functionality for port id and saves the connection to the parent operation.
""" """
_operation: "Operation"
_index: int _index: int
_operation: Operation _latency_offset: Optional[int]
def __init__(self, index: int, operation: Operation): def __init__(self, operation: "Operation", index: int, latency_offset: int = None):
self._index = index
self._operation = operation self._operation = operation
self._index = index
self._latency_offset = latency_offset
@property @property
def operation(self) -> Operation: def operation(self) -> "Operation":
return self._operation return self._operation
@property @property
def index(self) -> PortIndex: def index(self) -> int:
return self._index return self._index
@property
def latency_offset(self) -> int:
return self._latency_offset
@latency_offset.setter
def latency_offset(self, latency_offset: int):
self._latency_offset = latency_offset
class SignalSourceProvider(ABC):
"""Signal source provider interface.
TODO: More info.
"""
@property
@abstractmethod
def source(self) -> "OutputPort":
"""Get the main source port provided by this object."""
raise NotImplementedError
class InputPort(AbstractPort): class InputPort(AbstractPort):
"""Input port. """Input port.
...@@ -120,104 +131,88 @@ class InputPort(AbstractPort): ...@@ -120,104 +131,88 @@ class InputPort(AbstractPort):
_source_signal: Optional[Signal] _source_signal: Optional[Signal]
def __init__(self, port_id: PortIndex, operation: Operation): def __init__(self, operation: "Operation", index: int):
super().__init__(port_id, operation) super().__init__(operation, index)
self._source_signal = None self._source_signal = None
@property @property
def signals(self) -> List[Signal]:
return [] if self._source_signal is None else [self._source_signal]
def signal(self, i: int = 0) -> Signal:
assert 0 <= i < self.signal_count(), "Signal index out of bound."
assert self._source_signal is not None, "No Signal connect to InputPort."
return self._source_signal
@property
def connected_ports(self) -> List[Port]:
return [] if self._source_signal is None or self._source_signal.source is None \
else [self._source_signal.source]
def signal_count(self) -> int: def signal_count(self) -> int:
return 0 if self._source_signal is None else 1 return 0 if self._source_signal is None else 1
def connect(self, port: "OutputPort") -> Signal: @property
assert self._source_signal is None, "Connecting new port to already connected input port." def signals(self) -> Iterable[Signal]:
return Signal(port, self) # self._source_signal is set by the signal constructor return [] if self._source_signal is None else [self._source_signal]
def add_signal(self, signal: Signal) -> None: def add_signal(self, signal: Signal) -> None:
assert self._source_signal is None, "Connecting new port to already connected input port." assert self._source_signal is None, "Input port may have only one signal added."
self._source_signal: Signal = signal assert signal is not self._source_signal, "Attempted to add already connected signal."
if self is not signal.destination: self._source_signal = signal
# Connect this inputport as destination for this signal if it isn't already. signal.set_destination(self)
signal.set_destination(self)
def disconnect(self, port: "OutputPort") -> None:
assert self._source_signal.source is port, "The entered port is not connected to this port."
self._source_signal.remove_source()
def remove_signal(self, signal: Signal) -> None: def remove_signal(self, signal: Signal) -> None:
old_signal: Signal = self._source_signal assert signal is self._source_signal, "Attempted to remove signal that is not connected."
self._source_signal = None self._source_signal = None
if self is old_signal.destination: signal.remove_destination()
# Disconnect the dest of the signal if this inputport currently is the dest
old_signal.remove_destination()
def clear(self) -> None: def clear(self) -> None:
self.remove_signal(self._source_signal) if self._source_signal is not None:
self.remove_signal(self._source_signal)
@property
def connected_source(self) -> Optional["OutputPort"]:
"""Get the output port that is currently connected to this input port,
or None if it is unconnected.
"""
return None if self._source_signal is None else self._source_signal.source
class OutputPort(AbstractPort): def connect(self, src: SignalSourceProvider, name: Name = "") -> Signal:
"""Connect the provided signal source to this input port by creating a new signal.
Returns the new signal.
"""
assert self._source_signal is None, "Attempted to connect already connected input port."
# self._source_signal is set by the signal constructor.
return Signal(source=src.source, destination=self, name=name)
def __lshift__(self, src: SignalSourceProvider) -> Signal:
"""Overloads the left shift operator to make it connect the provided signal source to this input port.
Returns the new signal.
"""
return self.connect(src)
class OutputPort(AbstractPort, SignalSourceProvider):
"""Output port. """Output port.
TODO: More info. TODO: More info.
""" """
_destination_signals: List[Signal] _destination_signals: List[Signal]
def __init__(self, port_id: PortIndex, operation: Operation): def __init__(self, operation: "Operation", index: int):
super().__init__(port_id, operation) super().__init__(operation, index)
self._destination_signals = [] self._destination_signals = []
@property @property
def signals(self) -> List[Signal]:
return self._destination_signals.copy()
def signal(self, i: int = 0) -> Signal:
assert 0 <= i < self.signal_count(), "Signal index out of bounds."
return self._destination_signals[i]
@property
def connected_ports(self) -> List[Port]:
return [signal.destination for signal in self._destination_signals \
if signal.destination is not None]
def signal_count(self) -> int: def signal_count(self) -> int:
return len(self._destination_signals) return len(self._destination_signals)
def connect(self, port: InputPort) -> Signal: @property
return Signal(self, port) # Signal is added to self._destination_signals in signal constructor def signals(self) -> Iterable[Signal]:
return self._destination_signals
def add_signal(self, signal: Signal) -> None: def add_signal(self, signal: Signal) -> None:
assert signal not in self.signals, \ assert signal not in self._destination_signals, "Attempted to add already connected signal."
"Attempting to connect to Signal already connected."
self._destination_signals.append(signal) self._destination_signals.append(signal)
if self is not signal.source: signal.set_source(self)
# Connect this outputport to the signal if it isn't already
signal.set_source(self)
def disconnect(self, port: InputPort) -> None:
assert port in self.connected_ports, "Attempting to disconnect port that isn't connected."
for sig in self._destination_signals:
if sig.destination is port:
sig.remove_destination()
break
def remove_signal(self, signal: Signal) -> None: def remove_signal(self, signal: Signal) -> None:
i: int = self._destination_signals.index(signal) assert signal in self._destination_signals, "Attempted to remove signal that is not connected."
old_signal: Signal = self._destination_signals[i] self._destination_signals.remove(signal)
del self._destination_signals[i] signal.remove_source()
if self is old_signal.source:
old_signal.remove_source()
def clear(self) -> None: def clear(self) -> None:
for signal in self._destination_signals: for signal in copy(self._destination_signals):
self.remove_signal(signal) self.remove_signal(signal)
@property
def source(self) -> "OutputPort":
return self
"""@package docstring
B-ASIC Precedence Chart Module.
TODO: More info.
"""
from b_asic.signal_flow_graph import SFG
class PrecedenceChart:
"""Precedence chart constructed from a signal flow graph.
TODO: More info.
"""
sfg: SFG
# TODO: More members.
def __init__(self, sfg: SFG):
self.sfg = sfg
# TODO: Implement.
# TODO: More stuff.
"""@package docstring
B-ASIC Save/Load SFG Module.
Given a structure try to serialize it and save it to a file.
Given a serialized file try to deserialize it and load it to the library.
"""
from b_asic import SFG, GraphComponent
from datetime import datetime
from inspect import signature
from os import path
def sfg_to_python(sfg: SFG, counter: int = 0, suffix: str = None) -> str:
result = (
"\n\"\"\"\nB-ASIC automatically generated SFG file.\n" +
"Name: " + f"{sfg.name}" + "\n" +
"Last saved: " + f"{datetime.now()}" + ".\n" +
"\"\"\""
)
result += "\nfrom b_asic import SFG, Signal, Input, Output"
for op in {type(op) for op in sfg.operations}:
result += f", {op.__name__}"
def kwarg_unpacker(comp: GraphComponent, params=None) -> str:
if params is None:
params_filtered = {attr: getattr(op, attr) for attr in signature(op.__init__).parameters if attr != "latency" and hasattr(op, attr)}
params = {attr: getattr(op, attr) if not isinstance(getattr(op, attr), str) else f'"{getattr(op, attr)}"' for attr in params_filtered}
return ", ".join([f"{param[0]}={param[1]}" for param in params.items()])
result += "\n# Inputs:\n"
for op in sfg._input_operations:
result += f"{op.graph_id} = Input({kwarg_unpacker(op)})\n"
result += "\n# Outputs:\n"
for op in sfg._output_operations:
result += f"{op.graph_id} = Output({kwarg_unpacker(op)})\n"
result += "\n# Operations:\n"
for op in sfg.split():
if isinstance(op, SFG):
counter += 1
result = sfg_to_python(op, counter) + result
continue
result += f"{op.graph_id} = {op.__class__.__name__}({kwarg_unpacker(op)})\n"
result += "\n# Signals:\n"
connections = list() # Keep track of already existing connections to avoid adding duplicates
for op in sfg.split():
for out in op.outputs:
for signal in out.signals:
dest_op = signal.destination.operation
connection = f"\nSignal(source={op.graph_id}.output({op.outputs.index(signal.source)}), destination={dest_op.graph_id}.input({dest_op.inputs.index(signal.destination)}))"
if connection in connections:
continue
result += connection
connections.append(connection)
inputs = "[" + ", ".join(op.graph_id for op in sfg.input_operations) + "]"
outputs = "[" + ", ".join(op.graph_id for op in sfg.output_operations) + "]"
sfg_name = sfg.name if sfg.name else "sfg" + str(counter) if counter > 0 else 'sfg'
result += f"\n{sfg_name} = SFG(inputs={inputs}, outputs={outputs}, name='{sfg_name}')\n"
result += "\n# SFG Properties:\n" + "prop = {'name':" + f"{sfg_name}" + "}"
if suffix is not None:
result += "\n" + suffix + "\n"
return result
def python_to_sfg(path: str) -> SFG:
with open(path) as f:
code = compile(f.read(), path, 'exec')
exec(code, globals(), locals())
return locals()["prop"]["name"], locals()["positions"] if "positions" in locals() else None
\ No newline at end of file
"""@package docstring """@package docstring
B-ASIC Schema Module. This module contains the Schema class.
TODO: More info. TODO: More info
""" """
from b_asic.precedence_chart import PrecedenceChart from typing import Dict, List
from b_asic.signal_flow_graph import SFG
from b_asic.graph_component import GraphID
from b_asic.operation import Operation
from b_asic.special_operations import *
class Schema: class Schema:
"""Schema constructed from a precedence chart. """A class that represents an SFG with scheduled Operations."""
TODO: More info.
""" _sfg: SFG
_start_times: Dict[GraphID, int]
_laps: Dict[GraphID, List[int]]
_schedule_time: int
_cyclic: bool
_resolution: int
def __init__(self, sfg: SFG, schedule_time: int = None, cyclic: bool = False, resolution: int = 1, scheduling_alg: str = "ASAP"):
self._sfg = sfg
self._start_times = dict()
self._laps = dict()
self._cyclic = cyclic
self._resolution = resolution
self._memory_elements = dict()
if scheduling_alg == "ASAP":
self._schedule_asap()
else:
raise NotImplementedError(f"No algorithm with name: {scheduling_alg} defined.")
max_end_time = 0
for op_id, op_start_time in self._start_times.items():
op = self._sfg.find_by_id(op_id)
for outport in op.outputs:
max_end_time = max(max_end_time, op_start_time + outport.latency_offset)
if not self._cyclic:
if schedule_time is None:
self._schedule_time = max_end_time
elif schedule_time < max_end_time:
raise ValueError("Too short schedule time for non-cyclic Schedule entered.")
else:
self._schedule_time = schedule_time
def start_time_of_operation(self, op_id: GraphID):
"""Get the start time of the operation with the specified by the op_id."""
assert op_id in self._start_times, "No operation with the specified op_id in this schema."
return self._start_times[op_id]
def forward_slack(self, op_id):
raise NotImplementedError
def backward_slack(self, op_id):
raise NotImplementedError
def print_slacks(self):
raise NotImplementedError
def _schedule_asap(self):
pl = self._sfg.get_precedence_list()
if len(pl) < 2:
print("Empty signal flow graph cannot be scheduled.")
return
non_schedulable_ops = set((outp.operation.graph_id for outp in pl[0]))
for outport in pl[1]:
op = outport.operation
if op not in self._start_times:
# Set start time of all operations in the first iter to 0
self._start_times[op.graph_id] = 0
for outports in pl[2:]:
for outport in outports:
op = outport.operation
if op.graph_id not in self._start_times:
# Schedule the operation if it doesn't have a start time yet.
op_start_time = 0
for inport in op.inputs:
assert len(inport.signals) == 1, "Error in scheduling, dangling input port detected."
assert inport.signals[0].source is not None, "Error in scheduling, signal with no source detected."
source_port = inport.signals[0].source
source_end_time = None
if source_port.operation.graph_id in non_schedulable_ops:
source_end_time = 0
else:
source_op_time = self._start_times[source_port.operation.graph_id]
assert source_port.latency_offset is not None, f"Output port: {source_port.index} of operation: \
{source_port.operation.graph_id} has no latency-offset."
assert inport.latency_offset is not None, f"Input port: {inport.index} of operation: \
{inport.operation.graph_id} has no latency-offset."
source_end_time = source_op_time + source_port.latency_offset
op_start_time_from_in = source_end_time - inport.latency_offset
op_start_time = max(op_start_time, op_start_time_from_in)
self._start_times[op.graph_id] = op_start_time
def get_memory_elements(self):
operation_orderd = self._sfg.get_operations_topological_order()
for op in operation_orderd:
if isinstance(op, Input) or isinstance(op, Output):
pass
for key in self._start_times:
if op.graph_id == key:
for i in range(len(op.outputs)):
time_list = []
start_time = self._start_times.get(op.graph_id)+op.outputs[i].latency_offset
time_list.append(start_time)
for j in range(len(op.outputs[i].signals)):
new_op = self.get_op_after_delay(op.outputs[i].signals[j].destination.operation, op.outputs[i].signals[j].destination)
end_start_time = self._start_times.get(new_op[0].graph_id)
end_start_time_latency_offset = new_op[1].latency_offset
if end_start_time_latency_offset is None:
end_start_time_latency_offset = 0
if end_start_time is None:
end_time = self._schedule_time
else:
end_time = end_start_time + end_start_time_latency_offset
time_list.append(end_time)
read_name = op.name
write_name = new_op[0].name
key_name = read_name + "->" + write_name
self._memory_elements[key_name] = time_list
def get_op_after_delay(self, op, destination):
if isinstance(op, Delay):
for i in range(len(op.outputs[0].signals)):
connected_op = op.outputs[0].signals[i].destination.operation
dest = op.outputs[0].signals[i].destination
return self.get_op_after_delay(connected_op, dest)
return [op, destination]
pc: PrecedenceChart def print_memory_elements(self):
# TODO: More members. self.get_memory_elements()
output_string = ""
for key in self._memory_elements:
output_string += key
output_string += ": start time: "
output_string += str(self._memory_elements[key][0])
output_string += " end time: "
output_string += str(self._memory_elements[key][1])
output_string += '\n'
print(output_string)
def __init__(self, pc: PrecedenceChart):
self.pc = pc
# TODO: Implement.
# TODO: More stuff.
"""@package docstring """@package docstring
B-ASIC Signal Module. B-ASIC Signal Module.
""" """
from typing import Optional, TYPE_CHECKING from typing import Optional, Iterable, TYPE_CHECKING
from b_asic.graph_component import AbstractGraphComponent, TypeName, Name from b_asic.graph_component import GraphComponent, AbstractGraphComponent, TypeName, Name
if TYPE_CHECKING: if TYPE_CHECKING:
from b_asic.port import InputPort, OutputPort from b_asic.port import InputPort, OutputPort
...@@ -12,30 +12,34 @@ if TYPE_CHECKING: ...@@ -12,30 +12,34 @@ if TYPE_CHECKING:
class Signal(AbstractGraphComponent): class Signal(AbstractGraphComponent):
"""A connection between two ports.""" """A connection between two ports."""
_source: "OutputPort" _source: Optional["OutputPort"]
_destination: "InputPort" _destination: Optional["InputPort"]
def __init__(self, source: Optional["OutputPort"] = None, \
destination: Optional["InputPort"] = None, name: Name = ""):
def __init__(self, source: Optional["OutputPort"] = None, destination: Optional["InputPort"] = None, bits: Optional[int] = None, name: Name = ""):
super().__init__(name) super().__init__(name)
self._source = None
self._source = source self._destination = None
self._destination = destination
if source is not None: if source is not None:
self.set_source(source) self.set_source(source)
if destination is not None: if destination is not None:
self.set_destination(destination) self.set_destination(destination)
self.set_param("bits", bits)
@classmethod
def type_name(cls) -> TypeName:
return "s"
@property @property
def source(self) -> "OutputPort": def neighbors(self) -> Iterable[GraphComponent]:
return [p.operation for p in [self.source, self.destination] if p is not None]
@property
def source(self) -> Optional["OutputPort"]:
"""Return the source OutputPort of the signal.""" """Return the source OutputPort of the signal."""
return self._source return self._source
@property @property
def destination(self) -> "InputPort": def destination(self) -> Optional["InputPort"]:
"""Return the destination "InputPort" of the signal.""" """Return the destination "InputPort" of the signal."""
return self._destination return self._destination
...@@ -47,11 +51,11 @@ class Signal(AbstractGraphComponent): ...@@ -47,11 +51,11 @@ class Signal(AbstractGraphComponent):
Keyword arguments: Keyword arguments:
- src: OutputPort to connect as source to the signal. - src: OutputPort to connect as source to the signal.
""" """
self.remove_source() if src is not self._source:
self._source = src self.remove_source()
if self not in src.signals: self._source = src
# If the new source isn't connected to this signal then connect it. if self not in src.signals:
src.add_signal(self) src.add_signal(self)
def set_destination(self, dest: "InputPort") -> None: def set_destination(self, dest: "InputPort") -> None:
"""Disconnect the previous destination InputPort of the signal and """Disconnect the previous destination InputPort of the signal and
...@@ -61,36 +65,44 @@ class Signal(AbstractGraphComponent): ...@@ -61,36 +65,44 @@ class Signal(AbstractGraphComponent):
Keywords argments: Keywords argments:
- dest: InputPort to connect as destination to the signal. - dest: InputPort to connect as destination to the signal.
""" """
self.remove_destination() if dest is not self._destination:
self._destination = dest self.remove_destination()
if self not in dest.signals: self._destination = dest
# If the new destination isn't connected to tis signal then connect it. if self not in dest.signals:
dest.add_signal(self) dest.add_signal(self)
@property
def type_name(self) -> TypeName:
return "s"
def remove_source(self) -> None: def remove_source(self) -> None:
"""Disconnect the source OutputPort of the signal. If the source port """Disconnect the source OutputPort of the signal. If the source port
still is connected to this signal then also disconnect the source port.""" still is connected to this signal then also disconnect the source port."""
if self._source is not None: src = self._source
old_source: "OutputPort" = self._source if src is not None:
self._source = None self._source = None
if self in old_source.signals: if self in src.signals:
# If the old destination port still is connected to this signal, then disconnect it. src.remove_signal(self)
old_source.remove_signal(self)
def remove_destination(self) -> None: def remove_destination(self) -> None:
"""Disconnect the destination InputPort of the signal.""" """Disconnect the destination InputPort of the signal."""
if self._destination is not None: dest = self._destination
old_destination: "InputPort" = self._destination if dest is not None:
self._destination = None self._destination = None
if self in old_destination.signals: if self in dest.signals:
# If the old destination port still is connected to this signal, then disconnect it. dest.remove_signal(self)
old_destination.remove_signal(self)
def is_connected(self) -> bool: def dangling(self) -> bool:
"""Returns true if the signal is connected to both a source and a destination, """Returns true if the signal is missing either a source or a destination,
else false.""" else false."""
return self._source is not None and self._destination is not None return self._source is None or self._destination is None
@property
def bits(self) -> Optional[int]:
"""Get the number of bits that this operations using this signal as an input should truncate received values to.
None = unlimited."""
return self.param("bits")
@bits.setter
def bits(self, bits: Optional[int]) -> None:
"""Set the number of bits that operations using this signal as an input should truncate received values to.
None = unlimited."""
assert bits is None or (isinstance(bits, int)
and bits >= 0), "Bits must be non-negative."
self.set_param("bits", bits)
...@@ -3,14 +3,41 @@ B-ASIC Signal Flow Graph Module. ...@@ -3,14 +3,41 @@ B-ASIC Signal Flow Graph Module.
TODO: More info. TODO: More info.
""" """
from typing import List, Dict, Optional, DefaultDict from typing import List, Iterable, Sequence, Dict, Optional, DefaultDict, MutableSet, Tuple
from collections import defaultdict from numbers import Number
from collections import defaultdict, deque
from io import StringIO
from queue import PriorityQueue
import itertools as it
from graphviz import Digraph
from b_asic.operation import Operation from b_asic.port import SignalSourceProvider, OutputPort
from b_asic.operation import AbstractOperation from b_asic.operation import Operation, AbstractOperation, ResultKey, DelayMap, MutableResultMap, MutableDelayMap
from b_asic.signal import Signal from b_asic.signal import Signal
from b_asic.graph_id import GraphIDGenerator, GraphID from b_asic.graph_component import GraphID, GraphIDNumber, GraphComponent, Name, TypeName
from b_asic.graph_component import GraphComponent, Name, TypeName from b_asic.special_operations import Input, Output, Delay
DelayQueue = List[Tuple[str, ResultKey, OutputPort]]
class GraphIDGenerator:
"""A class that generates Graph IDs for objects."""
_next_id_number: DefaultDict[TypeName, GraphIDNumber]
def __init__(self, id_number_offset: GraphIDNumber = 0):
self._next_id_number = defaultdict(lambda: id_number_offset)
def next_id(self, type_name: TypeName) -> GraphID:
"""Get the next graph id for a certain graph id type."""
self._next_id_number[type_name] += 1
return type_name + str(self._next_id_number[type_name])
@property
def id_number_offset(self) -> GraphIDNumber:
"""Get the graph id number offset of this generator."""
return self._next_id_number.default_factory() # pylint: disable=not-callable
class SFG(AbstractOperation): class SFG(AbstractOperation):
...@@ -18,74 +45,746 @@ class SFG(AbstractOperation): ...@@ -18,74 +45,746 @@ class SFG(AbstractOperation):
TODO: More info. TODO: More info.
""" """
_graph_components_by_id: Dict[GraphID, GraphComponent] _components_by_id: Dict[GraphID, GraphComponent]
_graph_components_by_name: DefaultDict[Name, List[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 _graph_id_generator: GraphIDGenerator
_input_operations: List[Input]
_output_operations: List[Output]
_original_components_to_new: MutableSet[GraphComponent]
_original_input_signals_to_indices: Dict[Signal, int]
_original_output_signals_to_indices: Dict[Signal, int]
_precedence_list: Optional[List[List[OutputPort]]]
def __init__(self, input_signals: Optional[Sequence[Signal]] = None, output_signals: Optional[Sequence[Signal]] = None,
inputs: Optional[Sequence[Input]] = None, outputs: Optional[Sequence[Output]] = None,
id_number_offset: GraphIDNumber = 0, name: Name = "",
input_sources: Optional[Sequence[Optional[SignalSourceProvider]]] = None):
input_signal_count = 0 if input_signals is None else len(input_signals)
input_operation_count = 0 if inputs is None else len(inputs)
output_signal_count = 0 if output_signals is None else len(
output_signals)
output_operation_count = 0 if outputs is None else len(outputs)
super().__init__(input_count=input_signal_count + input_operation_count,
output_count=output_signal_count + output_operation_count,
name=name, input_sources=input_sources)
self._components_by_id = dict()
self._components_by_name = defaultdict(list)
self._components_dfs_order = []
self._operations_dfs_order = []
self._operations_topological_order = []
self._graph_id_generator = GraphIDGenerator(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):
assert signal not in self._original_components_to_new, "Duplicate input signals supplied to SFG construcctor."
new_input_op = self._add_component_unconnected_copy(Input())
new_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 ater input signals.
if inputs is not None:
for input_index, input_op in enumerate(inputs, input_signal_count):
assert input_op not in self._original_components_to_new, "Duplicate input operations supplied to SFG constructor."
new_input_op = self._add_component_unconnected_copy(input_op)
for signal in input_op.output(0).signals:
assert signal not in self._original_components_to_new, "Duplicate input signals connected to input ports supplied to SFG construcctor."
new_signal = self._add_component_unconnected_copy(signal)
new_signal.set_source(new_input_op.output(0))
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 = self._add_component_unconnected_copy(Output())
if signal in self._original_components_to_new:
# Signal was already added when setting up inputs.
new_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 = 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
def __init__(self, input_signals: List[Signal] = None, output_signals: List[Signal] = None, \ # Setup output operations, starting from indices after output signals.
ops: List[Operation] = None, **kwds): if outputs is not None:
super().__init__(**kwds) for output_index, output_op in enumerate(outputs, output_signal_count):
if input_signals is None: assert output_op not in self._original_components_to_new, "Duplicate output operations supplied to SFG constructor."
input_signals = [] new_output_op = self._add_component_unconnected_copy(output_op)
if output_signals is None: for signal in output_op.input(0).signals:
output_signals = [] new_signal = None
if ops is None: if signal in self._original_components_to_new:
ops = [] # 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(
signal)
self._graph_components_by_id = dict() # Maps Graph ID to objects new_signal.set_destination(new_output_op.input(0))
self._graph_components_by_name = defaultdict(list) # Maps Name to objects self._original_output_signals_to_indices[signal] = output_index
self._graph_id_generator = GraphIDGenerator()
for operation in ops: self._output_operations.append(new_output_op)
self._add_graph_component(operation)
for input_signal in input_signals: output_operations_set = set(self._output_operations)
self._add_graph_component(input_signal)
# TODO: Construct SFG based on what inputs that were given # Search the graph inwards from each input signal.
# TODO: Traverse the graph between the inputs/outputs and add to self._operations. for signal, input_index in self._original_input_signals_to_indices.items():
# TODO: Connect ports with signals with appropriate IDs. # Check if already added destination.
new_signal = self._original_components_to_new[signal]
if new_signal.destination is None:
if signal.destination is None:
raise ValueError(
f"Input signal #{input_index} is missing destination in SFG")
if signal.destination.operation not in self._original_components_to_new:
self._add_operation_connected_tree_copy(
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(
[new_signal.source.operation, new_signal, new_signal.destination.operation])
self._operations_dfs_order.extend(
[new_signal.source.operation, new_signal.destination.operation])
# Search the graph inwards from each output signal.
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(
f"Output signal #{output_index} is missing source in SFG")
if signal.source.operation not in self._original_components_to_new:
self._add_operation_connected_tree_copy(
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():
string_io.write(str(operation) + "\n")
string_io.write(line)
return string_io.getvalue()
def __call__(self, *src: Optional[SignalSourceProvider], name: Name = "") -> "SFG":
"""Get a new independent SFG instance that is identical to this SFG except without any of its external connections."""
return SFG(inputs=self._input_operations, outputs=self._output_operations,
id_number_offset=self.id_number_offset, name=name, input_sources=src if src else None)
@classmethod
def type_name(cls) -> TypeName:
return "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
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(
f"Output index out of range (expected 0-{self.output_count - 1}, got {index})")
if len(input_values) != self.input_count:
raise ValueError(
f"Wrong number of inputs supplied to SFG for evaluation (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.
for op, arg in zip(self._input_operations, self.truncate_inputs(input_values, bits_override) if truncate else input_values):
op.value = arg
deferred_delays = []
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(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 succesful, False otherwise. """
if len(self.inputs) != len(self.input_operations):
raise IndexError(f"Number of inputs does not match the number of input_operations in SFG.")
if len(self.outputs) != len(self.output_operations):
raise IndexError(f"Number of outputs does not match the number of output_operations SFG.")
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
dest.clear()
port.signals[0].set_destination(dest)
# For each output_signal, connect it to the corresponding operation
for port, output_operation in zip(self.outputs, self.output_operations):
src = output_operation.input(0).signals[0].source
src.clear()
port.signals[0].set_source(src)
return True
@property
def input_operations(self) -> Sequence[Operation]:
"""Get the internal input operations in the same order as their respective input ports."""
return self._input_operations
@property
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 evaluate(self, *inputs) -> list: def split(self) -> Iterable[Operation]:
return [] # TODO: Implement return self.operations
def _add_graph_component(self, graph_component: GraphComponent) -> GraphID: def to_sfg(self) -> 'SFG':
"""Add the entered graph component to the SFG's dictionary of graph objects and return self
return a generated GraphID for it.
def inputs_required_for_output(self, output_index: int) -> Iterable[int]:
if output_index < 0 or output_index >= self.output_count:
raise IndexError(
f"Output index out of range (expected 0-{self.output_count - 1}, got {output_index})")
input_indexes_required = []
sfg_input_operations_to_indexes = {
input_op: index for index, input_op in enumerate(self._input_operations)}
output_op = self._output_operations[output_index]
queue = deque([output_op])
visited = set([output_op])
while queue:
op = queue.popleft()
if isinstance(op, Input):
if op in sfg_input_operations_to_indexes:
input_indexes_required.append(
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:
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
def components(self) -> Iterable[GraphComponent]:
"""Get all components of this graph in depth-first order."""
return self._components_dfs_order
@property
def operations(self) -> Iterable[Operation]:
"""Get all operations of this graph in depth-first order."""
return self._operations_dfs_order
def get_components_with_type_name(self, type_name: TypeName) -> List[GraphComponent]:
"""Get a list with all components in this graph with the specified type_name.
Keyword arguments: Keyword arguments:
graph_component: Graph component to add to the graph. type_name: The type_name of the desired components.
""" """
# Add to name dict i = self.id_number_offset + 1
self._graph_components_by_name[graph_component.name].append(graph_component) components = []
found_comp = self.find_by_id(type_name + str(i))
while found_comp is not None:
components.append(found_comp)
i += 1
found_comp = self.find_by_id(type_name + str(i))
# Add to ID dict return components
graph_id: GraphID = self._graph_id_generator.get_next_id(graph_component.type_name)
self._graph_components_by_id[graph_id] = graph_component
return graph_id
def find_by_id(self, graph_id: GraphID) -> Optional[GraphComponent]: def find_by_id(self, graph_id: GraphID) -> Optional[GraphComponent]:
"""Find a graph object based on the entered Graph ID and return it. If no graph """Find the graph component with the specified ID.
object with the entered ID was found then return None. Returns None if the component was not found.
Keyword arguments: Keyword arguments:
graph_id: Graph ID of the wanted object. graph_id: Graph ID of the desired component.
""" """
if graph_id in self._graph_components_by_id: return self._components_by_id.get(graph_id, None)
return self._graph_components_by_id[graph_id]
return None def find_by_name(self, name: Name) -> Sequence[GraphComponent]:
"""Find all graph components with the specified name.
Returns an empty sequence if no components were found.
def find_by_name(self, name: Name) -> List[GraphComponent]: Keyword arguments:
"""Find all graph objects that have the entered name and return them name: Name of the desired component(s)
in a list. If no graph object with the entered name was found then return an """
empty list. return self._components_by_name.get(name, [])
def find_result_keys_by_name(self, name: Name, output_index: int = 0) -> Sequence[ResultKey]:
"""Find all graph components with the specified name and
return a sequence of the keys to use when fetching their results
from a simulation.
Keyword arguments: Keyword arguments:
name: Name of the wanted object. name: Name of the desired component(s)
output_index: The desired output index to get the result from
""" """
return self._graph_components_by_name[name] keys = []
for comp in self.find_by_name(name):
if isinstance(comp, Operation):
keys.append(comp.key(output_index, comp.graph_id))
return keys
def replace_component(self, component: Operation, _id: GraphID):
"""Find and replace all components matching either on GraphID, Type or both.
Then return a new deepcopy of the sfg with the replaced component.
@property Arguments:
def type_name(self) -> TypeName: component: The new component(s), e.g Multiplication
return "sfg" _id: The GraphID to match the component to replace.
"""
_sfg_copy = self()
_component = _sfg_copy.find_by_id(_id)
assert _component is not None and isinstance(_component, Operation), \
"No operation matching the criteria found"
assert _component.output_count == component.output_count, \
"The output count may not differ between the operations"
assert _component.input_count == component.input_count, \
"The input count may not differ between the operations"
for index_in, _inp in enumerate(_component.inputs):
for _signal in _inp.signals:
_signal.remove_destination()
_signal.set_destination(component.input(index_in))
for index_out, _out in enumerate(_component.outputs):
for _signal in _out.signals:
_signal.remove_source()
_signal.set_source(component.output(index_out))
# The old SFG will be deleted by Python GC
return _sfg_copy()
def insert_operation(self, component: Operation, output_comp_id: GraphID) -> Optional["SFG"]:
"""Insert an operation in the SFG after a given source operation.
The source operation output count must match the input count of the operation as well as the output
Then return a new deepcopy of the sfg with the inserted component.
Arguments:
component: The new component, e.g Multiplication.
output_comp_id: The source operation GraphID to connect from.
"""
# Preserve the original SFG by creating a copy.
sfg_copy = self()
output_comp = sfg_copy.find_by_id(output_comp_id)
if output_comp is None:
return None
assert not isinstance(output_comp, Output), \
"Source operation can not be an output operation."
assert len(output_comp.output_signals) == component.input_count, \
"Source operation output count does not match input count for component."
assert len(output_comp.output_signals) == component.output_count, \
"Destination operation input count does not match output for component."
for index, signal_in in enumerate(output_comp.output_signals):
destination = signal_in.destination
signal_in.set_destination(component.input(index))
destination.connect(component.output(index))
# Recreate the newly coupled SFG so that all attributes are correct.
return sfg_copy()
def remove_operation(self, operation_id: GraphID) -> "SFG":
"""Returns a version of the SFG where the operation with the specified GraphID removed.
The operation has to have the same amount of input- and output ports or a ValueError will
be raised. If no operation with the entered operation_id is found then returns None and does nothing."""
sfg_copy = self()
operation = sfg_copy.find_by_id(operation_id)
if operation is None:
return None
if operation.input_count != operation.output_count:
raise ValueError("Different number of input and output ports of operation with the specified id")
for i, outport in enumerate(operation.outputs):
if outport.signal_count > 0:
if operation.input(i).signal_count > 0 and operation.input(i).signals[0].source is not None:
in_sig = operation.input(i).signals[0]
source_port = in_sig.source
source_port.remove_signal(in_sig)
operation.input(i).remove_signal(in_sig)
for out_sig in outport.signals.copy():
out_sig.set_source(source_port)
else:
for out_sig in outport.signals.copy():
out_sig.remove_source()
else:
if operation.input(i).signal_count > 0:
in_sig = operation.input(i).signals[0]
operation.input(i).remove_signal(in_sig)
return sfg_copy()
def get_precedence_list(self) -> List[List[OutputPort]]:
"""Returns a Precedence list of the SFG where each element in n:th the list consists
of elements that are executed in the n:th step. If the precedence list already has been
calculated for the current SFG then returns the cached version."""
if self._precedence_list:
return self._precedence_list
# Find all operations with only outputs and no inputs.
no_input_ops = list(filter(lambda op: op.input_count == 0, self.operations))
delay_ops = self.get_components_with_type_name(Delay.type_name())
# Find all first iter output ports for precedence
first_iter_ports = [op.output(i) for op in (no_input_ops + delay_ops) for i in range(op.output_count)]
self._precedence_list = self._traverse_for_precedence_list(first_iter_ports)
return self._precedence_list
def _traverse_for_precedence_list(self, first_iter_ports: List[OutputPort]) -> List[List[OutputPort]]:
# Find dependencies of output ports and input ports.
remaining_inports_per_operation = {op: op.input_count for op in self.operations}
# Traverse output ports for precedence
curr_iter_ports = first_iter_ports
precedence_list = []
while curr_iter_ports:
# Add the found ports to the current iter
precedence_list.append(curr_iter_ports)
next_iter_ports = []
for outport in curr_iter_ports:
for signal in outport.signals:
new_inport = signal.destination
# Don't traverse over delays.
if new_inport is not None and not isinstance(new_inport.operation, Delay):
new_op = new_inport.operation
remaining_inports_per_operation[new_op] -= 1
if remaining_inports_per_operation[new_op] == 0:
next_iter_ports.extend(new_op.outputs)
curr_iter_ports = next_iter_ports
return precedence_list
def show_precedence_graph(self) -> None:
p_list = self.get_precedence_list()
pg = Digraph()
pg.attr(rankdir='LR')
# Creates nodes for each output port in the precedence list
for i in range(len(p_list)):
ports = p_list[i]
with pg.subgraph(name='cluster_' + str(i)) as sub:
sub.attr(label='N' + str(i + 1))
for port in ports:
sub.node(port.operation.graph_id + '.' + str(port.index))
# Creates edges for each output port and creates nodes for each operation and edges for them as well
for i in range(len(p_list)):
ports = p_list[i]
for port in ports:
for signal in port.signals:
pg.edge(port.operation.graph_id + '.' + str(port.index), signal.destination.operation.graph_id)
pg.node(signal.destination.operation.graph_id, shape='square')
pg.edge(port.operation.graph_id, port.operation.graph_id + '.' + str(port.index))
pg.node(port.operation.graph_id, shape='square')
pg.view()
def print_precedence_graph(self) -> None:
"""Prints a representation of the SFG's precedence list to the standard out.
If the precedence list already has been calculated then it uses the cached version,
otherwise it calculates the precedence list and then prints it."""
precedence_list = self.get_precedence_list()
line = "-" * 120
out_str = StringIO()
out_str.write(line)
printed_ops = set()
for iter_num, iter in enumerate(precedence_list, start=1):
for outport_num, outport in enumerate(iter, start=1):
if outport not in printed_ops:
# Only print once per operation, even if it has multiple outports
out_str.write("\n")
out_str.write(str(iter_num))
out_str.write(".")
out_str.write(str(outport_num))
out_str.write(" \t")
out_str.write(str(outport.operation))
printed_ops.add(outport)
out_str.write("\n")
out_str.write(line)
print(out_str.getvalue())
def get_operations_topological_order(self) -> Iterable[Operation]:
"""Returns an Iterable of the Operations in the SFG in Topological Order.
Feedback loops makes an absolutely correct Topological order impossible, so an
approximative Topological Order is returned in such cases in this implementation."""
if self._operations_topological_order:
return self._operations_topological_order
no_inputs_queue = deque(list(filter(lambda op: op.input_count == 0, self.operations)))
remaining_inports_per_operation = {op: op.input_count for op in self.operations}
# Maps number of input counts to a queue of seen objects with such a size.
seen_with_inputs_dict = defaultdict(deque)
seen = set()
top_order = []
assert len(no_inputs_queue) > 0, "Illegal SFG state, dangling signals in SFG."
first_op = no_inputs_queue.popleft()
visited = set([first_op])
p_queue = PriorityQueue()
p_queue_entry_num = it.count()
# Negative priority as max-heap popping is wanted
p_queue.put((-first_op.output_count, -next(p_queue_entry_num), first_op))
operations_left = len(self.operations) - 1
seen_but_not_visited_count = 0
while operations_left > 0:
while not p_queue.empty():
op = p_queue.get()[2]
operations_left -= 1
top_order.append(op)
visited.add(op)
for neighbor_op in op.subsequent_operations:
if neighbor_op not in visited:
remaining_inports_per_operation[neighbor_op] -= 1
remaining_inports = remaining_inports_per_operation[neighbor_op]
if remaining_inports == 0:
p_queue.put((-neighbor_op.output_count, -next(p_queue_entry_num), neighbor_op))
elif remaining_inports > 0:
if neighbor_op in seen:
seen_with_inputs_dict[remaining_inports + 1].remove(neighbor_op)
else:
seen.add(neighbor_op)
seen_but_not_visited_count += 1
seen_with_inputs_dict[remaining_inports].append(neighbor_op)
# Check if have to fetch Operations from somewhere else since p_queue is empty
if operations_left > 0:
# First check if can fetch from Operations with no input ports
if no_inputs_queue:
new_op = no_inputs_queue.popleft()
p_queue.put((-new_op.output_count, -next(p_queue_entry_num), new_op))
# Else fetch operation with lowest input count that is not zero
elif seen_but_not_visited_count > 0:
for i in it.count(start=1):
seen_inputs_queue = seen_with_inputs_dict[i]
if seen_inputs_queue:
new_op = seen_inputs_queue.popleft()
p_queue.put((-new_op.output_count, -next(p_queue_entry_num), new_op))
seen_but_not_visited_count -= 1
break
else:
raise RuntimeError("Unallowed structure in SFG detected")
self._operations_topological_order = top_order
return self._operations_topological_order
def set_latency_of_type(self, type_name: TypeName, latency: int) -> None:
"""TODO: docstring"""
for op in self.get_components_with_type_name(type_name):
op.set_latency(latency)
def set_latency_offsets_of_type(self, type_name: TypeName, latency_offsets: Dict[str, int]) -> None:
"""TODO: docstring"""
for op in self.get_components_with_type_name(type_name):
op.set_latency_offsets(latency_offsets)
def _add_component_unconnected_copy(self, original_component: GraphComponent) -> GraphComponent:
assert original_component not in self._original_components_to_new, "Tried to add duplicate SFG component"
new_component = original_component.copy_component()
self._original_components_to_new[original_component] = new_component
new_id = self._graph_id_generator.next_id(new_component.type_name())
new_component.graph_id = new_id
self._components_by_id[new_id] = new_component
self._components_by_name[new_component.name].append(new_component)
return new_component
def _add_operation_connected_tree_copy(self, start_op: Operation) -> None:
op_stack = deque([start_op])
while op_stack:
original_op = op_stack.pop()
# Add or get the new copy of the operation.
new_op = None
if original_op not in self._original_components_to_new:
new_op = self._add_component_unconnected_copy(original_op)
self._components_dfs_order.append(new_op)
self._operations_dfs_order.append(new_op)
else:
new_op = self._original_components_to_new[original_op]
# Connect input ports to new signals.
for original_input_port in original_op.inputs:
if original_input_port.signal_count < 1:
raise ValueError("Unconnected input port in SFG")
for original_signal in original_input_port.signals:
# Check if the signal is one of the SFG's input signals.
if original_signal in self._original_input_signals_to_indices:
# New signal already created during first step of constructor.
new_signal = self._original_components_to_new[original_signal]
new_signal.set_destination(new_op.input(original_input_port.index))
self._components_dfs_order.extend([new_signal, new_signal.source.operation])
self._operations_dfs_order.append(new_signal.source.operation)
# Check if the signal has not been added before.
elif original_signal not in self._original_components_to_new:
if original_signal.source is None:
raise ValueError("Dangling signal without source in SFG")
new_signal = self._add_component_unconnected_copy(original_signal)
new_signal.set_destination(new_op.input(original_input_port.index))
self._components_dfs_order.append(new_signal)
original_connected_op = original_signal.source.operation
# Check if connected Operation has been added before.
if original_connected_op in self._original_components_to_new:
# Set source to the already added operations port.
new_signal.set_source(self._original_components_to_new[original_connected_op].output(
original_signal.source.index))
else:
# Create new operation, set signal source to it.
new_connected_op = self._add_component_unconnected_copy(original_connected_op)
new_signal.set_source(new_connected_op.output(original_signal.source.index))
self._components_dfs_order.append(new_connected_op)
self._operations_dfs_order.append(new_connected_op)
# Add connected operation to queue of operations to visit.
op_stack.append(original_connected_op)
# Connect output ports.
for original_output_port in original_op.outputs:
for original_signal in original_output_port.signals:
# Check if the signal is one of the SFG's output signals.
if original_signal in self._original_output_signals_to_indices:
# New signal already created during first step of constructor.
new_signal = self._original_components_to_new[original_signal]
new_signal.set_source(new_op.output(original_output_port.index))
self._components_dfs_order.extend([new_signal, new_signal.destination.operation])
self._operations_dfs_order.append(new_signal.destination.operation)
# Check if signal has not been added before.
elif original_signal not in self._original_components_to_new:
if original_signal.source is None:
raise ValueError("Dangling signal without source in SFG")
new_signal = self._add_component_unconnected_copy(original_signal)
new_signal.set_source(new_op.output(original_output_port.index))
self._components_dfs_order.append(new_signal)
original_connected_op = original_signal.destination.operation
# Check if connected operation has been added.
if original_connected_op in self._original_components_to_new:
# Set destination to the already connected operations port.
new_signal.set_destination(self._original_components_to_new[original_connected_op].input(
original_signal.destination.index))
else:
# Create new operation, set destination to it.
new_connected_op = self._add_component_unconnected_copy(original_connected_op)
new_signal.set_destination(new_connected_op.input(original_signal.destination.index))
self._components_dfs_order.append(new_connected_op)
self._operations_dfs_order.append(new_connected_op)
# Add connected operation to the queue of operations to visit.
op_stack.append(original_connected_op)
def _evaluate_source(self, src: OutputPort, results: MutableResultMap, delays: MutableDelayMap, prefix: str, bits_override: Optional[int], truncate: bool, deferred_delays: DelayQueue) -> Number:
key_base = (prefix + "." + src.operation.graph_id) if prefix else src.operation.graph_id
key = src.operation.key(src.index, key_base)
if key in results:
value = results[key]
if value is None:
raise RuntimeError("Direct feedback loop detected when evaluating operation.")
return value
value = src.operation.current_output(src.index, delays, key_base)
results[key] = value
if value is None:
value = self._do_evaluate_source(key_base, key, src, results, delays, prefix, bits_override, truncate, deferred_delays)
else:
deferred_delays.append((key_base, key, src)) # Evaluate later. Use current value for now.
return value
def _do_evaluate_source(self, key_base: str, key: ResultKey, src: OutputPort, results: MutableResultMap, delays: MutableDelayMap, prefix: str, bits_override: Optional[int], truncate: bool, deferred_delays: DelayQueue) -> Number:
input_values = [self._evaluate_source(input_port.signals[0].source, results, delays, prefix, bits_override, truncate, deferred_delays) for input_port in src.operation.inputs]
value = src.operation.evaluate_output(src.index, input_values, results, delays, key_base, bits_override, truncate)
results[key] = value
return value
def _inputs_required_for_source(self, src: OutputPort, visited: MutableSet[Operation]) -> Sequence[bool]:
if src.operation in visited:
return []
visited.add(src.operation)
if isinstance(src.operation, Input):
for i, input_operation in enumerate(self._input_operations):
if input_operation is src.operation:
return [i]
input_indices = []
for i in src.operation.inputs_required_for_output(src.index):
input_indices.extend(self._inputs_required_for_source(src.operation.input(i).signals[0].source, visited))
return input_indices