From e54e09bb44b19738369fb81fc72d5a9e8757870c Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Wed, 14 Jan 2026 16:57:37 -0500 Subject: [PATCH 1/6] adding interactive agent with how-to-guide --- docs/source/how-to-guides.rst | 1 + .../interactive-optimization.rst | 206 ++++++++++++++++++ src/blop/ax/agent.py | 12 +- src/blop/plans/__init__.py | 5 +- src/blop/plans/plans.py | 164 +++++++++++++- src/blop/plans/utils.py | 76 +++++++ 6 files changed, 458 insertions(+), 6 deletions(-) create mode 100644 docs/source/how-to-guides/interactive-optimization.rst diff --git a/docs/source/how-to-guides.rst b/docs/source/how-to-guides.rst index e4a1780b..68e06f35 100644 --- a/docs/source/how-to-guides.rst +++ b/docs/source/how-to-guides.rst @@ -7,6 +7,7 @@ How-to Guides how-to-guides/use-ophyd-devices.rst how-to-guides/attach-data-to-experiments.rst how-to-guides/custom-generation-strategies.rst + how-to-guides/interactive-optimization.rst how-to-guides/set-dof-constraints.rst how-to-guides/set-outcome-constraints.rst how-to-guides/acquire-baseline.rst diff --git a/docs/source/how-to-guides/interactive-optimization.rst b/docs/source/how-to-guides/interactive-optimization.rst new file mode 100644 index 00000000..e870bf2e --- /dev/null +++ b/docs/source/how-to-guides/interactive-optimization.rst @@ -0,0 +1,206 @@ +Interactive Optimization +======================= + +This guide explains how to use Blop's interactive optimization mode, which gives you fine-grained control over the optimization process through user prompts and manual interventions. + +Overview +-------- + +Interactive optimization allows you to: + +- Manually approve suggested points before they are evaluated +- Suggest custom points with known objective values during optimization +- Decide when to continue or stop the optimization + +The Interactive Optimization Flow +---------------------------------- + +When you run an optimization, the system follows this workflow: + +.. code-block:: text + + 1. Would you like to go interactively? + ├─ No: Run optimize_normal (automatic mode) + │ └─ Completes all iterations automatically + │ + └─ Yes: Interactive mode + │ + 1. Manually approve suggestions? + ├─ No: Use optimize_step (automated suggestions) + │ └─ Go to step 8 (post-iteration options) + │ + └─ Yes: Manual approval mode + │ + 1. How many steps before next approval? (x) + │ + 2. For each iteration: + | - Suggest point + | - Ask: "Do you approve this point?" + | - If Yes → evaluate point + | - If No → abandon point, suggest new one + │ + 3. After x iterations complete + └─ Go to step 8 + + 2. What would you like to do? + ├─ c → Continue optimization (no manual suggestions) + │ └─ Return to step 2 + │ + ├─ s → Suggest points manually + │ └─ Enter DOF values and objective values + │ └─ Ingest into model + │ └─ Return to step 2 + │ + └─ q → Quit optimization + +Starting an Interactive Optimization +------------------------------------- + +To start an interactive optimization, simply run the ``optimize`` method after defining your agent: + +.. code-block:: python + + RE(agent.optimize(iterations=10, n_points=1)) + +Initial Prompt +~~~~~~~~~~~~~~ + +When you start the optimization, you'll see: + +.. code-block:: text + + +----------------------------------------------------------+ + | Would you like to run the optimization in interactive | + | mode? | + +----------------------------------------------------------+ + y: Yes + n: No + + Enter choice [y,n]: + +- Choose ``y`` for interactive mode with full control +- Choose ``n`` for automatic mode (runs all iterations without prompts) + +Automatic Mode (Non-Interactive) +--------------------------------- + +If you choose ``n`` (No) at the initial prompt, the optimization runs in automatic mode: + +- All iterations execute without user intervention +- Points are suggested, evaluated, and ingested automatically + +Manual Approval Mode +-------------------- + +If you choose ``y`` (Yes) for interactive mode, you'll be asked: + +.. code-block:: text + + +----------------------------------------------------------+ + | Would you like to manually approve suggestions? | + +----------------------------------------------------------+ + y: Yes + n: No + + Enter choice [y,n]: + +Choosing Manual Approval +~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you choose ``y`` (Yes), you'll then be asked: + +.. code-block:: text + + +----------------------------------------------------------+ + | Number of steps before next approval | + +----------------------------------------------------------+ + > + +Enter how often you would like to be able to manually approve a suggested point. If for a suggested point, manual approval is given, you'll see: + +.. code-block:: text + + Do you approve this point {'x1': 2.34, 'x2': -1.56, '_id': 5}? (y/n): + +**Note:** The first 5 points are automatically approved to build an initial model. + +- Enter ``y`` to evaluate this point +- Enter ``n`` to abandon this point (it won't be evaluated, and will be marked as ``abandoned``) + +Automated Suggestions (No Manual Approval) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you choose ``n`` (No) for manual approval, the optimizer will: + +1. Generate suggestions automatically +2. Evaluate all points without asking for approval +3. Proceed to post-iteration options (see below) + +Post-Iteration Options +---------------------- + +After completing the iteration(s), you'll be prompted: + +.. code-block:: text + + +----------------------------------------------------------+ + | What would you like to do? | + +----------------------------------------------------------+ + c: continue optimization without suggestion + s: suggest points manually + q: quit optimization + + Enter choice [c,s,q]: + +Option c: Continue Optimization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Continues with another round of optimization iterations. The system will ask again if you want manual approval for the next round. + +Use this when: + +- You want to run more iterations +- You're satisfied with the current model and want to let it explore more + +Option s: Suggest Points Manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Allows you to manually input points with known objective values. This is useful when: + +- You have external data to add to the model +- You want to guide the optimization to specific regions +- You've performed experiments outside of Blop and want to incorporate the results + +When you choose this option, you'll be prompted to enter values for each DOF and objective: + +.. code-block:: text + + Enter value for x1 (float): 2.5 + Enter value for x2 (float): 1.3 + Enter value for my_objective (float): 42.7 + + +----------------------------------------------------------+ + | Do you want to suggest another point? | + +----------------------------------------------------------+ + y: Yes + n: No, finish suggestions + + Enter choice [y,n]: + +If you enter an invalid number (like "abc"), you'll see: + +.. code-block:: text + + Invalid input. Please enter a valid number for x1. + +And you'll be asked to try again for that specific parameter. + +Option q: Quit Optimization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ends the optimization immediately + +See Also +-------- + +- :doc:`/tutorials/simple-experiment` - Basic optimization tutorial diff --git a/src/blop/ax/agent.py b/src/blop/ax/agent.py index 687e382e..24366eed 100644 --- a/src/blop/ax/agent.py +++ b/src/blop/ax/agent.py @@ -44,6 +44,8 @@ class Agent: Constraints on outcomes to be satisfied during optimization. checkpoint_path : str | None, optional The path to the checkpoint file to save the optimizer's state to. + interactive : bool | False + Whether the optimization should be interactive. **kwargs : Any Additional keyword arguments to configure the Ax experiment. @@ -74,12 +76,14 @@ def __init__( dof_constraints: Sequence[DOFConstraint] | None = None, outcome_constraints: Sequence[OutcomeConstraint] | None = None, checkpoint_path: str | None = None, + interactive: bool = False, **kwargs: Any, ): self._sensors = sensors self._actuators = [dof.actuator for dof in dofs if dof.actuator is not None] self._evaluation_function = evaluation_function self._acquisition_plan = acquisition_plan + self._interactive = interactive self._optimizer = AxOptimizer( parameters=[dof.to_ax_parameter_config() for dof in dofs], objective=to_ax_objective_str(objectives), @@ -215,7 +219,7 @@ def ingest(self, points: list[dict]) -> None: points : list[dict] A list of dictionaries, each containing outcomes for a trial. For suggested points, include the "_id" key. For external data, include DOF names and - objective values, and omit "_id". + objective values, and omit "_id".difference between master and main in github Notes ----- @@ -250,7 +254,7 @@ def acquire_baseline(self, parameterization: dict[str, Any] | None = None) -> Ms """ yield from acquire_baseline(self.to_optimization_problem(), parameterization=parameterization) - def optimize(self, iterations: int = 1, n_points: int = 1) -> MsgGenerator[None]: + def optimize(self, iterations: int = 1, n_points: int = 1, interactive: bool = False) -> MsgGenerator[None]: """ Run Bayesian optimization. @@ -285,7 +289,9 @@ def optimize(self, iterations: int = 1, n_points: int = 1) -> MsgGenerator[None] suggest : Get point suggestions without running acquisition. ingest : Manually ingest evaluation results. """ - yield from optimize(self.to_optimization_problem(), iterations=iterations, n_points=n_points) + yield from optimize( + self.to_optimization_problem(), iterations=iterations, n_points=n_points, interactive=interactive + ) def plot_objective( self, x_dof_name: str, y_dof_name: str, objective_name: str, *args: Any, **kwargs: Any diff --git a/src/blop/plans/__init__.py b/src/blop/plans/__init__.py index fc196814..ce465ed5 100644 --- a/src/blop/plans/__init__.py +++ b/src/blop/plans/__init__.py @@ -4,11 +4,12 @@ default_acquire, optimize, optimize_step, + optimize_step_with_approval, per_step_background_read, read, take_reading_with_background, ) -from .utils import get_route_index, route_suggestions +from .utils import get_route_index, retrieve_suggestions_from_user, route_suggestions __all__ = [ "acquire_baseline", @@ -17,6 +18,8 @@ "get_route_index", "optimize", "optimize_step", + "optimize_step_with_approval", + "retrieve_suggestions_from_user", "per_step_background_read", "read", "route_suggestions", diff --git a/src/blop/plans/plans.py b/src/blop/plans/plans.py index a1efa157..289ff344 100644 --- a/src/blop/plans/plans.py +++ b/src/blop/plans/plans.py @@ -9,7 +9,7 @@ from bluesky.utils import MsgGenerator, plan from ..protocols import ID_KEY, Actuator, Checkpointable, OptimizationProblem, Sensor -from .utils import route_suggestions +from .utils import ask_user_for_input, retrieve_suggestions_from_user, route_suggestions logger = logging.getLogger(__name__) @@ -122,7 +122,56 @@ def optimize_step( @plan -def optimize( +def optimize_step_with_approval( + optimization_problem: OptimizationProblem, + n_points: int = 1, + *args: Any, + **kwargs: Any, +) -> MsgGenerator[None]: + """ + A single step of the optimization loop with manual approval of suggestions. + + Parameters + ---------- + optimization_problem : OptimizationProblem + The optimization problem to solve. + n_points : int, optional + The number of points to suggest. + """ + if optimization_problem.acquisition_plan is None: + acquisition_plan = default_acquire + else: + acquisition_plan = optimization_problem.acquisition_plan + optimizer = optimization_problem.optimizer + actuators = optimization_problem.actuators + suggestions = optimizer.suggest(n_points) + + approved_suggestions = [] + for suggestion in suggestions: + trial_id = suggestion.get(ID_KEY) + + # Auto-approve first 5 points to build initial model + if trial_id is not None and trial_id < 5: + approved_suggestions.append(suggestion) + continue + + # Ask for manual approval for subsequent points + if input(f"Do you approve this point: {suggestion}? (y/n): ").strip().lower() == "y": + approved_suggestions.append(suggestion) + else: + optimizer.ax_client._experiment.trials[trial_id].mark_abandoned() # type: ignore[attr-defined] + + if len(approved_suggestions) == 0: + print("No suggestions approved. Skipping this optimization step.") + return + + uid = yield from acquisition_plan(approved_suggestions, actuators, optimization_problem.sensors, *args, **kwargs) + outcomes = optimization_problem.evaluation_function(uid, approved_suggestions) + optimizer.ingest(outcomes) + + +@plan +def optimize_manual( optimization_problem: OptimizationProblem, iterations: int = 1, n_points: int = 1, @@ -156,7 +205,65 @@ def optimize( blop.protocols.Checkpointable : The protocol for checkpointable objects. optimize_step : The plan to execute a single step of the optimization. """ + while True: + # Ask once per cycle if user wants manual approval + use_manual_approval = ask_user_for_input( + "Would you like to manually approve suggestions?", options={"y": "Yes", "n": "No"} + ) + n_steps = int(ask_user_for_input("Number of steps before next approval")) if use_manual_approval == "y" else n_points + for i in range(iterations): + if use_manual_approval == "y": + yield from optimize_step_with_approval(optimization_problem, n_steps, n_points, *args, **kwargs) + else: + yield from optimize_step(optimization_problem, n_points, *args, **kwargs) + + if checkpoint_interval and (i + 1) % checkpoint_interval == 0: + if not isinstance(optimization_problem.optimizer, Checkpointable): + raise ValueError( + "The optimizer is not checkpointable. Please review your optimizer configuration or implementation." + ) + optimization_problem.optimizer.checkpoint() + result = ask_user_for_input( + "What would you like to do?", + options={ + "c": "continue optimization without suggestion", + "s": "suggest points manually", + "q": "quit optimization", + }, + ) + if result.lower().strip() == "s": + suggestions = retrieve_suggestions_from_user( + actuators=optimization_problem.actuators, # type: ignore[attr-defined] + optimizer=optimization_problem.optimizer, # type: ignore[attr-defined] + ) + if suggestions is not None: + optimization_problem.optimizer.ingest(suggestions) + elif result.lower().strip() == "q": + break + # If result == 'c', just continue the loop (do nothing) + + +@plan +def optimize_normal( + optimization_problem: OptimizationProblem, + iterations: int = 1, + n_points: int = 1, + checkpoint_interval: int | None = None, + *args: Any, + **kwargs: Any, +) -> MsgGenerator[None]: + """ + A plan to solve the optimization problem. + Parameters + ---------- + optimization_problem : OptimizationProblem + The optimization problem to solve. + iterations : int, optional + The number of optimization iterations to run. + n_points : int, optional + The number of points to suggest per iteration. + """ for i in range(iterations): yield from optimize_step(optimization_problem, n_points, *args, **kwargs) if checkpoint_interval and (i + 1) % checkpoint_interval == 0: @@ -167,6 +274,59 @@ def optimize( optimization_problem.optimizer.checkpoint() +def optimize( + optimization_problem: OptimizationProblem, + iterations: int = 1, + n_points: int = 1, + checkpoint_interval: int | None = None, + interactive: bool = False, + *args: Any, + **kwargs: Any, +) -> MsgGenerator[None]: + """ + A plan to solve the optimization problem. + + Parameters + ---------- + optimization_problem : OptimizationProblem + The optimization problem to solve. + iterations : int, optional + The number of optimization iterations to run. + n_points : int, optional + The number of points to suggest per iteration. + checkpoint_interval : int | None, optional + The number of iterations between optimizer checkpoints. If None, checkpoints + will not be saved. Optimizer must implement the + :class:`blop.protocols.Checkpointable` protocol. + *args : Any + Additional positional arguments to pass to the :func:`optimize_step` plan. + **kwargs : Any + Additional keyword arguments to pass to the :func:`optimize_step` plan. + + See Also + -------- + blop.protocols.OptimizationProblem : The problem to solve. + blop.protocols.Checkpointable : The protocol for checkpointable objects. + optimize_step : The plan to execute a single step of the optimization. + """ + import sys + + # Only prompt for interactive mode if stdin is available (not in tests/automated environments) + if sys.stdin.isatty(): + response = ask_user_for_input( + "Would you like to run the optimization in interactive mode?", options={"y": "Yes", "n": "No"} + ) + use_interactive = response == "y" + else: + # Running in automated/test environment - use non-interactive mode + use_interactive = False + + if use_interactive: + yield from optimize_manual(optimization_problem, iterations, n_points, *args, **kwargs) + else: + yield from optimize_normal(optimization_problem, iterations, n_points, checkpoint_interval, *args, **kwargs) + + @plan def read(readables: Sequence[Readable], **kwargs: Any) -> MsgGenerator[dict[str, Any]]: """ diff --git a/src/blop/plans/utils.py b/src/blop/plans/utils.py index f33a06f2..5de89bdd 100644 --- a/src/blop/plans/utils.py +++ b/src/blop/plans/utils.py @@ -1,3 +1,5 @@ +from typing import Any + import networkx as nx import numpy as np @@ -35,3 +37,77 @@ def route_suggestions(suggestions: list[dict], starting_position: dict | None = starting_point = np.array([starting_position[dim] for dim in dims_to_route]) if starting_position else None return [suggestions[i] for i in get_route_index(points=points, starting_point=starting_point)] + + +def ask_user_for_input(prompt: str, options: dict | None = None) -> Any: + BOLD = "\033[1m" + BLUE = "\033[94m" + RESET = "\033[0m" + + print() + print(f"{BOLD}{BLUE}") + print("+" + "-" * max((len(prompt) + 2 * 2), 58) + "+") + print(f"| {prompt}".ljust(max((len(prompt) + 2 * 2), 58)) + " |") + print("+" + "-" * max((len(prompt) + 2 * 2), 58) + "+") + print(RESET, end="") + if options is not None: + for key, value in options.items(): + print(f" {BOLD}{key}{RESET}: {value}") + + while True: + # Build the prompt string with keys dynamically + valid_keys = list(options.keys()) + keys_prompt = ",".join(valid_keys) + choice = input(f"\nEnter choice [{keys_prompt}]: ").lower().strip() + if choice in options: + user_input = choice + break + print("Invalid selection.") + else: + user_input = input("> ") + return user_input + + +def retrieve_suggestions_from_user(actuators: list, optimizer) -> list[dict] | None: + """ + Retrieve manual point suggestions from the user. + + Parameters + ---------- + actuators : list + The actuators to suggest values for. + optimizer : Optimizer + The optimizer containing parameter and objective configurations. + + Returns + ------- + list[dict] | None + A list of suggested points as dictionaries with DOF and objective values, or None if user declined. + """ + # Get parameter configs and objectives from the optimizer + objectives = optimizer.ax_client._experiment.optimization_config.objective.metrics + model_components = actuators + objectives + + suggestions = [] + while True: + # Build a single suggestion point with all DOF and objective values + new_suggestion = {} + for item in model_components: + while True: + try: + value = float(input(f"Enter value for {item.name} (float): ")) + new_suggestion[item.name] = value + break + except ValueError: + print(f"Invalid input. Please enter a valid number for {item.name}.") + + suggestions.append(new_suggestion) + + # Ask if user wants to add more points + if ( + ask_user_for_input("Do you want to suggest another point?", options={"y": "Yes", "n": "No, finish suggestions"}) + == "n" + ): + break + + return suggestions From d5e3b13e3a2b7b141c466cc9b9a81b01750d6914 Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Wed, 14 Jan 2026 17:03:18 -0500 Subject: [PATCH 2/6] fixed build_docs --- docs/source/how-to-guides/interactive-optimization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/how-to-guides/interactive-optimization.rst b/docs/source/how-to-guides/interactive-optimization.rst index e870bf2e..08832881 100644 --- a/docs/source/how-to-guides/interactive-optimization.rst +++ b/docs/source/how-to-guides/interactive-optimization.rst @@ -1,5 +1,5 @@ Interactive Optimization -======================= +======================== This guide explains how to use Blop's interactive optimization mode, which gives you fine-grained control over the optimization process through user prompts and manual interventions. From d664dfaa562a2e75d7f7fc2d5e6939a99cf6647a Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Thu, 15 Jan 2026 11:32:13 -0500 Subject: [PATCH 3/6] making code easier to read --- .../interactive-optimization.rst | 54 +++++++++---------- src/blop/plans/plans.py | 5 +- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/docs/source/how-to-guides/interactive-optimization.rst b/docs/source/how-to-guides/interactive-optimization.rst index 08832881..16b1fc80 100644 --- a/docs/source/how-to-guides/interactive-optimization.rst +++ b/docs/source/how-to-guides/interactive-optimization.rst @@ -19,39 +19,39 @@ When you run an optimization, the system follows this workflow: .. code-block:: text - 1. Would you like to go interactively? + Would you like to go interactively? ├─ No: Run optimize_normal (automatic mode) │ └─ Completes all iterations automatically │ └─ Yes: Interactive mode │ 1. Manually approve suggestions? - ├─ No: Use optimize_step (automated suggestions) - │ └─ Go to step 8 (post-iteration options) - │ - └─ Yes: Manual approval mode - │ - 1. How many steps before next approval? (x) - │ - 2. For each iteration: - | - Suggest point - | - Ask: "Do you approve this point?" - | - If Yes → evaluate point - | - If No → abandon point, suggest new one - │ - 3. After x iterations complete - └─ Go to step 8 - - 2. What would you like to do? - ├─ c → Continue optimization (no manual suggestions) - │ └─ Return to step 2 - │ - ├─ s → Suggest points manually - │ └─ Enter DOF values and objective values - │ └─ Ingest into model - │ └─ Return to step 2 - │ - └─ q → Quit optimization + | ├─ No: Use optimize_step (automated suggestions) + | │ └─ Go to step 2 (post-iteration options) + | │ + | └─ Yes: Manual approval mode + | │ + | a. How many steps before next approval? (x) + | │ + | b. For each iteration: + | | - Suggest point + | | - Ask: "Do you approve this point?" + | | - If Yes → evaluate point + | | - If No → abandon point, suggest new one + | │ + | c. After x iterations complete + | └─ Go to step 2 + | + 2. What would you like to do? + ├─ c: Continue optimization (no manual suggestions) + │ └─ Return to step 1 + │ + ├─ s: Suggest points manually + │ └─ Enter DOF values and objective values + │ └─ Ingest into model + │ └─ Return to step 1 + │ + └─ q: Quit optimization Starting an Interactive Optimization ------------------------------------- diff --git a/src/blop/plans/plans.py b/src/blop/plans/plans.py index 289ff344..d95fda24 100644 --- a/src/blop/plans/plans.py +++ b/src/blop/plans/plans.py @@ -313,15 +313,14 @@ def optimize( # Only prompt for interactive mode if stdin is available (not in tests/automated environments) if sys.stdin.isatty(): - response = ask_user_for_input( + use_interactive = ask_user_for_input( "Would you like to run the optimization in interactive mode?", options={"y": "Yes", "n": "No"} ) - use_interactive = response == "y" else: # Running in automated/test environment - use non-interactive mode use_interactive = False - if use_interactive: + if use_interactive == "y": yield from optimize_manual(optimization_problem, iterations, n_points, *args, **kwargs) else: yield from optimize_normal(optimization_problem, iterations, n_points, checkpoint_interval, *args, **kwargs) From 6118a5d00b5e38515ea7b5c0d9abf19551d32a38 Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Thu, 15 Jan 2026 15:48:37 -0500 Subject: [PATCH 4/6] fixed bug with interactive agent and jupyter notebook + fixed docstrings --- src/blop/ax/agent.py | 10 ++-------- src/blop/plans/plans.py | 37 ++++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/blop/ax/agent.py b/src/blop/ax/agent.py index 24366eed..e1bf5b8b 100644 --- a/src/blop/ax/agent.py +++ b/src/blop/ax/agent.py @@ -44,8 +44,6 @@ class Agent: Constraints on outcomes to be satisfied during optimization. checkpoint_path : str | None, optional The path to the checkpoint file to save the optimizer's state to. - interactive : bool | False - Whether the optimization should be interactive. **kwargs : Any Additional keyword arguments to configure the Ax experiment. @@ -76,14 +74,12 @@ def __init__( dof_constraints: Sequence[DOFConstraint] | None = None, outcome_constraints: Sequence[OutcomeConstraint] | None = None, checkpoint_path: str | None = None, - interactive: bool = False, **kwargs: Any, ): self._sensors = sensors self._actuators = [dof.actuator for dof in dofs if dof.actuator is not None] self._evaluation_function = evaluation_function self._acquisition_plan = acquisition_plan - self._interactive = interactive self._optimizer = AxOptimizer( parameters=[dof.to_ax_parameter_config() for dof in dofs], objective=to_ax_objective_str(objectives), @@ -254,7 +250,7 @@ def acquire_baseline(self, parameterization: dict[str, Any] | None = None) -> Ms """ yield from acquire_baseline(self.to_optimization_problem(), parameterization=parameterization) - def optimize(self, iterations: int = 1, n_points: int = 1, interactive: bool = False) -> MsgGenerator[None]: + def optimize(self, iterations: int = 1, n_points: int = 1) -> MsgGenerator[None]: """ Run Bayesian optimization. @@ -289,9 +285,7 @@ def optimize(self, iterations: int = 1, n_points: int = 1, interactive: bool = F suggest : Get point suggestions without running acquisition. ingest : Manually ingest evaluation results. """ - yield from optimize( - self.to_optimization_problem(), iterations=iterations, n_points=n_points, interactive=interactive - ) + yield from optimize(self.to_optimization_problem(), iterations=iterations, n_points=n_points) def plot_objective( self, x_dof_name: str, y_dof_name: str, objective_name: str, *args: Any, **kwargs: Any diff --git a/src/blop/plans/plans.py b/src/blop/plans/plans.py index d95fda24..c19877ea 100644 --- a/src/blop/plans/plans.py +++ b/src/blop/plans/plans.py @@ -1,5 +1,7 @@ import functools import logging +import os +import sys from collections.abc import Callable, Mapping, Sequence from typing import Any, cast @@ -125,6 +127,7 @@ def optimize_step( def optimize_step_with_approval( optimization_problem: OptimizationProblem, n_points: int = 1, + n_steps: int = 1, *args: Any, **kwargs: Any, ) -> MsgGenerator[None]: @@ -137,6 +140,8 @@ def optimize_step_with_approval( The optimization problem to solve. n_points : int, optional The number of points to suggest. + n_steps : int, optional + The number of steps before next manual approval. """ if optimization_problem.acquisition_plan is None: acquisition_plan = default_acquire @@ -156,10 +161,13 @@ def optimize_step_with_approval( continue # Ask for manual approval for subsequent points - if input(f"Do you approve this point: {suggestion}? (y/n): ").strip().lower() == "y": - approved_suggestions.append(suggestion) + if trial_id is not None and trial_id % n_steps == 0: + if input(f"Do you approve this point: {suggestion}? (y/n): ").strip().lower() == "y": + approved_suggestions.append(suggestion) + else: + optimizer.ax_client._experiment.trials[trial_id].mark_abandoned() # type: ignore[attr-defined] else: - optimizer.ax_client._experiment.trials[trial_id].mark_abandoned() # type: ignore[attr-defined] + approved_suggestions.append(suggestion) if len(approved_suggestions) == 0: print("No suggestions approved. Skipping this optimization step.") @@ -180,7 +188,8 @@ def optimize_manual( **kwargs: Any, ) -> MsgGenerator[None]: """ - A plan to solve the optimization problem. + A plan to solve the optimization problem that allows for manual iteraction with the optimization process + through manual approval of suggestions and the ability to manually suggest points. Parameters ---------- @@ -198,12 +207,6 @@ def optimize_manual( Additional positional arguments to pass to the :func:`optimize_step` plan. **kwargs : Any Additional keyword arguments to pass to the :func:`optimize_step` plan. - - See Also - -------- - blop.protocols.OptimizationProblem : The problem to solve. - blop.protocols.Checkpointable : The protocol for checkpointable objects. - optimize_step : The plan to execute a single step of the optimization. """ while True: # Ask once per cycle if user wants manual approval @@ -213,7 +216,7 @@ def optimize_manual( n_steps = int(ask_user_for_input("Number of steps before next approval")) if use_manual_approval == "y" else n_points for i in range(iterations): if use_manual_approval == "y": - yield from optimize_step_with_approval(optimization_problem, n_steps, n_points, *args, **kwargs) + yield from optimize_step_with_approval(optimization_problem, n_points, n_steps, *args, **kwargs) else: yield from optimize_step(optimization_problem, n_points, *args, **kwargs) @@ -253,7 +256,7 @@ def optimize_normal( **kwargs: Any, ) -> MsgGenerator[None]: """ - A plan to solve the optimization problem. + A plan to solve the optimization problem that completes the loop automatically. Parameters ---------- @@ -279,7 +282,6 @@ def optimize( iterations: int = 1, n_points: int = 1, checkpoint_interval: int | None = None, - interactive: bool = False, *args: Any, **kwargs: Any, ) -> MsgGenerator[None]: @@ -309,15 +311,16 @@ def optimize( blop.protocols.Checkpointable : The protocol for checkpointable objects. optimize_step : The plan to execute a single step of the optimization. """ - import sys - # Only prompt for interactive mode if stdin is available (not in tests/automated environments) - if sys.stdin.isatty(): + # Check if running under pytest + in_pytest = "pytest" in sys.modules or "PYTEST_CURRENT_TEST" in os.environ + + # Only prompt for interactive mode (NOT in pytest (terminal or notebook is OK)) + if not in_pytest: use_interactive = ask_user_for_input( "Would you like to run the optimization in interactive mode?", options={"y": "Yes", "n": "No"} ) else: - # Running in automated/test environment - use non-interactive mode use_interactive = False if use_interactive == "y": From 693ad9645778219f85dd8bf0a8bda27349a71903 Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Thu, 15 Jan 2026 16:37:44 -0500 Subject: [PATCH 5/6] fix build_docs --- src/blop/plans/plans.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/blop/plans/plans.py b/src/blop/plans/plans.py index c19877ea..d9cac130 100644 --- a/src/blop/plans/plans.py +++ b/src/blop/plans/plans.py @@ -312,15 +312,17 @@ def optimize( optimize_step : The plan to execute a single step of the optimization. """ - # Check if running under pytest + # Check if running under pytest or doctest in_pytest = "pytest" in sys.modules or "PYTEST_CURRENT_TEST" in os.environ + in_doctest = "doctest" in sys.modules or "_pytest.doctest" in sys.modules - # Only prompt for interactive mode (NOT in pytest (terminal or notebook is OK)) - if not in_pytest: + # Only prompt for interactive mode if NOT in automated test environments + if not (in_pytest or in_doctest): use_interactive = ask_user_for_input( "Would you like to run the optimization in interactive mode?", options={"y": "Yes", "n": "No"} ) else: + # Running in automated test environment - use non-interactive mode use_interactive = False if use_interactive == "y": From 9268f6c4fc0bc6b97ffbd24c8d0248da4d97a353 Mon Sep 17 00:00:00 2001 From: jessica-moylan Date: Thu, 15 Jan 2026 17:05:00 -0500 Subject: [PATCH 6/6] prevented interactive agent when running build-docs --- src/blop/plans/plans.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/blop/plans/plans.py b/src/blop/plans/plans.py index d9cac130..cdbd97ac 100644 --- a/src/blop/plans/plans.py +++ b/src/blop/plans/plans.py @@ -312,17 +312,38 @@ def optimize( optimize_step : The plan to execute a single step of the optimization. """ - # Check if running under pytest or doctest - in_pytest = "pytest" in sys.modules or "PYTEST_CURRENT_TEST" in os.environ - in_doctest = "doctest" in sys.modules or "_pytest.doctest" in sys.modules + # Check if stdin is available for user input + stdin_available = True - # Only prompt for interactive mode if NOT in automated test environments - if not (in_pytest or in_doctest): + # Check if running under pytest or doctest + if ( + "pytest" in sys.modules + or "PYTEST_CURRENT_TEST" in os.environ + or "doctest" in sys.modules + or "_pytest.doctest" in sys.modules + ): + stdin_available = False + # if: + # Check if running in a Jupyter environment without stdin support + try: + from IPython.core.getipython import get_ipython + + ipython = get_ipython() + if ipython is not None: + # The kernel attribute only exists in IPyKernel environments + kernel = getattr(ipython, "kernel", None) + if kernel is not None and not getattr(kernel, "_allow_stdin", True): + stdin_available = False + except (ImportError, AttributeError): + pass + + # Only prompt for interactive mode if stdin is available + if stdin_available: use_interactive = ask_user_for_input( "Would you like to run the optimization in interactive mode?", options={"y": "Yes", "n": "No"} ) else: - # Running in automated test environment - use non-interactive mode + # stdin not available - use non-interactive mode use_interactive = False if use_interactive == "y":