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
83 changes: 79 additions & 4 deletions src/badger/core_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
MEASUREMENT_ACTION_TYPE,
MEASUREMENT_ACTION_RETRY,
MEASUREMENT_ACTION_ABORT,
TERMINATION_REACHED_TYPE,
TERMINATION_ACTION_TYPE,
TERMINATION_ACTION_CONTINUE,
TERMINATION_ACTION_END,
)
from badger.logger import _get_default_logger
from badger.logger.event import Events
Expand Down Expand Up @@ -89,6 +93,47 @@ def evaluate_measurement_with_retry(
)


def pause_for_termination_dialog_action(
queue: mp.Queue,
stop_process: mp.Event,
pause_process: mp.Event,
dialog_action_queue: mp.Queue,
tc_condition: dict,
) -> None:
"""Pause the run and wait for user action when run-until condition is reached."""
queue.put(
{
"type": TERMINATION_REACHED_TYPE,
"tc_condition": tc_condition,
}
)

while True:
if stop_process.is_set():
raise BadgerRunTerminated

try:
msg = dialog_action_queue.get(
timeout=0.1
) # short timeout here, so we can make checks for stop_process
except Empty:
continue

if (
isinstance(msg, dict)
and msg.get("type") == TERMINATION_ACTION_TYPE
and msg.get("action")
in [TERMINATION_ACTION_CONTINUE, TERMINATION_ACTION_END]
):
if msg["action"] == TERMINATION_ACTION_CONTINUE:
pause_process.set()
return

raise BadgerRunTerminated(
"Run terminated after termination condition reached"
)


def convert_to_solution(result: DataFrame, routine: Routine):
"""
This method is passed the latest evaluated solution and converts that to a printable format for the terminal.
Expand Down Expand Up @@ -323,16 +368,46 @@ def run_routine_subprocess(

if count >= max_eval:
logger.info(
"Max evaluations reached. Terminating optimization."
"Max evaluations reached. Pausing optimization and waiting for user action."
)
raise BadgerRunTerminated
pause_process.clear()
pause_for_termination_dialog_action(
queue=queue,
stop_process=stop_process,
pause_process=pause_process,
dialog_action_queue=dialog_action_queue,
tc_condition={
"type": "max_eval",
"config": max_eval,
"state": count,
},
)
# reset termination condition
termination_condition = None
continue
elif idx == 1:
max_time = tc_config["max_time"]
dt = time.time() - start_time
logger.debug(f"Checking max_time termination: {dt} >= {max_time}")
if dt >= max_time:
logger.info("Max time reached. Terminating optimization.")
raise BadgerRunTerminated
logger.info(
"Max time reached. Pausing optimization and waiting for user action."
)
pause_process.clear()
pause_for_termination_dialog_action(
queue=queue,
stop_process=stop_process,
pause_process=pause_process,
dialog_action_queue=dialog_action_queue,
tc_condition={
"type": "max_time",
"config": max_time,
"state": dt,
},
)
# reset termination condition
termination_condition = None
continue

candidates = routine.generator.generate(1)[0]
logger.debug(f"Generated candidates: {candidates}")
Expand Down
7 changes: 7 additions & 0 deletions src/badger/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,10 @@ def __init__(self, message="Optimization run has been terminated!"):
MEASUREMENT_ACTION_TYPE = "measurement_action"
MEASUREMENT_ACTION_RETRY = "retry"
MEASUREMENT_ACTION_ABORT = "abort"

# Constants for run-until termination dialog feature.
# Used in communication between routine runner and subprocess.
TERMINATION_REACHED_TYPE = "termination_reached"
TERMINATION_ACTION_TYPE = "termination_action"
TERMINATION_ACTION_CONTINUE = "continue"
TERMINATION_ACTION_END = "end"
93 changes: 83 additions & 10 deletions src/badger/gui/components/action_bar.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
"""Toolbar with run-control buttons (start, pause, stop), logbook submission,
docs access, and the extensions palette launcher."""

from PyQt5.QtWidgets import QWidget, QHBoxLayout
from PyQt5.QtWidgets import QStyle, QStyleOptionToolButton, QWidget, QHBoxLayout
from PyQt5.QtWidgets import QToolButton, QMenu, QAction
from PyQt5.QtGui import QIcon, QFont
from PyQt5.QtCore import pyqtSignal, QSize
from PyQt5.QtCore import QEvent, pyqtSignal, QSize
from importlib import resources
from badger.gui.utils import create_button
from badger.gui.windows.docs_window import BadgerDocsWindow


class SplitTooltipToolButton(QToolButton):
"""
QToolButton that shows a separate tooltip over the dropdown-arrow area.
Use arg menu_tooltip="desired tooltip" to set the menu tooltip
"""

def __init__(self, menu_tooltip="", parent=None):
"""
Parameters
----------
menu_tooltip (str)
tooltip for menu
"""
super().__init__(parent)
self.menu_tooltip = menu_tooltip

def _over_menu_arrow(self, pos):
opt = QStyleOptionToolButton()
self.initStyleOption(opt)
rect = self.style().subControlRect(
QStyle.CC_ToolButton, opt, QStyle.SC_ToolButtonMenu, self
)
return rect.contains(pos)

def event(self, event):
if event.type() == QEvent.ToolTip and self._over_menu_arrow(event.pos()):
from PyQt5.QtWidgets import QToolTip

QToolTip.showText(event.globalPos(), self.menu_tooltip, self)
return True
return super().event(event)


stylesheet_del = """
QPushButton:hover:pressed
{
Expand Down Expand Up @@ -89,7 +123,9 @@

class BadgerActionBar(QWidget):
sig_start = pyqtSignal()
sig_start_until = pyqtSignal()
sig_start_until = pyqtSignal(
bool
) # bool True launches termination condition dialog menu
sig_stop = pyqtSignal()

sig_delete_run = pyqtSignal()
Expand Down Expand Up @@ -160,7 +196,9 @@ def load_internal_icon(name: str) -> QIcon:
self.btn_ctrl.setDisabled(True)

# self.btn_stop = btn_stop = QPushButton('Run')
self.btn_stop = QToolButton()
self.btn_stop = SplitTooltipToolButton(
menu_tooltip="Update Termination Condition"
)
self.btn_stop.setFixedSize(96, 32)
self.btn_stop.setFont(cool_font)
self.btn_stop.setStyleSheet(stylesheet_run)
Expand Down Expand Up @@ -198,15 +236,21 @@ def load_internal_icon(name: str) -> QIcon:
run_action.setIcon(self.icon_play)
self.run_until_action = run_until_action = QAction("Run until", self)
run_until_action.setIcon(self.icon_play)
self.run_until_menu_action = run_until_menu_action = QAction("Run until", self)
run_until_menu_action.setIcon(self.icon_play)
menu.addAction(run_action)
menu.addAction(run_until_action)
menu.addAction(run_until_menu_action)
# Note: run_until_menu_action is triggered by selecting "run until" from the menu
# It emits sig_start_until(True) to launch the BadgerTerminationConditionDialog
# and sets the default run action to run_until_action. Pressing the play/stop button
# will then emit sig_start_until(False) and skip the dialog popup.

# Set the menu as the run button's dropdown menu
self.btn_stop.setMenu(menu)
self.btn_stop.setDefaultAction(run_action)
self.btn_stop.setPopupMode(QToolButton.MenuButtonPopup)
self.btn_stop.setDisabled(False)
# btn_stop.setToolTip('')
run_action.setToolTip("Run")

# Config button
self.btn_config = btn_config = create_button("tools.png", "Configure run")
Expand Down Expand Up @@ -244,8 +288,11 @@ def config_logic(self):
self.btn_opt.clicked.connect(self.jump_to_optimal)
self.btn_set.clicked.connect(self.dial_in)
self.btn_ctrl.clicked.connect(self.ctrl_routine)
self.run_action.triggered.connect(self.set_run_action)
self.run_until_action.triggered.connect(self.set_run_until_action)
self.run_action.triggered.connect(self._on_run_action_triggered)
self.run_until_action.triggered.connect(self._on_run_until_action_triggered)
self.run_until_menu_action.triggered.connect(
self._on_run_until_menu_action_triggered
)
self.save_checkpoint_action.triggered.connect(
lambda: self.sig_save_checkpoint.emit()
)
Expand Down Expand Up @@ -293,6 +340,8 @@ def routine_finished(self):
self.run_action.setIcon(self.icon_play)
self.run_until_action.setText("Run until")
self.run_until_action.setIcon(self.icon_play)
self.run_until_menu_action.setText("Run until")
self.run_until_menu_action.setIcon(self.icon_play)
# self.btn_stop.setToolTip('')
self.btn_stop.setDisabled(False)

Expand Down Expand Up @@ -320,6 +369,8 @@ def run_start(self):
self.run_action.setIcon(self.icon_stop)
self.run_until_action.setText("Stop")
self.run_until_action.setIcon(self.icon_stop)
self.run_until_menu_action.setText("Stop")
self.run_until_menu_action.setIcon(self.icon_stop)
self.btn_checkpoint.setDisabled(False)
self.btn_ctrl.setDisabled(False)
self.btn_set.setDisabled(True)
Expand All @@ -335,16 +386,25 @@ def set_run_action(self):
self.btn_stop.setDisabled(True)
self.sig_stop.emit()

def set_run_until_action(self):
def set_run_until_action(self, from_menu=False):
if self.btn_stop.defaultAction() is not self.run_until_action:
self.btn_stop.setDefaultAction(self.run_until_action)

if self.run_until_action.text() == "Run until":
self.sig_start_until.emit()
self.sig_start_until.emit(from_menu)
else:
self.btn_stop.setDisabled(True)
self.sig_stop.emit()

def _on_run_action_triggered(self):
self.set_run_action()

def _on_run_until_action_triggered(self):
self.set_run_until_action(from_menu=False)

def _on_run_until_menu_action_triggered(self):
self.set_run_until_action(from_menu=True)

def delete_run(self):
self.sig_delete_run.emit()

Expand Down Expand Up @@ -382,3 +442,16 @@ def open_extensions_palette(self):
def env_ready(self):
self.btn_log.setDisabled(False)
self.btn_opt.setDisabled(False)

def update_run_tooltip(self, tc=None):
"""Update btn_stop tooltip: tc dict for run-until mode, or None."""
if tc is None:
self.run_action.setToolTip("Run")
else:
tc_idx = tc.get("tc_idx", 0)
if tc_idx == 0:
tip = f"Run until: n iterations = {tc.get('max_eval')}"
elif tc_idx == 1:
tip = f"Run until: timeout = {tc.get('max_time')}s"
self.run_until_action.setToolTip(tip)
self.run_until_menu_action.setToolTip(tip)
48 changes: 48 additions & 0 deletions src/badger/gui/components/routine_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@
MEASUREMENT_ACTION_TYPE,
MEASUREMENT_ACTION_RETRY,
MEASUREMENT_ACTION_ABORT,
TERMINATION_REACHED_TYPE,
TERMINATION_ACTION_TYPE,
TERMINATION_ACTION_CONTINUE,
TERMINATION_ACTION_END,
)
from badger.tests.utils import get_current_vars
from badger.routine import calculate_variable_bounds, calculate_initial_points
from badger.settings import init_settings
from badger.gui.components.process_manager import ProcessManager
from badger.gui.windows.measurement_retry_dialog import BadgerMeasurementRetryDialog
from badger.gui.windows.termination_reached_dialog import (
BadgerTerminationReachedDialog,
)
from badger.routine import Routine

logger = logging.getLogger(__name__)
Expand All @@ -40,6 +47,7 @@ class BadgerRoutineSignals(QObject):
error = pyqtSignal(Exception)
info = pyqtSignal(str)
states = pyqtSignal(str)
sig_status = pyqtSignal(str) # status message information


class BadgerRoutineSubprocess:
Expand Down Expand Up @@ -261,6 +269,17 @@ def check_queue(self) -> None:
"action": action,
}
)
elif (
isinstance(msg, dict)
and msg.get("type") == TERMINATION_REACHED_TYPE
):
action = self.handle_termination_reached(msg)
self.dialog_action_queue.put(
{
"type": TERMINATION_ACTION_TYPE,
"action": action,
}
)
else:
error_title, error_traceback = msg
BadgerError(error_title, error_traceback)
Expand All @@ -281,6 +300,33 @@ def handle_measurement_error(self, msg: dict) -> str:
return MEASUREMENT_ACTION_RETRY
return MEASUREMENT_ACTION_ABORT

def handle_termination_reached(self, msg: dict) -> str:
# update status
tc_condition = msg.get("tc_condition")
status_str = self._format_tc_status_str(tc_condition)
self.signals.sig_status.emit(status_str)

# launch dialog
dialog = BadgerTerminationReachedDialog(
tc_condition=tc_condition,
text=msg.get("title"),
)
result = dialog.exec_()
if result == QDialog.Accepted:
self.signals.sig_status.emit(f"Running routine {self.routine.name}...")
return TERMINATION_ACTION_CONTINUE
return TERMINATION_ACTION_END

def _format_tc_status_str(self, tc_condition: dict) -> str:
tc_type = tc_condition["type"]
if tc_type == "max_eval":
tc_type_text = "N iterations"
state = tc_condition["state"]
else:
tc_type_text = "timeout"
state = f"{tc_condition['state']:.2f} s"
return f"Routine {self.routine.name} paused: Condition {tc_type_text} = {state} reached"

def after_evaluate(self, results: pd.DataFrame) -> None:
logger.debug("Received evaluation results from subprocess.")
"""
Expand Down Expand Up @@ -329,8 +375,10 @@ def ctrl_routine(self, pause: bool) -> None:
pause : bool
"""
if pause:
self.signals.sig_status.emit(f"Routine {self.routine.name} paused")
self.pause_event.clear()
else:
self.signals.sig_status.emit(f"Running routine {self.routine.name}...")
self.pause_event.set()

def close(self) -> None:
Expand Down
1 change: 1 addition & 0 deletions src/badger/gui/components/run_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ def init_routine_runner(self):
routine_runner.signals.error.connect(self.on_error)
routine_runner.signals.info.connect(self.on_info)
routine_runner.signals.states.connect(self.states)
routine_runner.signals.sig_status.connect(self.sig_status.emit)

self.sig_pause.connect(routine_runner.ctrl_routine)
self.sig_stop.connect(routine_runner.stop_routine)
Expand Down
Loading
Loading