Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
scheduler_item.py 9.81 KiB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
B-ASIC Scheduler-gui Graphics Graph Item Module.

Contains the scheduler-gui SchedulerItem class for drawing and
maintain a component in a graph.
"""
from collections import defaultdict
from math import floor
from pprint import pprint
from typing import Dict, List, Optional, Set, cast

# QGraphics and QPainter imports
from qtpy.QtWidgets import QGraphicsItem, QGraphicsItemGroup

# B-ASIC
from b_asic.operation import Operation
from b_asic.port import InputPort
from b_asic.schedule import Schedule
from b_asic.scheduler_gui._preferences import (
    OPERATION_GAP,
    OPERATION_HEIGHT,
    SCHEDULE_INDENT,
)
from b_asic.scheduler_gui.axes_item import AxesItem
from b_asic.scheduler_gui.operation_item import OperationItem
from b_asic.scheduler_gui.scheduler_event import SchedulerEvent
from b_asic.scheduler_gui.signal_item import SignalItem


class SchedulerItem(SchedulerEvent, QGraphicsItemGroup):  # PySide2 / PyQt5
    """
    A class to represent a graph in a QGraphicsScene. This class is a
    subclass of QGraphicsItemGroup and contains the objects, axes from
    AxesItem, as well as components from OperationItem. It
    also inherits from SchedulerEvent, which acts as a filter for events
    to OperationItem objects.

    Parameters
    ==========

    schedule : Schedule
        The Schedule to draw.

    parent : QGraphicsItem, optional
        The parent. Passed to the constructor of QGraphicsItemGroup
    """

    _axes: Optional[AxesItem]
    _operation_items: Dict[str, OperationItem]
    _x_axis_indent: float
    _event_items: List[QGraphicsItem]
    _signal_dict: Dict[OperationItem, Set[SignalItem]]

    def __init__(
        self, schedule: Schedule, parent: Optional[QGraphicsItem] = None
    ):
        """
        Construct a SchedulerItem. *parent* is passed to QGraphicsItemGroup's constructor.
        """
        # QGraphicsItemGroup.__init__(self, self)
        # SchedulerEvent.__init__(self)
        super().__init__(parent=parent)
        # if isinstance(parent, QGraphicsItem):
        #     super().__init__(parent=parent)
        # else:
        #     super().__init__(parent=self)
        self._schedule = schedule
        self._axes = None
        self._operation_items = {}
        self._x_axis_indent = SCHEDULE_INDENT
        self._event_items = []
        self._signal_dict = defaultdict(set)
        self._make_graph()

    def clear(self) -> None:
        """
        Set all children's parent to 'None' and delete the children objects.
        """
        self._event_items = []
        for item in self.childItems():
            item.setParentItem(None)
            del item

    def is_component_valid_pos(self, item: OperationItem, pos: float) -> bool:
        """
        Take in a component position and return True if the component's new
        position is valid, False otherwise.

        Parameters
        ==========

        item : OperationItem
            The component.

        pos : float
            The x-position to check.
        """
        # TODO: implement
        if self.schedule is None:
            raise ValueError("No schedule installed.")
        end_time = item.end_time
        new_start_time = floor(pos) - floor(self._x_axis_indent)
        slacks = self.schedule.slacks(item.graph_id)
        op_start_time = self.schedule.start_time_of_operation(item.graph_id)
        if not -slacks[0] <= new_start_time - op_start_time <= slacks[1]:
            # Cannot move due to dependencies
            return False
        if self.schedule.cyclic:
            if new_start_time < -1:
                # Moving one position before left edge => wrap
                return False
            if new_start_time > self.schedule.schedule_time + 1:
                # Moving one position after schedule_time => wrap
                return False
        else:
            if pos < 0:
                return False
            if new_start_time + end_time > self.schedule.schedule_time:
                return False

        return True

    def _redraw_all_lines(self) -> None:
        """Redraw all lines in schedule."""
        s = set()
        for signals in self._signal_dict.values():
            s.update(signals)
        for signal in s:
            signal.update_path()

    def _redraw_lines(self, item: OperationItem) -> None:
        """Update lines connected to *item*."""
        for signal in self._signal_dict[item]:
            signal.update_path()

    def set_item_active(self, item: OperationItem) -> None:
        """
        Set an item as active, i.e., draw it and connecting signals in special colors.

        Parameters
        ----------
        item : OperationItem
            The item to set as active.

        """
        item.set_active()
        for signal in self._signal_dict[item]:
            signal.set_active()

    def set_item_inactive(self, item: OperationItem) -> None:
        """
        Set an item as inactive, i.e., draw it and connecting signals in standard colors.

        Parameters
        ----------
        item : OperationItem
            The item to set as active.

        """
        item.set_inactive()
        for signal in self._signal_dict[item]:
            signal.set_inactive()

    def set_new_start_time(self, item: OperationItem) -> None:
        """Set new start time for *item*."""
        pos = item.x()
        op_start_time = self.schedule.start_time_of_operation(item.graph_id)
        new_start_time = floor(pos) - floor(self._x_axis_indent)
        move_time = new_start_time - op_start_time
        if move_time:
            self.schedule.move_operation(item.graph_id, move_time)

    def is_valid_delta_time(self, delta_time: int) -> bool:
        """
        Takes in a delta time and returns True if the schedule time can be changed by *delta_time*. False otherwise.
        """
        # TODO: implement
        # item = self.scene().mouseGrabberItem()
        if self.schedule is None:
            raise ValueError("No schedule installed.")
        return (
            self.schedule.schedule_time + delta_time
            >= self.schedule.get_max_end_time()
        )

    def set_schedule_time(self, delta_time: int) -> None:
        """Change the schedule time by *delta_time* and redraw the graph."""
        if self._axes is None:
            raise RuntimeError("No AxesItem!")
        if self.schedule is None:
            raise ValueError("No schedule installed.")
        self.schedule.set_schedule_time(
            self.schedule.schedule_time + delta_time
        )
        self._axes.set_width(self._axes.width + delta_time)
        # Redraw all lines
        self._redraw_all_lines()

    @property
    def schedule(self) -> Schedule:
        """The schedule."""
        return self._schedule

    @property
    def axes(self) -> Optional[AxesItem]:
        return self._axes

    @property
    def components(self) -> List[OperationItem]:
        return list(component for component in self._operation_items.values())

    @property
    def event_items(self) -> List[QGraphicsItem]:
        """Return a list of objects that receives events."""
        return self._event_items

    def _set_position(self, graph_id) -> None:
        op_item = self._operation_items[graph_id]
        op_item.setPos(
            self._x_axis_indent + self.schedule.start_times[graph_id],
            self.schedule._get_y_position(
                graph_id, OPERATION_HEIGHT, OPERATION_GAP
            ),
        )

    def _redraw_from_start(self) -> None:
        self.schedule._reset_y_locations()
        for graph_id in {
            k: v
            for k, v in sorted(
                self.schedule.start_times.items(), key=lambda item: item[1]
            )
        }:
            self._set_position(graph_id)
        self._redraw_all_lines()

    def _update_axes(self, build=False) -> None:
        # build axes
        schedule_time = self.schedule.schedule_time
        max_pos_graph_id = max(
            self.schedule._y_locations, key=self.schedule._y_locations.get
        )
        y_pos_min = self.schedule._get_y_position(
            max_pos_graph_id, OPERATION_HEIGHT, OPERATION_GAP
        )

        if self._axes is None or build:
            self._axes = AxesItem(schedule_time, y_pos_min + 0.5)
            self._event_items += self._axes.event_items
        else:
            self._axes.set_height(y_pos_min + 0.5)
        self._axes.setPos(0, y_pos_min + OPERATION_HEIGHT + OPERATION_GAP)

    def _make_graph(self) -> None:
        """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))
            component = OperationItem(
                operation, height=OPERATION_HEIGHT, parent=self
            )
            self._operation_items[graph_id] = component
            self._set_position(graph_id)
            self._event_items += component.event_items

        # self._axes.width = schedule_time

        # add axes and components
        self._update_axes(build=True)
        self.addToGroup(self._axes)
        for component in self.components:
            self.addToGroup(component)

        # add signals
        for component in self.components:
            for output_port in component.operation.outputs:
                for signal in output_port.signals:
                    destination = cast(InputPort, signal.destination)
                    destination_component = self._operation_items[
                        destination.operation.graph_id
                    ]
                    gui_signal = SignalItem(
                        component, destination_component, signal, parent=self
                    )
                    self.addToGroup(gui_signal)
                    self._signal_dict[component].add(gui_signal)
                    self._signal_dict[destination_component].add(gui_signal)


pprint(SchedulerItem.__mro__)