Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
61a06af
set run_until as default behavior for mini
michaellans Aug 6, 2026
9a9cfce
add termination_reached_dialog
michaellans Aug 11, 2026
6d5b174
reaching termination condition in optimization loop pauses and opens …
michaellans Aug 11, 2026
9434db8
Add logic to skip tc dialog from stop button press, show if selected …
michaellans Aug 12, 2026
cc1d5e4
Update tooltips for stop/run button to indicate termination condition
michaellans Aug 13, 2026
7cc2ced
update status when routine paused, including termination_condition
michaellans Aug 13, 2026
82692db
block signals on history_tree update after run
michaellans Aug 14, 2026
fa8b1f2
import xopt.generators during intial load (bayesian and sequential)
michaellans Aug 17, 2026
7a5c645
add noqa F401
michaellans Aug 17, 2026
1ef6078
Add resume option to mini gui and logic to load displayed data into n…
michaellans Aug 17, 2026
5214559
add SIGTERM handler to raise BadgerRunTerminated
michaellans Aug 17, 2026
e6c0763
add routine data to run_table when loaded
michaellans Aug 18, 2026
02c155f
Skip error message on intentional BadgerRunTerminated during evaluate…
michaellans Sep 10, 2026
bb00428
support resume run action in main gui
michaellans Sep 10, 2026
1234916
add separate args_queue for subprocess startup args instead of reusin…
michaellans Sep 10, 2026
4f7455d
remove unused import
michaellans Sep 11, 2026
845e6af
minor termination_reached_dialog ui update
michaellans Sep 15, 2026
a326546
add BadgerTerminationReachedDialog to supress_popups mocker for testing
michaellans Sep 15, 2026
76e72eb
update tests for subprocess with args_queue
michaellans Sep 15, 2026
c621527
Merge branch 'main' into run_until
michaellans Sep 17, 2026
10cc8c3
lint after fixing import merge conflict
michaellans Sep 17, 2026
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
86 changes: 81 additions & 5 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 @@ -141,6 +186,7 @@ def convert_to_solution(result: DataFrame, routine: Routine):


def run_routine_subprocess(
args_queue: mp.Queue,
queue: mp.Queue,
evaluate_queue: mp.Pipe,
stop_process: mp.Event,
Expand Down Expand Up @@ -192,7 +238,7 @@ def run_routine_subprocess(

args: dict[str, Any] = {}
try:
args = queue.get(timeout=1)
args = args_queue.get(timeout=1)
logger.debug(f"Received args from queue: {args}")
except Exception as e:
logger.error(f"Error in subprocess queue.get: {type(e).__name__}, {str(e)}")
Expand Down Expand Up @@ -323,16 +369,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"
98 changes: 88 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 All @@ -98,6 +134,7 @@ class BadgerActionBar(QWidget):
sig_jump_to_optimal = pyqtSignal()
sig_dial_in = pyqtSignal()
sig_ctrl = pyqtSignal(bool)
sig_run_with_data = pyqtSignal()
sig_open_extensions_palette = pyqtSignal()

sig_save_checkpoint = pyqtSignal()
Expand Down Expand Up @@ -160,7 +197,7 @@ 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="Run Options Menu")
self.btn_stop.setFixedSize(96, 32)
self.btn_stop.setFont(cool_font)
self.btn_stop.setStyleSheet(stylesheet_run)
Expand Down Expand Up @@ -198,15 +235,24 @@ 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)
self.run_with_data_action = run_with_data_action = QAction("Resume", self)
run_with_data_action.setIcon(self.icon_play)
menu.addAction(run_action)
menu.addAction(run_until_action)
menu.addAction(run_until_menu_action)
menu.addAction(run_with_data_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 +290,14 @@ 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.run_with_data_action.triggered.connect(
lambda: self.sig_run_with_data.emit()
)
self.save_checkpoint_action.triggered.connect(
lambda: self.sig_save_checkpoint.emit()
)
Expand Down Expand Up @@ -293,6 +345,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 +374,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 +391,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 +447,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)
3 changes: 3 additions & 0 deletions src/badger/gui/components/create_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def create_subprocess(self) -> None:
"""
self.stop_event = Event()
self.pause_event = Event()
self.args_queue = Queue()
self.data_queue = Queue()
self.evaluate_queue = Pipe()
self.wait_event = Event()
Expand All @@ -47,6 +48,7 @@ def create_subprocess(self) -> None:
new_process = Process(
target=run_routine_subprocess,
args=(
self.args_queue,
self.data_queue,
self.evaluate_queue,
self.stop_event,
Expand All @@ -61,6 +63,7 @@ def create_subprocess(self) -> None:
self.subprocess_prepared.emit(
{
"process": new_process,
"args_queue": self.args_queue,
"stop_event": self.stop_event,
"pause_event": self.pause_event,
"data_queue": self.data_queue,
Expand Down
Loading
Loading