Asynchronous Optimization with Bluesky Queueserver#

In this tutorial, you will learn how to run Blop optimization against a remote Bluesky Queueserver. This architecture is used when:

  • The experiment hardware is controlled by a shared instrument server

  • You want the optimizer to run in a separate process from the RunEngine

  • You need asynchronous, non-blocking optimization (the agent submits plans and reacts to completions)

We will optimize the same Himmelblau function from the simple experiment tutorial, but now the devices live inside a remote queueserver process rather than in the same Python session as the agent.

Architecture#

The distributed system has five components:

        flowchart
    subgraph docker["Docker Compose Stack"]
        redis["Redis"]
        rem["RE Manager<br/>(devices + plans)"]
        zmqp["ZMQ Proxy<br/>(pub/sub)"]
        bridge["ZMQ-Tiled Bridge<br/>(persists docs to Tiled)"]
        tiled["Tiled Server<br/>(data storage)"]

        redis <-->|state| rem
        rem -->|publishes documents| zmqp
        zmqp -->|document stream| bridge
        bridge -->|writes| tiled
    end

    agent["Blop QueueserverAgent<br/>(suggests points, listens for completions, evaluates data)"]

    agent -->|submit plans via REManagerAPI| rem
    zmqp -->|document stream| agent
    tiled -->|read data| agent
    

Data flow:

  1. The agent suggests parameter values and submits an acquisition plan to the RE Manager

  2. The RE Manager executes the plan (moves motors, reads detectors)

  3. Bluesky documents are published via ZMQ to the proxy

  4. The ZMQ-Tiled bridge persists documents to the Tiled server (using bluesky-tiled-plugins’s TiledWriter callback)

  5. The agent’s ZMQ listener detects plan completion

  6. The agent’s evaluation function reads results from Tiled and computes objectives

  7. The optimizer ingests outcomes and suggests the next point

Prerequisites#

  • Docker and Docker Compose installed

  • The blop Python package installed (with bluesky-queueserver-api and tiled)

  • The blop GitHub repository cloned (for the service definitions under docs/source/tutorials/queueserver/)

Starting the Infrastructure#

All services are defined in a docker-compose.yml in the docs/source/tutorials/queueserver/ directory. Before running this tutorial, start the stack in a separate terminal:

cd docs/source/tutorials/queueserver
docker compose up -d --build

Wait until all containers are healthy:

docker compose ps

You should see all services in a “healthy” or “running” state. The services expose the following ports on localhost:

Service

Port

Purpose

RE Manager

60615

ZMQ control channel (REManagerAPI connects here)

ZMQ Proxy (out)

5578

Document stream (agent listens for plan completions)

Tiled

8000

Data access (evaluation function reads results)

Redis

6379

Internal message broker for queueserver

Once the containers are up, proceed with the tutorial below.

import time

from bluesky_queueserver_api.zmq import REManagerAPI

RM = REManagerAPI(zmq_control_addr="tcp://localhost:60615")
RM.environment_open()
RM.wait_for_idle(timeout=30)
status = RM.status()
print(f"RE Manager state: {status['manager_state']}")
print(f"Worker environment exists: {status['worker_environment_exists']}")
assert status["worker_environment_exists"], "Open the RE environment before continuing (see instructions above)"
RE Manager state: idle
Worker environment exists: True

The Queueserver Environment#

The queueserver startup script (shown below for reference) defines the devices and plans available in the remote environment. This script runs inside the RE Manager process — not in your notebook:

# startup.py (runs inside the RE Manager)
from bluesky import RunEngine
from bluesky_queueserver import is_re_worker_active
from ophyd.sim import SynAxis, SynSignal

RE = RunEngine({})

# Publish documents to ZMQ so external subscribers can react
if is_re_worker_active():
    import os
    from bluesky.callbacks.zmq import Publisher as ZmqPublisher

    addr = os.environ.get("BLUESKY_ZMQ_PROXY_IN_ADDR", "tcp://zmq-proxy:5577")
    host, port = addr.replace("tcp://", "").split(":")
    publisher = ZmqPublisher((host, int(port)))
    RE.subscribe(publisher)

# Simulated motors
motor1 = SynAxis(name="motor1", labels={"motors"})
motor2 = SynAxis(name="motor2", labels={"motors"})


# Simulated detector that computes the Himmelblau function
def _compute_himmelblau():
    x = motor1.read()["motor1"]["value"]
    y = motor2.read()["motor2"]["value"]
    return float((x**2 + y - 11) ** 2 + (x + y**2 - 7) ** 2)


himmel_det = SynSignal(name="himmel_det", func=_compute_himmelblau, labels={"detectors"})

# Acquisition plan — moves actuators to suggested positions and reads sensors
import bluesky.plans as bp


def default_acquire(suggestions, actuators, sensors, *, md=None):
    plan_args = []
    for actuator in actuators:
        values = [s[actuator.name] for s in suggestions]
        plan_args.append(actuator)
        plan_args.append(values)

    _md = {"blop_suggestions": suggestions}
    if md:
        _md.update(md)

    yield from bp.list_scan(list(sensors), *plan_args, md=_md)

Key points:

  • is_re_worker_active() gates code that should only run inside the queueserver worker

  • The ZMQ publisher sends all run documents to the proxy for external consumption

  • default_acquire is a simple plan that wraps Bluesky’s list_scan — it moves actuators to each suggested position and reads sensors

Connecting to Tiled#

The evaluation function will read experimental data from the Tiled server:

from tiled.client import from_uri

tiled_client = from_uri("http://localhost:8000", api_key="tutorialkey")

Defining the Optimization Problem#

Just as in the simple experiment tutorial, we define DOFs and objectives. The key difference: since devices exist only in the remote queueserver environment, DOFs reference device names as strings (no actuator objects).

from blop.ax import QueueserverAgent, RangeDOF, Objective

dofs = [
    RangeDOF(actuator="motor1", bounds=(-5.0, 5.0), parameter_type="float"),
    RangeDOF(actuator="motor2", bounds=(-5.0, 5.0), parameter_type="float"),
]

objectives = [
    Objective(name="himmelblau", minimize=True),
]

# Sensors are referenced by name — these are the detectors in the queueserver environment
sensors = ["himmel_det"]
[WARNING 05-19 23:15:31] ax.storage.sqa_store.with_db_settings_base: Ax currently requires a sqlalchemy version below 2.0. This will be addressed in a future release. Disabling SQL storage in Ax for now, if you would like to use SQL storage please install Ax with mysql extras via `pip install ax-platform[mysql]`.

Writing the Evaluation Function#

The evaluation function is called each time a plan completes. It reads data from Tiled and computes the objective values. The function receives:

  • uid: the Bluesky run UID (use this to look up data in Tiled)

  • suggestions: the list of parameter dicts that were evaluated

It must return a list of outcome dicts, each containing the objective value(s) and an _id matching the suggestion.

Because the agent and the ZMQ-Tiled bridge are separate subscribers to the same ZMQ stream, there is a race condition: the agent may receive the stop document before the bridge has finished writing data to Tiled. The evaluation function should poll Tiled until both the run and the detector data are available.

import numpy as np
from tiled.client.container import Container


class HimmelblauEvaluation:
    """Reads detector data from Tiled and computes the Himmelblau objective."""

    def __init__(self, tiled_client: Container, timeout: float = 30.0, poll_interval: float = 0.5):
        self.tiled_client = tiled_client
        self.timeout = timeout
        self.poll_interval = poll_interval

    def _wait_for_run(self, uid: str):
        """Poll Tiled until the run with the given UID is available."""
        deadline = time.time() + self.timeout
        while time.time() < deadline:
            try:
                return self.tiled_client[uid]
            except KeyError:
                time.sleep(self.poll_interval)
        raise TimeoutError(
            f"Run '{uid}' not found in Tiled after {self.timeout}s. "
            "The ZMQ-Tiled bridge may not be running."
        )

    def _wait_for_detector_data(self, run, path: str):
        """Poll Tiled until the detector data path is readable."""
        deadline = time.time() + self.timeout
        while time.time() < deadline:
            try:
                return run[path].read()
            except KeyError:
                time.sleep(self.poll_interval)
        raise TimeoutError(
            f"Data path '{path}' for run '{run.metadata['start']['uid']}' was not readable after {self.timeout}s. "
            "The ZMQ-Tiled bridge may still be writing the run."
        )

    def __call__(self, uid: str, suggestions: list[dict]) -> list[dict]:
        run = self._wait_for_run(uid)

        # Read the detector values from the primary data stream
        himmel_values = self._wait_for_detector_data(run, "primary/himmel_det")

        outcomes = []
        for idx, suggestion in enumerate(suggestions):
            outcomes.append({
                "_id": suggestion["_id"],
                "himmelblau": float(himmel_values[idx]),
            })

        return outcomes

Creating the Queueserver Agent#

Now we bring everything together. The QueueserverAgent needs:

  • re_manager_api: how to communicate with the queueserver (submit plans, check status)

  • zmq_consumer_addr: where to listen for document completion events

  • The DOFs, objectives, sensors, and evaluation function

agent = QueueserverAgent(
    re_manager_api=RM,
    zmq_consumer_addr=("localhost", 5578),
    sensors=sensors,
    dofs=dofs,
    objectives=objectives,
    evaluation_function=HimmelblauEvaluation(tiled_client),
    acquisition_plan="default_acquire",
)

Note

The re_manager_api argument also accepts an HTTP-based client (bluesky_queueserver_api.http.REManagerAPI) for deployments that expose the queueserver over HTTP rather than ZMQ. The zmq_consumer_addr points to the ZMQ proxy output port (5578) where Bluesky documents are published.

Running the Optimization#

The run() method is non-blocking — it submits the first plan and returns immediately. The agent reacts asynchronously to plan completions via ZMQ callbacks.

future = agent.run(iterations=10, n_points=1)
[INFO 05-19 23:15:31] ax.api.client: GenerationStrategy(name='Center+Sobol+MBM:fast', nodes=[CenterGenerationNode(next_node_name='Sobol'), GenerationNode(name='Sobol', generator_specs=[GeneratorSpec(generator_enum=Sobol, generator_key_override=None)], transition_criteria=[MinTrials(transition_to='MBM'), MinTrials(transition_to='MBM')], suggested_experiment_status=ExperimentStatus.INITIALIZATION, pausing_criteria=[MaxTrialsAwaitingData(threshold=5)]), GenerationNode(name='MBM', generator_specs=[GeneratorSpec(generator_enum=BoTorch, generator_key_override=None)], transition_criteria=None, suggested_experiment_status=ExperimentStatus.OPTIMIZATION, pausing_criteria=None)]) chosen based on user input and problem structure.
[INFO 05-19 23:15:31] ax.api.client: Generated new trial 0 with parameters {'motor1': 0.0, 'motor2': 0.0} using GenerationNode CenterOfSearchSpace.

Wait for the optimization to complete and inspect the result:

result = future.result()

print(f"Iterations completed : {result.iterations_completed}")
print(f"Points per iteration : {result.num_points}")
print(f"Total acquisitions   : {len(result.uids)}")
print()
print("Run UIDs:")
for uid in result.uids:
    print(f"  {uid}")
Iterations completed : 10
Points per iteration : 1
Total acquisitions   : 10
Run UIDs:
  32fbc221-fe74-4290-8da4-c8bf1359084b
  612b07fd-042d-4625-bddf-a1ecd84ddcd4
  072b67f4-7ee1-45fe-82f2-6175b507ca0a
  5f0fd053-c721-4acc-8e9a-78124675963c
  c16252be-e383-4e8a-9a8e-3864c7ed2bf1
  c0297074-5ce3-4a70-903c-96a177d868c7
  50ddd420-2732-45ba-9abe-7a22d6863289
  a7245027-9bb4-401c-940c-7a7a34e1b3d9
  68871038-21e2-4fd0-a802-a1b2d8053659
  f79e615d-c550-457a-9733-266ad813c5e0
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:32] ax.api.client: Trial 0 marked COMPLETED.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:32] ax.api.client: Generated new trial 1 with parameters {'motor1': -4.074273, 'motor2': 4.15749} using GenerationNode Sobol.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:33] ax.api.client: Trial 1 marked COMPLETED.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:33] ax.api.client: Generated new trial 2 with parameters {'motor1': 1.839365, 'motor2': -3.059355} using GenerationNode Sobol.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:34] ax.api.client: Trial 2 marked COMPLETED.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:34] ax.api.client: Generated new trial 3 with parameters {'motor1': 4.839857, 'motor2': 1.682082} using GenerationNode Sobol.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:35] ax.api.client: Trial 3 marked COMPLETED.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:35] ax.api.client: Generated new trial 4 with parameters {'motor1': -2.372609, 'motor2': -0.593413} using GenerationNode Sobol.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:36] ax.api.client: Trial 4 marked COMPLETED.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:37] ax.api.client: Generated new trial 5 with parameters {'motor1': -3.23326, 'motor2': 0.083762} using GenerationNode MBM.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:38] ax.api.client: Trial 5 marked COMPLETED.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:38] ax.api.client: Generated new trial 6 with parameters {'motor1': -4.672127, 'motor2': -0.882074} using GenerationNode MBM.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:39] ax.api.client: Trial 6 marked COMPLETED.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:40] ax.api.client: Generated new trial 7 with parameters {'motor1': -2.952319, 'motor2': 1.833646} using GenerationNode MBM.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:41] ax.api.client: Trial 7 marked COMPLETED.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:42] ax.api.client: Generated new trial 8 with parameters {'motor1': -2.390378, 'motor2': 1.966985} using GenerationNode MBM.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:42] ax.api.client: Trial 8 marked COMPLETED.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:43] ax.api.client: Generated new trial 9 with parameters {'motor1': -2.560377, 'motor2': 2.648524} using GenerationNode MBM.
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
[INFO 05-19 23:15:43] ax.api.client: Trial 9 marked COMPLETED.

Viewing Results#

Since QueueserverAgent uses the same Ax optimizer backend as the local Agent, all the familiar analysis methods are available:

agent.ax_client.summarize()
/home/runner/work/blop/blop/.pixi/envs/docs/lib/python3.13/site-packages/ax/core/data.py:365: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  self.full_df.groupby(self.DEDUPLICATE_BY_COLUMNS).apply(
trial_index arm_name trial_status generation_node himmelblau motor1 motor2
0 0 0_0 COMPLETED CenterOfSearchSpace 170.000000 0.000000 0.000000
1 1 1_0 COMPLETED Sobol 133.772427 -4.074273 4.157490
2 2 2_0 COMPLETED Sobol 131.610719 1.839365 -3.059355
3 3 3_0 COMPLETED Sobol 199.435458 4.839857 1.682082
4 4 4_0 COMPLETED Sobol 116.939819 -2.372609 -0.593413
5 5 5_0 COMPLETED MBM 104.789759 -3.233260 0.083762
6 6 6_0 COMPLETED MBM 217.617593 -4.672127 -0.882074
7 7 7_0 COMPLETED MBM 43.631569 -2.952319 1.833646
8 8 8_0 COMPLETED MBM 41.501757 -2.390378 1.966985
9 9 9_0 COMPLETED MBM 9.705999 -2.560377 2.648524

The Himmelblau function has four global minima (all with value 0). The optimizer should have made some progress toward these optima.

  • (3.0, 2.0)

  • (-2.805, 3.131)

  • (-3.779, -3.283)

  • (3.584, -1.848)

Cleanup#

When you’re done, close the RE environment and stop the Docker services:

RM.environment_close()
RM.wait_for_idle(timeout=30)
RM.close()
cd docs/source/tutorials/queueserver
docker compose down

What You Learned#

  • Distributed architecture: The queueserver separates experiment execution from optimization logic, connected via ZMQ and Tiled

  • String-based device references: Since devices live in the remote process, DOFs, sensors, and plans are referenced by name

  • Asynchronous operation: agent.run() is non-blocking; the agent reacts to events via ZMQ callbacks

  • Evaluation function: Reads from Tiled (not direct device access) to compute objectives after each plan completes

Next Steps#

  • Add multiple objectives for multi-objective optimization (see KB Mirrors tutorial)

  • Use agent.submit_suggestions() to manually evaluate specific parameter combinations (see Manual Point Injection)

  • Implement outcome_constraints to constrain the optimization (see Set outcome constraints)

  • Add a checkpoint_path to persist optimizer state across restarts (see Agent)