Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 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
8f7633b
extract termination condition check logic into a separate method
michaellans Sep 15, 2026
fa9c6d4
Add SmartRunController class to control play/pause/resume/restart beh…
michaellans Sep 15, 2026
16ec1cf
extract boolean compatibility check to new method loaded_data_keys_co…
michaellans Sep 15, 2026
b243d95
add smart_run_action and stop_run_action to BadgerActionBar run optio…
michaellans Sep 16, 2026
0a1b185
add run_controller to mini/home_page.py for smart_run
michaellans Sep 16, 2026
623c752
add self.paused status to run_monitor
michaellans Sep 16, 2026
f3166da
Add get_routine_snapshot and switch template_cb trigger from textChan…
michaellans Sep 16, 2026
af34d48
update closeEvent handling to check for paused state and stop routine…
michaellans Sep 16, 2026
b3ebbc7
patch to allow adding data to sequential generators
michaellans Sep 16, 2026
25ba2de
remove error message when loading data with neldermead
michaellans Sep 16, 2026
29d8d7e
update status with termination condition while running
michaellans Sep 16, 2026
fb219cb
update run_controller comments for clarity
michaellans Sep 16, 2026
d971221
remove pause button
michaellans Sep 16, 2026
bba3c1b
handle close while paused
michaellans Sep 16, 2026
21b5047
fix pre-existing bug with close behavior while routine is running
michaellans Sep 17, 2026
a0427ad
linting
michaellans Sep 17, 2026
d760f2c
add tests for run_controller and update test_pause_play to remove btn…
michaellans Sep 17, 2026
bbfa13e
Merge branch 'main' into smart_run2
michaellans Sep 18, 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
175 changes: 143 additions & 32 deletions src/badger/core_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@
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
from badger.routine import Routine
from badger.log import configure_process_logging
from xopt.errors import FeasibilityError, XoptError
from xopt.vocs import select_best
from xopt.generators.sequential import SequentialGenerator


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -89,6 +94,119 @@ 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 check_termination_condition(
termination_condition: dict,
start_time: float,
routine: Routine,
queue: mp.Queue,
stop_process: mp.Event,
pause_process: mp.Event,
dialog_action_queue: mp.Queue,
) -> bool:
"""
Check whether termination conditon has been reached.
Pause for user action when a configured termination condition is reached.
"""
if not termination_condition or not start_time:
return False

tc_config = termination_condition
idx = tc_config["tc_idx"]
if idx == 0:
max_eval = tc_config["max_eval"]
if routine.data is not None:
if "live" in routine.data.columns:
# Only count number of live data points
count = sum(1 for live_val in routine.data["live"] if live_val == 1)
else:
count = len(routine.data)
logger.debug(f"Checking max_eval termination: {count} >= {max_eval}")
else:
count = 0

if count >= max_eval:
logger.info(
"Max evaluations 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_eval",
"config": max_eval,
"state": count,
},
)
return True
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. 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,
},
)
return True

return False


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 +259,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 +311,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 All @@ -214,6 +333,9 @@ def run_routine_subprocess(
if routine.data is not None:
logger.info("Resetting routine data")
routine.data = routine.data.iloc[0:0] # reset the data
else:
if isinstance(routine.generator, SequentialGenerator):
routine.generator.add_data(routine.data.copy())

except Exception as e:
error_title = f"{type(e).__name__}: {e}"
Expand Down Expand Up @@ -302,37 +424,26 @@ def run_routine_subprocess(
logger.info("Pause process not set. Waiting...")
pause_process.wait()

if termination_condition and start_time:
tc_config = termination_condition
idx = tc_config["tc_idx"]
if idx == 0:
max_eval = tc_config["max_eval"]
if routine.data is not None:
if "live" in routine.data.columns:
# Only count number of live data points
count = sum(
1 for live_val in routine.data["live"] if live_val == 1
)
else:
count = len(routine.data)
logger.debug(
f"Checking max_eval termination: {count} >= {max_eval}"
)
else:
count = 0

if count >= max_eval:
logger.info(
"Max evaluations reached. Terminating optimization."
)
raise BadgerRunTerminated
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
if check_termination_condition(
termination_condition,
start_time,
routine,
queue,
stop_process,
pause_process,
dialog_action_queue,
):
# termination_condition = extend_termination_condition(
# termination_condition, original_termination_condition
# )
termination_condition = None
queue.put(
{
"type": "termination_extended",
"termination_condition": termination_condition,
}
)
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"
Loading
Loading