diff --git a/README.md b/README.md index b2972f30828f19038e5efe612831f989b8f5ffd5..d28399ce800d0240881c14405c892eb65d23b072 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,15 @@ To run the test suite, the following additional packages are required: * pytest * pytest-cov (for testing with coverage) +To build a binary distribution, the following additional packages are required: +* Python: + * wheel + +To run the test suite, the following additional packages are required: +* Python: + * pytest + * pytest-cov (for testing with coverage) + ### Using CMake directly How to build using CMake. diff --git a/b_asic/__init__.py b/b_asic/__init__.py index b35d0c1be97e5dc2758e8647147e918916103d40..8dc848b23ed11edf63d8ff88c3aa2e553a32e095 100644 --- a/b_asic/__init__.py +++ b/b_asic/__init__.py @@ -10,3 +10,4 @@ from b_asic.signal_flow_graph import * from b_asic.signal import * from b_asic.simulation import * from b_asic.special_operations import * +from b_asic.schema import * diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index a70c86b7d966197850604ad7a269242d364eca36..9a11d91943e9621e43daa3c88ddfa67d3441060c 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -4,7 +4,7 @@ TODO: More info. """ from numbers import Number -from typing import Optional +from typing import Optional, Dict from numpy import conjugate, sqrt, abs as np_abs from b_asic.port import SignalSourceProvider, InputPort, OutputPort @@ -44,9 +44,9 @@ class Addition(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=2, output_count=1, - name=name, input_sources=[src0, src1]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -61,9 +61,9 @@ class Subtraction(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=2, output_count=1, - name=name, input_sources=[src0, src1]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -78,9 +78,9 @@ class Multiplication(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=2, output_count=1, - name=name, input_sources=[src0, src1]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -95,9 +95,9 @@ class Division(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=2, output_count=1, - name=name, input_sources=[src0, src1]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -112,9 +112,9 @@ class Min(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=2, output_count=1, - name=name, input_sources=[src0, src1]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -131,9 +131,9 @@ class Max(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=2, output_count=1, - name=name, input_sources=[src0, src1]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -150,9 +150,9 @@ class SquareRoot(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=1, output_count=1, - name=name, input_sources=[src0]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -167,9 +167,9 @@ class ComplexConjugate(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=1, output_count=1, - name=name, input_sources=[src0]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -184,9 +184,9 @@ class Absolute(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=1, output_count=1, - name=name, input_sources=[src0]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -201,9 +201,9 @@ class ConstantMultiplication(AbstractOperation): TODO: More info. """ - def __init__(self, value: Number = 0, src0: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=1, output_count=1, - name=name, input_sources=[src0]) + def __init__(self, value: Number = 0, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0], + latency=latency, latency_offsets=latency_offsets) self.set_param("value", value) @classmethod @@ -230,9 +230,9 @@ class Butterfly(AbstractOperation): TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count=2, output_count=2, - name=name, input_sources=[src0, src1]) + def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: int = None, latency_offsets: Dict[str, int] = None): + super().__init__(input_count=2, output_count=2, name=name, input_sources=[src0, src1], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: @@ -241,13 +241,15 @@ class Butterfly(AbstractOperation): def evaluate(self, a, b): return a + b, a - b + class MAD(AbstractOperation): """Multiply-and-add operation. TODO: More info. """ - def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, src2: Optional[SignalSourceProvider] = None, name: Name = ""): - super().__init__(input_count = 3, output_count = 1, name = name, input_sources = [src0, src1, src2]) + 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__(input_count=3, output_count=1, name=name, input_sources=[src0, src1, src2], + latency=latency, latency_offsets=latency_offsets) @classmethod def type_name(cls) -> TypeName: diff --git a/b_asic/operation.py b/b_asic/operation.py index 02ba1aa50682448e931a0694d24e03e20eadd399..d1a820fbe49592e5545ff5624dc5fc121c77b71f 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -3,16 +3,17 @@ B-ASIC Operation Module. 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 numbers import Number -from typing import NewType, List, Sequence, Iterable, Mapping, MutableMapping, Optional, Any, Set, Union -from math import trunc +from typing import NewType, List, Dict, Sequence, Iterable, Mapping, MutableMapping, Optional, Any, Set, Union -from b_asic.graph_component import GraphComponent, AbstractGraphComponent, Name -from b_asic.port import SignalSourceProvider, InputPort, OutputPort -from b_asic.signal import Signal OutputKey = NewType("OutputKey", str) OutputMap = Mapping[OutputKey, Optional[Number]] @@ -193,6 +194,39 @@ class Operation(GraphComponent, SignalSourceProvider): """ 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 + @abstractmethod + def latency_offsets(self) -> Sequence[Sequence[int]]: + """Get a nested list with all the operations ports latency-offsets, the first list contains the + 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 + class AbstractOperation(Operation, AbstractGraphComponent): """Generic abstract operation class which most implementations will derive from. @@ -202,7 +236,7 @@ class AbstractOperation(Operation, AbstractGraphComponent): _input_ports: List[InputPort] _output_ports: List[OutputPort] - def __init__(self, input_count: int, output_count: int, name: Name = "", input_sources: Optional[Sequence[Optional[SignalSourceProvider]]] = None): + 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) self._input_ports = [InputPort(self, i) for i in range(input_count)] @@ -218,6 +252,22 @@ class AbstractOperation(Operation, AbstractGraphComponent): 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 def evaluate(self, *inputs) -> Any: # pylint: disable=arguments-differ """Evaluate the operation and generate a list of output values given a list of input values.""" @@ -433,6 +483,14 @@ class AbstractOperation(Operation, AbstractGraphComponent): 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})") @@ -483,3 +541,32 @@ class AbstractOperation(Operation, AbstractGraphComponent): else: args.append(input_values[i]) return args + + @property + def latency(self) -> int: + 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]]: + return ([inp.latency_offset for inp in self.inputs], [outp.latency_offset for outp in self.outputs]) + + 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") diff --git a/b_asic/port.py b/b_asic/port.py index 20783d5df0962b034aee2b6e934255a9fc9cd6e6..fb3f64177e74f2623d02284243a03529cf05b6e4 100644 --- a/b_asic/port.py +++ b/b_asic/port.py @@ -32,6 +32,18 @@ class Port(ABC): """Return the index of the port.""" raise NotImplementedError + @property + @abstractmethod + def latency_offset(self) -> int: + """Get the latency_offset of the port.""" + raise NotImplementedError + + @latency_offset.setter + @abstractmethod + def latency_offset(self, latency_offset: int) -> None: + """Set the latency_offset of the port to the integer specified value.""" + raise NotImplementedError + @property @abstractmethod def signal_count(self) -> int: @@ -76,10 +88,12 @@ class AbstractPort(Port): _operation: "Operation" _index: int + _latency_offset: Optional[int] - def __init__(self, operation: "Operation", index: int): + def __init__(self, operation: "Operation", index: int, latency_offset: int = None): self._operation = operation self._index = index + self._latency_offset = latency_offset @property def operation(self) -> "Operation": @@ -89,6 +103,14 @@ class AbstractPort(Port): def index(self) -> int: 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. diff --git a/b_asic/schema.py b/b_asic/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..157ed3b8ec57a35ffe66f1de476e2d77c3e28abd --- /dev/null +++ b/b_asic/schema.py @@ -0,0 +1,107 @@ +"""@package docstring +This module contains the Schema class. +TODO: More info +""" + +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 + + +class Schema: + """A class that represents an SFG with scheduled Operations.""" + + _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 + + 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: + print(inport.operation.graph_id) + 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 diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index d51f13b4209fd1d5fc87e369b2f23dc8bf69301b..1a0cd8e7cee0f1ae8798233348a7b45e12f7e74f 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -8,7 +8,7 @@ from numbers import Number from collections import defaultdict, deque from io import StringIO from queue import PriorityQueue -import itertools +import itertools as it from graphviz import Digraph from b_asic.port import SignalSourceProvider, OutputPort @@ -612,7 +612,7 @@ class SFG(AbstractOperation): def show_precedence_graph(self) -> None: p_list = self.get_precedence_list() pg = Digraph() - pg.attr(rankdir = 'LR') + pg.attr(rankdir='LR') # Creates nodes for each output port in the precedence list for i in range(len(p_list)): @@ -627,11 +627,11 @@ class SFG(AbstractOperation): 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.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.node(port.operation.graph_id, shape='square') - pg.view() + pg.view() def print_precedence_graph(self) -> None: """Prints a representation of the SFG's precedence list to the standard out. @@ -682,14 +682,18 @@ class SFG(AbstractOperation): first_op = no_inputs_queue.popleft() visited = set([first_op]) p_queue = PriorityQueue() - p_queue.put((-first_op.output_count, first_op)) # Negative priority as max-heap popping is wanted + 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()[1] + + op = p_queue.get()[2] operations_left -= 1 top_order.append(op) @@ -701,7 +705,7 @@ class SFG(AbstractOperation): remaining_inports = remaining_inports_per_operation[neighbor_op] if remaining_inports == 0: - p_queue.put((-neighbor_op.output_count, neighbor_op)) + p_queue.put((-neighbor_op.output_count, -next(p_queue_entry_num), neighbor_op)) elif remaining_inports > 0: if neighbor_op in seen: @@ -717,15 +721,15 @@ class SFG(AbstractOperation): # 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, new_op)) + 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 itertools.count(start=1): + 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, new_op)) + p_queue.put((-new_op.output_count, -next(p_queue_entry_num), new_op)) seen_but_not_visited_count -= 1 break else: @@ -734,3 +738,11 @@ class SFG(AbstractOperation): self._operations_topological_order = top_order return self._operations_topological_order + + def set_latency_of_type(self, type_name: TypeName, latency: int): + 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]): + for op in self.get_components_with_type_name(type_name): + op.set_latency_offsets(latency_offsets) diff --git a/b_asic/special_operations.py b/b_asic/special_operations.py index 0a256bc86c7d5e24582a54d95b7d3afbef1ad941..14440eb098e7100e5399f3ec023b17bcab4ed458 100644 --- a/b_asic/special_operations.py +++ b/b_asic/special_operations.py @@ -6,7 +6,7 @@ TODO: More info. from numbers import Number from typing import Optional, Sequence -from b_asic.operation import AbstractOperation, OutputKey, RegisterMap, MutableOutputMap, MutableRegisterMap +from b_asic.operation import AbstractOperation, RegisterMap, MutableOutputMap, MutableRegisterMap from b_asic.graph_component import Name, TypeName from b_asic.port import SignalSourceProvider diff --git a/test/fixtures/signal_flow_graph.py b/test/fixtures/signal_flow_graph.py index 08d9e8aa2bacd0b1c1a11c17c174179d853e6ed7..6cf434dbfa5fbad4380e4b4d05ef07b9a02e30ec 100644 --- a/test/fixtures/signal_flow_graph.py +++ b/test/fixtures/signal_flow_graph.py @@ -21,12 +21,12 @@ def sfg_two_inputs_two_outputs(): out1 = in1 + in2 out2 = in1 + 2 * in2 """ - in1 = Input() - in2 = Input() - add1 = in1 + in2 - add2 = add1 + in2 - out1 = Output(add1) - out2 = Output(add2) + in1 = Input("IN1") + in2 = Input("IN2") + add1 = Addition(in1, in2, "ADD1") + add2 = Addition(add1, in2, "ADD2") + out1 = Output(add1, "OUT1") + out2 = Output(add2, "OUT2") return SFG(inputs=[in1, in2], outputs=[out1, out2]) @@ -58,6 +58,31 @@ def sfg_two_inputs_two_outputs_independent(): return SFG(inputs=[in1, in2], outputs=[out1, out2]) +@pytest.fixture +def sfg_two_inputs_two_outputs_independent_with_cmul(): + """Valid SFG with two inputs and two outputs, where the first output only depends + on the first input and the second output only depends on the second input. + . . + in1--->cmul1--->cmul2--->out1 + . . + . . + c1------+ . + | + v . + in2--->add1---->cmul3--->out2 + """ + in1 = Input("IN1") + in2 = Input("IN2") + c1 = Constant(3, "C1") + add1 = Addition(in2, c1, "ADD1", 7) + cmul3 = ConstantMultiplication(2, add1, "CMUL3", 3) + cmul1 = ConstantMultiplication(5, in1, "CMUL1", 5) + cmul2 = ConstantMultiplication(4, cmul1, "CMUL2", 4) + out1 = Output(in1, "OUT1") + out2 = Output(add1, "OUT2") + return SFG(inputs=[in1, in2], outputs=[out1, out2]) + + @pytest.fixture def sfg_nested(): """Valid SFG with two inputs and one output. diff --git a/test/test_abstract_operation.py b/test/test_abstract_operation.py deleted file mode 100644 index 9163fce2a955c7fbc68d5d24de86896d251934da..0000000000000000000000000000000000000000 --- a/test/test_abstract_operation.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -B-ASIC test suite for the AbstractOperation class. -""" - -import pytest - -from b_asic import Addition, Subtraction, Multiplication, ConstantMultiplication, Division - - -def test_addition_overload(): - """Tests addition overloading for both operation and number argument.""" - add1 = Addition(None, None, "add1") - add2 = Addition(None, None, "add2") - - add3 = add1 + add2 - assert isinstance(add3, Addition) - assert add3.input(0).signals == add1.output(0).signals - assert add3.input(1).signals == add2.output(0).signals - - add4 = add3 + 5 - assert isinstance(add4, Addition) - assert add4.input(0).signals == add3.output(0).signals - assert add4.input(1).signals[0].source.operation.value == 5 - - add5 = 5 + add4 - assert isinstance(add5, Addition) - assert add5.input(0).signals[0].source.operation.value == 5 - assert add5.input(1).signals == add4.output(0).signals - - -def test_subtraction_overload(): - """Tests subtraction overloading for both operation and number argument.""" - add1 = Addition(None, None, "add1") - add2 = Addition(None, None, "add2") - - sub1 = add1 - add2 - assert isinstance(sub1, Subtraction) - assert sub1.input(0).signals == add1.output(0).signals - assert sub1.input(1).signals == add2.output(0).signals - - sub2 = sub1 - 5 - assert isinstance(sub2, Subtraction) - assert sub2.input(0).signals == sub1.output(0).signals - assert sub2.input(1).signals[0].source.operation.value == 5 - - sub3 = 5 - sub2 - assert isinstance(sub3, Subtraction) - assert sub3.input(0).signals[0].source.operation.value == 5 - assert sub3.input(1).signals == sub2.output(0).signals - - -def test_multiplication_overload(): - """Tests multiplication overloading for both operation and number argument.""" - add1 = Addition(None, None, "add1") - add2 = Addition(None, None, "add2") - - mul1 = add1 * add2 - assert isinstance(mul1, Multiplication) - assert mul1.input(0).signals == add1.output(0).signals - assert mul1.input(1).signals == add2.output(0).signals - - mul2 = mul1 * 5 - assert isinstance(mul2, ConstantMultiplication) - assert mul2.input(0).signals == mul1.output(0).signals - assert mul2.value == 5 - - mul3 = 5 * mul2 - assert isinstance(mul3, ConstantMultiplication) - assert mul3.input(0).signals == mul2.output(0).signals - assert mul3.value == 5 - - -def test_division_overload(): - """Tests division overloading for both operation and number argument.""" - add1 = Addition(None, None, "add1") - add2 = Addition(None, None, "add2") - - div1 = add1 / add2 - assert isinstance(div1, Division) - assert div1.input(0).signals == add1.output(0).signals - assert div1.input(1).signals == add2.output(0).signals - - div2 = div1 / 5 - assert isinstance(div2, Division) - assert div2.input(0).signals == div1.output(0).signals - assert div2.input(1).signals[0].source.operation.value == 5 - - div3 = 5 / div2 - assert isinstance(div3, Division) - assert div3.input(0).signals[0].source.operation.value == 5 - assert div3.input(1).signals == div2.output(0).signals diff --git a/test/test_operation.py b/test/test_operation.py index 77e9ba3cbd0eaa75886b5a7e5d11f00f6cfeb479..52ae8c53457994642c505f722655b47551862ba5 100644 --- a/test/test_operation.py +++ b/test/test_operation.py @@ -1,6 +1,94 @@ +""" +B-ASIC test suite for the AbstractOperation class. +""" + import pytest -from b_asic import Constant, Addition, MAD, Butterfly, SquareRoot +from b_asic import Addition, Subtraction, Multiplication, ConstantMultiplication, Division, Constant, Butterfly, \ + MAD, SquareRoot + + +class TestOperationOverloading: + def test_addition_overload(self): + """Tests addition overloading for both operation and number argument.""" + add1 = Addition(None, None, "add1") + add2 = Addition(None, None, "add2") + + add3 = add1 + add2 + assert isinstance(add3, Addition) + assert add3.input(0).signals == add1.output(0).signals + assert add3.input(1).signals == add2.output(0).signals + + add4 = add3 + 5 + assert isinstance(add4, Addition) + assert add4.input(0).signals == add3.output(0).signals + assert add4.input(1).signals[0].source.operation.value == 5 + + add5 = 5 + add4 + assert isinstance(add5, Addition) + assert add5.input(0).signals[0].source.operation.value == 5 + assert add5.input(1).signals == add4.output(0).signals + + def test_subtraction_overload(self): + """Tests subtraction overloading for both operation and number argument.""" + add1 = Addition(None, None, "add1") + add2 = Addition(None, None, "add2") + + sub1 = add1 - add2 + assert isinstance(sub1, Subtraction) + assert sub1.input(0).signals == add1.output(0).signals + assert sub1.input(1).signals == add2.output(0).signals + + sub2 = sub1 - 5 + assert isinstance(sub2, Subtraction) + assert sub2.input(0).signals == sub1.output(0).signals + assert sub2.input(1).signals[0].source.operation.value == 5 + + sub3 = 5 - sub2 + assert isinstance(sub3, Subtraction) + assert sub3.input(0).signals[0].source.operation.value == 5 + assert sub3.input(1).signals == sub2.output(0).signals + + def test_multiplication_overload(self): + """Tests multiplication overloading for both operation and number argument.""" + add1 = Addition(None, None, "add1") + add2 = Addition(None, None, "add2") + + mul1 = add1 * add2 + assert isinstance(mul1, Multiplication) + assert mul1.input(0).signals == add1.output(0).signals + assert mul1.input(1).signals == add2.output(0).signals + + mul2 = mul1 * 5 + assert isinstance(mul2, ConstantMultiplication) + assert mul2.input(0).signals == mul1.output(0).signals + assert mul2.value == 5 + + mul3 = 5 * mul2 + assert isinstance(mul3, ConstantMultiplication) + assert mul3.input(0).signals == mul2.output(0).signals + assert mul3.value == 5 + + def test_division_overload(self): + """Tests division overloading for both operation and number argument.""" + add1 = Addition(None, None, "add1") + add2 = Addition(None, None, "add2") + + div1 = add1 / add2 + assert isinstance(div1, Division) + assert div1.input(0).signals == add1.output(0).signals + assert div1.input(1).signals == add2.output(0).signals + + div2 = div1 / 5 + assert isinstance(div2, Division) + assert div2.input(0).signals == div1.output(0).signals + assert div2.input(1).signals[0].source.operation.value == 5 + + div3 = 5 / div2 + assert isinstance(div3, Division) + assert div3.input(0).signals[0].source.operation.value == 5 + assert div3.input(1).signals == div2.output(0).signals + class TestTraverse: def test_traverse_single_tree(self, operation): @@ -24,20 +112,21 @@ class TestTraverse: def test_traverse_loop(self, operation_graph_with_cycle): assert len(list(operation_graph_with_cycle.traverse())) == 8 + class TestToSfg: def test_convert_mad_to_sfg(self): mad1 = MAD() mad1_sfg = mad1.to_sfg() - assert mad1.evaluate(1,1,1) == mad1_sfg.evaluate(1,1,1) + assert mad1.evaluate(1, 1, 1) == mad1_sfg.evaluate(1, 1, 1) assert len(mad1_sfg.operations) == 6 def test_butterfly_to_sfg(self): but1 = Butterfly() but1_sfg = but1.to_sfg() - assert but1.evaluate(1,1)[0] == but1_sfg.evaluate(1,1)[0] - assert but1.evaluate(1,1)[1] == but1_sfg.evaluate(1,1)[1] + assert but1.evaluate(1, 1)[0] == but1_sfg.evaluate(1, 1)[0] + assert but1.evaluate(1, 1)[1] == but1_sfg.evaluate(1, 1)[1] assert len(but1_sfg.operations) == 8 def test_add_to_sfg(self): @@ -51,3 +140,47 @@ class TestToSfg: sqrt1_sfg = sqrt1.to_sfg() assert len(sqrt1_sfg.operations) == 3 + + +class TestLatency: + def test_latency_constructor(self): + bfly = Butterfly(latency=5) + + assert bfly.latency == 5 + assert list(bfly.latency_offsets) == [[0, 0], [5, 5]] + + def test_latency_offsets_constructor(self): + bfly = Butterfly(latency_offsets={'in0': 2, 'in1': 3, 'out0': 5, 'out1': 10}) + + assert bfly.latency == 8 + assert list(bfly.latency_offsets) == [[2, 3], [5, 10]] + + def test_latency_and_latency_offsets_constructor(self): + bfly = Butterfly(latency=5, latency_offsets={'in1': 2, 'out0': 9}) + + assert bfly.latency == 9 + assert list(bfly.latency_offsets) == [[0, 2], [9, 5]] + + def test_set_latency(self): + bfly = Butterfly() + + bfly.set_latency(9) + + assert bfly.latency == 9 + assert list(bfly.latency_offsets) == [[0, 0], [9, 9]] + + def test_set_latency_offsets(self): + bfly = Butterfly() + + bfly.set_latency_offsets({'in0': 3, 'out1': 5}) + + assert list(bfly.latency_offsets) == [[3, None], [None, 5]] + + +class TestCopyOperation: + def test_copy_buttefly_latency_offsets(self): + bfly = Butterfly(latency_offsets={'in0': 4, 'in1': 2, 'out0': 10, 'out1': 9}) + + bfly_copy = bfly.copy_component() + + assert list(bfly_copy.latency_offsets) == [[4, 2], [10, 9]] diff --git a/test/test_schema.py b/test/test_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..510a0d997cb6cbdb525d600f992cbbae3468e8dd --- /dev/null +++ b/test/test_schema.py @@ -0,0 +1,67 @@ +""" +B-ASIC test suite for the schema module and Schema class. +""" + +from b_asic import Schema, Addition, ConstantMultiplication + + +class TestInit: + def test_simple_filter_normal_latency(self, simple_filter): + simple_filter.set_latency_of_type(Addition.type_name(), 5) + simple_filter.set_latency_of_type(ConstantMultiplication.type_name(), 4) + + schema = Schema(simple_filter) + + assert schema._start_times == {"add1": 4, "cmul1": 0} + + def test_complicated_single_outputs_normal_latency(self, precedence_sfg_registers): + precedence_sfg_registers.set_latency_of_type(Addition.type_name(), 4) + precedence_sfg_registers.set_latency_of_type(ConstantMultiplication.type_name(), 3) + + schema = Schema(precedence_sfg_registers, scheduling_alg="ASAP") + + for op in schema._sfg.get_operations_topological_order(): + print(op.latency_offsets) + + start_times_names = dict() + for op_id, start_time in schema._start_times.items(): + op_name = precedence_sfg_registers.find_by_id(op_id).name + start_times_names[op_name] = start_time + + assert start_times_names == {"C0": 0, "B1": 0, "B2": 0, "ADD2": 3, "ADD1": 7, "Q1": 11, + "A0": 14, "A1": 0, "A2": 0, "ADD3": 3, "ADD4": 17} + + def test_complicated_single_outputs_complex_latencies(self, precedence_sfg_registers): + precedence_sfg_registers.set_latency_offsets_of_type(ConstantMultiplication.type_name(), {'in0': 3, 'out0': 5}) + + precedence_sfg_registers.find_by_name("B1")[0].set_latency_offsets({'in0': 4, 'out0': 7}) + precedence_sfg_registers.find_by_name("B2")[0].set_latency_offsets({'in0': 1, 'out0': 4}) + precedence_sfg_registers.find_by_name("ADD2")[0].set_latency_offsets({'in0': 4, 'in1': 2, 'out0': 4}) + precedence_sfg_registers.find_by_name("ADD1")[0].set_latency_offsets({'in0': 1, 'in1': 2, 'out0': 4}) + precedence_sfg_registers.find_by_name("Q1")[0].set_latency_offsets({'in0': 3, 'out0': 6}) + precedence_sfg_registers.find_by_name("A0")[0].set_latency_offsets({'in0': 0, 'out0': 2}) + + precedence_sfg_registers.find_by_name("A1")[0].set_latency_offsets({'in0': 0, 'out0': 5}) + precedence_sfg_registers.find_by_name("A2")[0].set_latency_offsets({'in0': 2, 'out0': 3}) + precedence_sfg_registers.find_by_name("ADD3")[0].set_latency_offsets({'in0': 2, 'in1': 1, 'out0': 4}) + precedence_sfg_registers.find_by_name("ADD4")[0].set_latency_offsets({'in0': 6, 'in1': 7, 'out0': 9}) + + schema = Schema(precedence_sfg_registers, scheduling_alg="ASAP") + + start_times_names = dict() + for op_id, start_time in schema._start_times.items(): + op_name = precedence_sfg_registers.find_by_id(op_id).name + start_times_names[op_name] = start_time + + assert start_times_names == {'C0': 0, 'B1': 0, 'B2': 0, 'ADD2': 3, 'ADD1': 5, 'Q1': 6, 'A0': 12, + 'A1': 0, 'A2': 0, 'ADD3': 3, 'ADD4': 8} + + def test_independent_sfg(self, sfg_two_inputs_two_outputs_independent_with_cmul): + schema = Schema(sfg_two_inputs_two_outputs_independent_with_cmul, scheduling_alg="ASAP") + + start_times_names = dict() + for op_id, start_time in schema._start_times.items(): + op_name = sfg_two_inputs_two_outputs_independent_with_cmul.find_by_id(op_id).name + start_times_names[op_name] = start_time + + assert start_times_names == {'CMUL1': 0, 'CMUL2': 5, "ADD1": 0, "CMUL3": 7} diff --git a/test/test_sfg.py b/test/test_sfg.py index a27b404e8d2ac0eaf6b4146ed497e2f06b5973cf..5a13e151dbb682c60f732cfc8399da4ea1e3a34b 100644 --- a/test/test_sfg.py +++ b/test/test_sfg.py @@ -691,12 +691,13 @@ class TestConnectExternalSignalsToComponentsMultipleComp: out1.input(0).connect(sub1, "S7") test_sfg = SFG(inputs=[inp1, inp2, inp3, inp4], outputs=[out1]) - + assert test_sfg.evaluate(1, 2, 3, 4) == 16 sfg1.connect_external_signals_to_components() assert test_sfg.evaluate(1, 2, 3, 4) == 16 assert not test_sfg.connect_external_signals_to_components() + class TestTopologicalOrderOperations: def test_feedback_sfg(self, simple_filter): topological_order = simple_filter.get_operations_topological_order() @@ -708,6 +709,12 @@ class TestTopologicalOrderOperations: assert [comp.name for comp in topological_order] == ["IN1", "OUT1", "IN2", "C1", "ADD1", "OUT2"] + def test_complex_graph(self, precedence_sfg_registers): + topological_order = precedence_sfg_registers.get_operations_topological_order() + + assert [comp.name for comp in topological_order] == \ + ['IN1', 'C0', 'ADD1', 'Q1', 'A0', 'T1', 'B1', 'A1', 'T2', 'B2', 'ADD2', 'A2', 'ADD3', 'ADD4', 'OUT1'] + class TestRemove: def test_remove_single_input_outputs(self, simple_filter): @@ -776,3 +783,19 @@ class TestRemove: def remove_different_number_inputs_outputs(self, simple_filter): with pytest.raises(ValueError): simple_filter.remove_operation("add1") + + +class TestGetComponentsOfType: + def test_get_no_operations_of_type(self, sfg_two_inputs_two_outputs): + assert [op.name for op in sfg_two_inputs_two_outputs.get_components_with_type_name(Multiplication.type_name())] \ + == [] + + def test_get_multple_operations_of_type(self, sfg_two_inputs_two_outputs): + assert [op.name for op in sfg_two_inputs_two_outputs.get_components_with_type_name(Addition.type_name())] \ + == ["ADD1", "ADD2"] + + assert [op.name for op in sfg_two_inputs_two_outputs.get_components_with_type_name(Input.type_name())] \ + == ["IN1", "IN2"] + + assert [op.name for op in sfg_two_inputs_two_outputs.get_components_with_type_name(Output.type_name())] \ + == ["OUT1", "OUT2"]