Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/ert/ensemble_evaluator/state.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from typing import Final

COLOR_FAILED: Final = (255, 200, 200)
COLOR_FINISHED: Final = (127, 201, 127)
COLOR_PENDING: Final = (190, 174, 212)
COLOR_RUNNING: Final = (255, 255, 153)
COLOR_UNKNOWN: Final = (128, 128, 128)
COLOR_WAITING: Final = (164, 200, 255)
COLOR_CANCELLED: Final = (235, 242, 246)
COLOR_QUEUED: Final = (183, 191, 199)
COLOR_FAILED: Final = (207, 34, 46)
COLOR_FINISHED: Final = (45, 164, 78)
COLOR_PENDING: Final = COLOR_QUEUED
COLOR_RUNNING: Final = (214, 178, 42)
COLOR_UNKNOWN: Final = (140, 149, 159)
COLOR_WAITING: Final = COLOR_QUEUED
COLOR_CANCELLED: Final = (234, 238, 242)
COLOR_WARNING: Final = (255, 103, 0)

ENSEMBLE_STATE_CANCELLED: Final = "Cancelled"
Expand Down
115 changes: 67 additions & 48 deletions src/ert/gui/experiments/view/progress_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,79 +11,98 @@
QVBoxLayout,
)

from ert.ensemble_evaluator.state import ENSEMBLE_STATE_FAILED, REAL_STATE_TO_COLOR
from ert.ensemble_evaluator.state import (
ENSEMBLE_STATE_FAILED,
REAL_STATE_TO_COLOR,
REALIZATION_STATE_WAITING,
)


class ProgressWidget(QFrame):
def __init__(self) -> None:
super().__init__()
self.setFixedHeight(70)

self._vertical_layout = QVBoxLayout(self)
self._vertical_layout.setContentsMargins(0, 0, 0, 0)
self._vertical_layout.setSpacing(0)
self.setLayout(self._vertical_layout)
self._main_layout = QVBoxLayout(self)
self._main_layout.setContentsMargins(0, 0, 0, 0)
self._main_layout.setSpacing(2)
self.setLayout(self._main_layout)

self._waiting_progress_bar = QProgressBar(self)
self._waiting_progress_bar.setRange(0, 0)
self._waiting_progress_bar.setFixedHeight(30)
self._vertical_layout.addWidget(self._waiting_progress_bar)
self._main_layout.addWidget(self._waiting_progress_bar)

self._progress_frame = QFrame(self)
self._vertical_layout.addWidget(self._progress_frame)
self._main_layout.addWidget(self._progress_frame)

self._horizontal_layout = QHBoxLayout(self._progress_frame)
self._horizontal_layout.setContentsMargins(0, 0, 0, 0)
self._horizontal_layout.setSpacing(0)
self._progress_frame.setLayout(self._horizontal_layout)

self._legend_frame = QFrame(self)
self._vertical_layout.addWidget(self._legend_frame)
self._legend_frame.setFixedHeight(30)
self._horizontal_legend_layout = QHBoxLayout(self._legend_frame)
self._horizontal_legend_layout.setContentsMargins(0, 0, 0, 0)
self._horizontal_legend_layout.setSpacing(0)
self._main_layout.addWidget(self._legend_frame)
self._legend_frame.setFixedHeight(24)
self._legend_layout = QHBoxLayout(self._legend_frame)
self._legend_layout.setContentsMargins(0, 0, 0, 0)
self._legend_layout.setSpacing(6)

self._status: dict[str, int] = {}
self._status_counts: dict[str, int] = {}
self._realization_count = 0
self._progress_label_map: dict[str, QLabel] = {}
self._legend_map_text = {}

for state, color in REAL_STATE_TO_COLOR.items():
label = QLabel(self)
label.setVisible(False)
label.setObjectName(f"progress_{state}")
label.setStyleSheet(f"background-color : {QColor(*color).name()}")
self._progress_label_map[state] = label
self._horizontal_layout.addWidget(label)

label = QLabel(self)
label.setFixedSize(20, 20)
label.setStyleSheet(
f"background-color : {QColor(*color).name()}; border: 1px solid black;"
)
self._horizontal_legend_layout.addWidget(label)

label = QLabel(self)
label.setObjectName(f"progress_label_text_{state}")
label.setText(f" {state} ({0}/{0})")
self._legend_map_text[state] = label
self._horizontal_legend_layout.addWidget(label)
self._progress_segments: dict[str, QLabel] = {}
self._legend_labels = {}
for realization_status, status_color_rgb in REAL_STATE_TO_COLOR.items():
status_color_hex = QColor(*status_color_rgb).name()

progress_segment = QLabel(self)
progress_segment.setVisible(False)
progress_segment.setObjectName(f"progress_{realization_status}")
progress_segment.setStyleSheet(f"background-color : {status_color_hex}")
self._progress_segments[realization_status] = progress_segment
self._horizontal_layout.addWidget(progress_segment)

if realization_status == REALIZATION_STATE_WAITING:
marker_style = (
"background-color: transparent;"
"border-radius: 7px;"
f"border: 2px solid {status_color_hex};"
)
else:
marker_style = (
f"background-color: {status_color_hex};border-radius: 7px;"
)

legend_marker = QLabel(self)
legend_marker.setFixedSize(14, 14)
legend_marker.setStyleSheet(marker_style)
self._legend_layout.addWidget(legend_marker)

legend_label = QLabel(self)
legend_label.setObjectName(f"progress_label_text_{realization_status}")
legend_label.setText(f"{realization_status} ({0}/{0})")
self._legend_labels[realization_status] = legend_label
self._legend_layout.addWidget(legend_label)
self._legend_layout.addSpacing(16)

self._legend_layout.addStretch()

def repaint_components(self) -> None:
if self._realization_count > 0:
full_width = self.width()
self.stop_waiting_progress_bar()

for state, label in self._progress_label_map.items():
label.setVisible(True)
count = self._status.get(state, 0)
for realization_status, progress_segment in self._progress_segments.items():
progress_segment.setVisible(True)
count = self._status_counts.get(realization_status, 0)
width = int((count / self._realization_count) * full_width)
label.setFixedWidth(width)
progress_segment.setFixedWidth(width)

for state, label in self._legend_map_text.items():
label.setText(
f" {state} ({self._status.get(state, 0)}/{self._realization_count})"
for realization_status, legend_label in self._legend_labels.items():
legend_label.setText(
f"{realization_status} "
f"({self._status_counts.get(realization_status, 0)}/"
f"{self._realization_count})"
)

def stop_waiting_progress_bar(self) -> None:
Expand All @@ -95,13 +114,13 @@ def start_waiting_progress_bar(self) -> None:
def set_all_failed(self) -> None:
self.stop_waiting_progress_bar()
full_width = self.width()
for state, label in self._progress_label_map.items():
label.setVisible(True)
width = full_width if state == ENSEMBLE_STATE_FAILED else 0
label.setFixedWidth(width)
for realization_status, progress_segment in self._progress_segments.items():
progress_segment.setVisible(True)
width = full_width if realization_status == ENSEMBLE_STATE_FAILED else 0
progress_segment.setFixedWidth(width)

def update_progress(self, status: dict[str, int], realization_count: int) -> None:
self._status = status
self._status_counts = status
self._realization_count = realization_count
if status.get("Finished", 0) < self._realization_count:
self.start_waiting_progress_bar()
Expand Down
113 changes: 68 additions & 45 deletions src/ert/gui/experiments/view/realization.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
QItemSelectionModel,
QModelIndex,
QObject,
QPoint,
QRect,
QSize,
Qt,
)
Expand All @@ -23,12 +23,17 @@
QWidget,
)

from ert.ensemble_evaluator.state import (
REALIZATION_STATE_RUNNING,
REALIZATION_STATE_WAITING,
)
from ert.gui.model.real_list import RealListModel
from ert.gui.model.snapshot import (
CallbackStatusMessageRole,
FMStepColorHint,
MemoryUsageRole,
RealIens,
StatusRole,
)
from ert.shared.status.utils import byte_with_unit

Expand All @@ -40,7 +45,7 @@ def __init__(self, it: int, parent: QWidget | None = None) -> None:
super().__init__(parent)

self._iter = it
self._delegate_size = QSize(90, 90)
self._delegate_size = QSize(70, 70)

self._real_view = QListView(self)
self._real_view.setViewMode(QListView.ViewMode.IconMode)
Expand Down Expand Up @@ -95,80 +100,96 @@ def refresh_current_selection(self) -> None:


class RealizationDelegate(QStyledItemDelegate):
def __init__(self, size: QSize, parent: QObject) -> None:
_STATUS_DOT_DIAMETER = 32
_PROGRESS_RING_MARGIN = 4
_PROGRESS_RING_WIDTH = 5

def __init__(self, item_size: QSize, parent: QObject) -> None:
super().__init__(parent)
self._size = size
self._item_size = item_size
parent.installEventFilter(self)
self.adjustment_point_for_job_rect_margin = QPoint(-20, -20)
self._color_black = QColor(0, 0, 0, 180)
self._color_progress = QColor(50, 173, 230, 200)
self._color_lightgray = QColor("LightGray").lighter(120)
self._pen_black = QPen(self._color_black, 2, Qt.PenStyle.SolidLine)
self._progress_track_color = QColor(0, 0, 0, 35)
self._status_dot_outline_pen = QPen(
QColor(0, 0, 0, 45), 1, Qt.PenStyle.SolidLine
)

@override
def paint(
self, painter: QPainter | None, option: QStyleOptionViewItem, index: QModelIndex
) -> None:
if painter is None:
return
text = index.data(RealIens)
selected_color, finished_count, total_count = tuple(index.data(FMStepColorHint))
realization_label = index.data(RealIens)
realization_status = index.data(StatusRole)
realization_status_color, completed_step_count, total_step_count = tuple(
index.data(FMStepColorHint)
)

painter.save()
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)

percentage_done = (
100 if total_count < 1 else int((finished_count * 100.0) / total_count)
)

painter.setPen(self._pen_black)
adjusted_rect = option.rect.adjusted(2, 2, -2, -2)

painter.setBrush(
self._color_progress if percentage_done == 100 else self._color_lightgray
if option.state & QStyle.StateFlag.State_Selected:
selection_color = QColor(option.palette.color(QPalette.ColorRole.Highlight))
selection_color.setAlpha(60)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(selection_color)
painter.drawRoundedRect(option.rect.adjusted(2, 2, -2, -2), 6, 6)

status_dot_rect = QRect(
option.rect.center().x() - self._STATUS_DOT_DIAMETER // 2,
option.rect.top() + 8,
self._STATUS_DOT_DIAMETER,
self._STATUS_DOT_DIAMETER,
)
painter.drawEllipse(adjusted_rect)

if 0 < percentage_done < 100:
painter.setBrush(self._color_progress)
painter.drawPie(adjusted_rect, 1440, -int(percentage_done * 57.6))

if option.state & QStyle.StateFlag.State_Selected:
factor: int = (
125
if selected_color.lighter(125).getRgb() != (255, 255, 255, 255)
else 110
if realization_status == REALIZATION_STATE_RUNNING and total_step_count > 0:
progress_ring_rect = status_dot_rect.adjusted(
-self._PROGRESS_RING_MARGIN,
-self._PROGRESS_RING_MARGIN,
self._PROGRESS_RING_MARGIN,
self._PROGRESS_RING_MARGIN,
)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.setPen(QPen(self._progress_track_color, self._PROGRESS_RING_WIDTH))
painter.drawEllipse(progress_ring_rect)
progress_arc_pen = QPen(realization_status_color, self._PROGRESS_RING_WIDTH)
progress_arc_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
painter.setPen(progress_arc_pen)
painter.drawArc(
progress_ring_rect,
90 * 16,
-int(360 * 16 * completed_step_count / total_step_count),
)
selected_color = selected_color.lighter(factor)

painter.setBrush(selected_color)
adjusted_rect = option.rect.adjusted(7, 7, -7, -7)
painter.drawEllipse(adjusted_rect)

font = painter.font()
font.setBold(True)
painter.setFont(font)
painter.setPen(self._status_dot_outline_pen)
if realization_status == REALIZATION_STATE_WAITING:
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.setPen(QPen(realization_status_color, 2))
painter.drawEllipse(status_dot_rect.adjusted(1, 1, -1, -1))
else:
painter.setBrush(realization_status_color)
painter.drawEllipse(status_dot_rect)

adj_rect = option.rect.adjusted(0, 20, 0, 0)
painter.drawText(adj_rect, Qt.AlignmentFlag.AlignHCenter, text)
adj_rect = option.rect.adjusted(0, 45, 0, 0)
painter.setPen(option.palette.color(QPalette.ColorRole.Text))
painter.drawText(
adj_rect, Qt.AlignmentFlag.AlignHCenter, f"{finished_count} / {total_count}"
option.rect.adjusted(0, self._STATUS_DOT_DIAMETER + 14, 0, 0),
Qt.AlignmentFlag.AlignHCenter,
realization_label,
)

painter.restore()

@override
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
return self._size
return self._item_size

@override
def eventFilter(self, object: QObject | None, event: QEvent | None) -> bool:
if isinstance(event, QHelpEvent) and event.type() == QEvent.Type.ToolTip:
mouse_pos = event.pos() + self.adjustment_point_for_job_rect_margin
parent: RealizationWidget = cast(RealizationWidget, self.parent())
view = parent._real_view
mouse_pos = view.viewport().mapFrom(parent, event.pos())
index = view.indexAt(mouse_pos)
if index.isValid():
tooltip_text = ""
Expand All @@ -182,7 +203,9 @@ def eventFilter(self, object: QObject | None, event: QEvent | None) -> bool:
tooltip_text += callback_error_msg
if tooltip_text:
parent.triggeredTooltipTextDisplay.emit(tooltip_text)
QToolTip.showText(view.mapToGlobal(mouse_pos), tooltip_text)
QToolTip.showText(
view.viewport().mapToGlobal(mouse_pos), tooltip_text
)
return True

return super().eventFilter(object, event)
2 changes: 1 addition & 1 deletion tests/ert/ui_tests/gui/test_missing_runpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def inner():
)
assert not run_dialog._progress_widget._waiting_progress_bar.isVisible()
assert (
run_dialog._progress_widget._progress_label_map[
run_dialog._progress_widget._progress_segments[
ENSEMBLE_STATE_FAILED
].width()
== run_dialog._progress_widget.width()
Expand Down
2 changes: 1 addition & 1 deletion tests/ert/unit_tests/gui/experiments/view/test_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_marker_label_text_correct(qtbot, status: dict[str, int]):

assert label_marker
count = status.get(state, 0)
assert f" {state} ({count}/{realization_count})" in label_marker.text()
assert f"{state} ({count}/{realization_count})" in label_marker.text()


@given(
Expand Down