diff --git a/b_asic/save_load_structure.py b/b_asic/save_load_structure.py index bcc10c34d58063af6f6538df9ddf69c79c33f7df..532489ffcfc024e10ac3db7d6ed60a44e1e434c1 100644 --- a/b_asic/save_load_structure.py +++ b/b_asic/save_load_structure.py @@ -11,11 +11,12 @@ from typing import Dict, Optional, Tuple, cast from b_asic.graph_component import GraphComponent from b_asic.port import InputPort +from b_asic.schedule import Schedule from b_asic.signal_flow_graph import SFG def sfg_to_python( - sfg: SFG, counter: int = 0, suffix: Optional[str] = None + sfg: SFG, counter: int = 0, suffix: Optional[str] = None, schedule=False ) -> str: """ Given an SFG structure try to serialize it for saving to a file. @@ -23,15 +24,20 @@ def sfg_to_python( Parameters ========== sfg : SFG - The SFG to serialize + The SFG to serialize. counter : int, default: 0 Number used for naming the SFG. Enables SFGs in SFGs. suffix : str, optional String to append at the end of the result. + schedule : bool, default: False + True if printing a schedule. """ + _type = "Schedule" if schedule else "SFG" + result = ( - '\n"""\nB-ASIC automatically generated SFG file.\n' + '\n"""\n' + + f"B-ASIC automatically generated {_type} file.\n" + "Name: " + f"{sfg.name}" + "\n" @@ -44,6 +50,8 @@ def sfg_to_python( result += "\nfrom b_asic import SFG, Signal, Input, Output" for op_type in {type(op) for op in sfg.operations}: result += f", {op_type.__name__}" + if schedule: + result += ", Schedule" def kwarg_unpacker(comp: GraphComponent, params=None) -> str: if params is None: @@ -61,56 +69,51 @@ def sfg_to_python( params = {k: v for k, v in params.items() if v} if params.get("latency_offsets", None) is not None: params["latency_offsets"] = { - k: v - for k, v in params["latency_offsets"].items() - if v is not None + k: v for k, v in params["latency_offsets"].items() if v is not None } if not params["latency_offsets"]: del params["latency_offsets"] - return ", ".join( - [f"{param}={value}" for param, value in params.items()] - ) + return ", ".join([f"{param}={value}" for param, value in params.items()]) # No need to redefined I/Os - io_ops = [*sfg._input_operations, *sfg._output_operations] + io_ops = [*sfg.input_operations, *sfg.output_operations] result += "\n# Inputs:\n" - for input_op in sfg._input_operations: + for input_op in sfg.input_operations: result += f"{input_op.graph_id} = Input({kwarg_unpacker(input_op)})\n" result += "\n# Outputs:\n" - for output_op in sfg._output_operations: - result += ( - f"{output_op.graph_id} = Output({kwarg_unpacker(output_op)})\n" - ) + for output_op in sfg.output_operations: + result += f"{output_op.graph_id} = Output({kwarg_unpacker(output_op)})\n" result += "\n# Operations:\n" - for op in sfg.split(): - if op in io_ops: + for operation in sfg.split(): + if operation in io_ops: continue - if isinstance(op, SFG): + if isinstance(operation, SFG): counter += 1 - result = sfg_to_python(op, counter) + result + result = sfg_to_python(operation, counter) + result continue result += ( - f"{op.graph_id} = {op.__class__.__name__}({kwarg_unpacker(op)})\n" + f"{operation.graph_id} =" + f" {operation.__class__.__name__}({kwarg_unpacker(operation)})\n" ) result += "\n# Signals:\n" # Keep track of already existing connections to avoid adding duplicates connections = [] - for op in sfg.split(): - for out in op.outputs: + for operation in sfg.split(): + for out in operation.outputs: for signal in out.signals: destination = cast(InputPort, signal.destination) dest_op = destination.operation connection = ( - f"\nSignal(source={op.graph_id}." - f"output({op.outputs.index(signal.source)})," + f"Signal(source={operation.graph_id}." + f"output({operation.outputs.index(signal.source)})," f" destination={dest_op.graph_id}." - f"input({dest_op.inputs.index(destination)}))" + f"input({dest_op.inputs.index(destination)}))\n" ) if connection in connections: continue @@ -119,20 +122,14 @@ def sfg_to_python( 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 f"sfg{counter}" if counter > 0 else "sfg" - ) - sfg_name_var = sfg_name.replace(" ", "_") - result += ( - f"\n{sfg_name_var} = SFG(inputs={inputs}, outputs={outputs}," - f" name='{sfg_name}')\n" - ) + outputs = "[" + ", ".join(op.graph_id for op in sfg.output_operations) + "]" + sfg_name = sfg.name if sfg.name else f"sfg{counter}" if counter > 0 else "sfg" + sfg_name_var = sfg_name.replace(" ", "_").replace("-", "_") + result += "\n# Signal flow graph:\n" result += ( - "\n# SFG Properties:\n" + "prop = {'name':" + f"{sfg_name_var}" + "}" + f"{sfg_name_var} = SFG(inputs={inputs}, outputs={outputs}, name='{sfg_name}')\n" ) + result += "\n# SFG Properties:\n" + "prop = {'name':" + f"{sfg_name_var}" + "}\n" if suffix is not None: result += "\n" + suffix + "\n" @@ -149,8 +146,8 @@ def python_to_sfg(path: str) -> Tuple[SFG, Dict[str, Tuple[int, int]]]: path : str Path to file to read and deserialize. """ - with open(path) as f: - code = compile(f.read(), path, "exec") + with open(path) as file: + code = compile(file.read(), path, "exec") exec(code, globals(), locals()) return ( @@ -159,3 +156,22 @@ def python_to_sfg(path: str) -> Tuple[SFG, Dict[str, Tuple[int, int]]]: else [v for k, v in locals().items() if isinstance(v, SFG)][0], locals()["positions"] if "positions" in locals() else {}, ) + + +def schedule_to_python(schedule: Schedule): + """ + Given a schedule structure try to serialize it for saving to a file. + + Parameters + ========== + schedule : Schedule + The schedule to serialize. + """ + sfg_name = schedule.sfg.name.replace(" ", "_").replace("-", "_") + result = "\n# Schedule:\n" + result += ( + f"{sfg_name}_schedule = Schedule({sfg_name}, {schedule.schedule_time}," + f" {schedule.cyclic}, 'provided', {schedule.start_times}," + f" {dict(schedule.laps)})\n" + ) + return sfg_to_python(schedule.sfg, schedule=True) + result diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 6eaa711166af4d6cd5db362c2f0451f6c4583af6..595c65c5e28f2206496acd0c1086fc221beddfdc 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -55,8 +55,15 @@ class Schedule: algorithm. cyclic : bool, default: False If the schedule is cyclic. - scheduling_algorithm : {'ASAP'}, optional + scheduling_algorithm : {'ASAP', 'provided'}, optional The scheduling algorithm to use. Currently, only "ASAP" is supported. + If 'provided', use provided *start_times* and *laps* dictionaries. + start_times : dict, optional + Dictionary with GraphIDs as keys and start times as values. + Used when *scheduling_algorithm* is 'provided'. + laps : dict, optional + Dictionary with GraphIDs as keys and laps as values. + Used when *scheduling_algorithm* is 'provided'. """ _sfg: SFG @@ -72,8 +79,11 @@ class Schedule: schedule_time: Optional[int] = None, cyclic: bool = False, scheduling_algorithm: str = "ASAP", + start_times: Dict[GraphID, int] = None, + laps: Dict[GraphID, int] = None, ): """Construct a Schedule from an SFG.""" + self._original_sfg = sfg() # Make a copy self._sfg = sfg self._start_times = {} self._laps = defaultdict(lambda: 0) @@ -81,6 +91,10 @@ class Schedule: self._y_locations = defaultdict(lambda: None) if scheduling_algorithm == "ASAP": self._schedule_asap() + elif scheduling_algorithm == "provided": + self._start_times = start_times + self._laps.update(laps) + self._remove_delays_no_laps() else: raise NotImplementedError( f"No algorithm with name: {scheduling_algorithm} defined." @@ -107,8 +121,8 @@ class Schedule: """Return the current maximum end time among all operations.""" max_end_time = 0 for graph_id, op_start_time in self._start_times.items(): - op = cast(Operation, self._sfg.find_by_id(graph_id)) - for outport in op.outputs: + operation = cast(Operation, self._sfg.find_by_id(graph_id)) + for outport in operation.outputs: max_end_time = max( max_end_time, op_start_time + cast(int, outport.latency_offset), @@ -149,8 +163,8 @@ class Schedule: ) -> Dict["OutputPort", Dict["Signal", int]]: ret = {} start_time = self._start_times[graph_id] - op = cast(Operation, self._sfg.find_by_id(graph_id)) - for output_port in op.outputs: + operation = cast(Operation, self._sfg.find_by_id(graph_id)) + for output_port in operation.outputs: output_slacks = {} available_time = start_time + cast(int, output_port.latency_offset) @@ -200,8 +214,8 @@ class Schedule: def _backward_slacks(self, graph_id: GraphID) -> Dict[InputPort, Dict[Signal, int]]: ret = {} start_time = self._start_times[graph_id] - op = cast(Operation, self._sfg.find_by_id(graph_id)) - for input_port in op.inputs: + operation = cast(Operation, self._sfg.find_by_id(graph_id)) + for input_port in operation.inputs: input_slacks = {} usage_time = start_time + cast(int, input_port.latency_offset) @@ -270,14 +284,19 @@ class Schedule: @property def sfg(self) -> SFG: - return self._sfg + """The SFG of the current schedule.""" + return self._original_sfg @property def start_times(self) -> Dict[GraphID, int]: + """The start times of the operations in the current schedule.""" return self._start_times @property def laps(self) -> Dict[GraphID, int]: + """ + The number of laps for the start times of the operations in the current schedule. + """ return self._laps @property @@ -317,8 +336,11 @@ class Schedule: ret = [self._schedule_time, *self._start_times.values()] # Loop over operations for graph_id in self._start_times: - op = cast(Operation, self._sfg.find_by_id(graph_id)) - ret += [cast(int, op.execution_time), *op.latency_offsets.values()] + operation = cast(Operation, self._sfg.find_by_id(graph_id)) + ret += [ + cast(int, operation.execution_time), + *operation.latency_offsets.values(), + ] # Remove not set values (None) ret = [v for v in ret if v is not None] return ret @@ -535,7 +557,16 @@ class Schedule: self._start_times[graph_id] = new_start return self + def _remove_delays_no_laps(self) -> None: + """Remove delay elements without updating laps. Used when loading schedule.""" + delay_list = self._sfg.find_by_type_name(Delay.type_name()) + while delay_list: + delay_op = cast(Delay, delay_list[0]) + self._sfg = cast(SFG, self._sfg.remove_operation(delay_op.graph_id)) + delay_list = self._sfg.find_by_type_name(Delay.type_name()) + def _remove_delays(self) -> None: + """Remove delay elements and update laps. Used after scheduling algorithm.""" delay_list = self._sfg.find_by_type_name(Delay.type_name()) while delay_list: delay_op = cast(Delay, delay_list[0]) @@ -549,35 +580,35 @@ class Schedule: def _schedule_asap(self) -> None: """Schedule the operations using as-soon-as-possible scheduling.""" - pl = self._sfg.get_precedence_list() + precedence_list = self._sfg.get_precedence_list() - if len(pl) < 2: + if len(precedence_list) < 2: print("Empty signal flow graph cannot be scheduled.") return non_schedulable_ops = set() - for outport in pl[0]: - op = outport.operation - if op.type_name() not in [Delay.type_name()]: - if op.graph_id not in self._start_times: + for outport in precedence_list[0]: + operation = outport.operation + if operation.type_name() not in [Delay.type_name()]: + if operation.graph_id not in self._start_times: # Set start time of all operations in the first iter to 0 - self._start_times[op.graph_id] = 0 + self._start_times[operation.graph_id] = 0 else: - non_schedulable_ops.add(op.graph_id) + non_schedulable_ops.add(operation.graph_id) - for outport in pl[1]: - op = outport.operation - if op.graph_id not in self._start_times: + for outport in precedence_list[1]: + operation = outport.operation + if operation.graph_id not in self._start_times: # Set start time of all operations in the first iter to 0 - self._start_times[op.graph_id] = 0 + self._start_times[operation.graph_id] = 0 - for outports in pl[2:]: + for outports in precedence_list[2:]: for outport in outports: - op = outport.operation - if op.graph_id not in self._start_times: + operation = outport.operation + if operation.graph_id not in self._start_times: # Schedule the operation if it does not have a start time yet. op_start_time = 0 - for inport in op.inputs: + for inport in operation.inputs: if len(inport.signals) != 1: raise ValueError( "Error in scheduling, dangling input port detected." @@ -617,7 +648,7 @@ class Schedule: 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 + self._start_times[operation.graph_id] = op_start_time for output in self._sfg.find_by_type_name(Output.type_name()): output = cast(Output, output) source_port = cast(OutputPort, output.inputs[0].signals[0].source) @@ -722,7 +753,7 @@ class Schedule: line_cache.append(start) elif end[0] == start[0]: - p = Path( + path = Path( [ start, [start[0] + SPLINE_OFFSET, start[1]], @@ -742,16 +773,16 @@ class Schedule: Path.CURVE4, ], ) - pp = PathPatch( - p, + path_patch = PathPatch( + path, fc='none', ec=_SIGNAL_COLOR, lw=SIGNAL_LINEWIDTH, zorder=10, ) - ax.add_patch(pp) + ax.add_patch(path_patch) else: - p = Path( + path = Path( [ start, [(start[0] + end[0]) / 2, start[1]], @@ -760,14 +791,14 @@ class Schedule: ], [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4], ) - pp = PathPatch( - p, + path_patch = PathPatch( + path, fc='none', ec=_SIGNAL_COLOR, lw=SIGNAL_LINEWIDTH, zorder=10, ) - ax.add_patch(pp) + ax.add_patch(path_patch) def _draw_offset_arrow(start, end, start_offset, end_offset, name="", laps=0): """Draw an arrow from *start* to *end*, but with an offset.""" @@ -784,12 +815,12 @@ class Schedule: ax.grid() for graph_id, op_start_time in self._start_times.items(): y_pos = self._get_y_position(graph_id, operation_gap=operation_gap) - op = cast(Operation, self._sfg.find_by_id(graph_id)) + operation = cast(Operation, self._sfg.find_by_id(graph_id)) # Rewrite to make better use of NumPy ( latency_coordinates, execution_time_coordinates, - ) = op.get_plot_coordinates() + ) = operation.get_plot_coordinates() _x, _y = zip(*latency_coordinates) x = np.array(_x) y = np.array(_y) @@ -809,11 +840,11 @@ class Schedule: yticklabels.append(cast(Operation, self._sfg.find_by_id(graph_id)).name) for graph_id, op_start_time in self._start_times.items(): - op = cast(Operation, self._sfg.find_by_id(graph_id)) - out_coordinates = op.get_output_coordinates() + operation = cast(Operation, self._sfg.find_by_id(graph_id)) + out_coordinates = operation.get_output_coordinates() source_y_pos = self._get_y_position(graph_id, operation_gap=operation_gap) - for output_port in op.outputs: + for output_port in operation.outputs: for output_signal in output_port.signals: destination = cast(InputPort, output_signal.destination) destination_op = destination.operation @@ -911,7 +942,7 @@ class Schedule: """ fig, ax = plt.subplots() self._plot_schedule(ax) - f = io.StringIO() - fig.savefig(f, format="svg") + buffer = io.StringIO() + fig.savefig(buffer, format="svg") - return f.getvalue() + return buffer.getvalue() diff --git a/b_asic/scheduler_gui/scheduler_item.py b/b_asic/scheduler_gui/scheduler_item.py index eceb35d2d4a190c752461f7dbc5adec683c0e64b..055857c6761b4328a5106d6ae6948aad6530849e 100644 --- a/b_asic/scheduler_gui/scheduler_item.py +++ b/b_asic/scheduler_gui/scheduler_item.py @@ -257,7 +257,7 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): # PySide2 / PyQt5 """Make a new graph out of the stored attributes.""" # build components for graph_id in self.schedule.start_times.keys(): - operation = cast(Operation, self.schedule.sfg.find_by_id(graph_id)) + operation = cast(Operation, self.schedule._sfg.find_by_id(graph_id)) component = OperationItem(operation, height=OPERATION_HEIGHT, parent=self) self._operation_items[graph_id] = component self._set_position(graph_id)