Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d1e22a1
eod
MTakahashi-KWH Apr 29, 2026
1daa8a8
[ENH] first "realization", onto sandbox
MTakahashi-KWH Apr 30, 2026
35d153b
add submodule init
MTakahashi-KWH Apr 30, 2026
ea6bfd7
fix
MTakahashi-KWH Apr 30, 2026
8695716
bugfixes
MTakahashi-KWH Apr 30, 2026
1c0083b
do more than one iteration
MTakahashi-KWH Apr 30, 2026
362f235
[IENH] added dual annealing support and cleaned worker prep
MTakahashi-KWH May 1, 2026
a8ee1e1
ruff fixes
MTakahashi-KWH May 4, 2026
0934ca8
scipy opt best_points and axis rescaling
MTakahashi-KWH May 14, 2026
cf36fb7
cleaning of print statements
MTakahashi-KWH May 14, 2026
d2df4d4
fixes to dual annealing
MTakahashi-KWH May 15, 2026
0e58f1f
updates to allow multi threaded optimizer sampling, more to come
MTakahashi-KWH Jun 22, 2026
f212b24
initial test implementations
MTakahashi-KWH Jun 22, 2026
99bfbad
ruff fixes
MTakahashi-KWH Jun 22, 2026
4534412
first unit test fixes
MTakahashi-KWH Jun 22, 2026
4665260
final test fixes for the day
MTakahashi-KWH Jun 22, 2026
f35ba09
ruff
MTakahashi-KWH Jun 22, 2026
f4bb9cd
bug fixes and code seperation. Continuing work on multi sampling
MTakahashi-KWH Jun 24, 2026
e2366bf
tiny patch to test now working multithread and imrove thread closure …
MTakahashi-KWH Jun 24, 2026
69a1be4
removed debug prints
MTakahashi-KWH Jun 24, 2026
aa8f365
GO MY BOTS, BUILD UNIT TESTS
MTakahashi-KWH Jun 24, 2026
b6bec4e
ruff fixes
MTakahashi-KWH Jun 24, 2026
0b254f3
strenum flaky in 3.11 and race condition fix on resessioning
MTakahashi-KWH Jun 24, 2026
e381a13
fix lint check from throwing error on backwards compat fix
MTakahashi-KWH Jun 25, 2026
cd36fe4
added tutorial for gradient
MTakahashi-KWH Jun 25, 2026
4e07388
lint fixes and new optimizer
MTakahashi-KWH Jun 25, 2026
2b5acd7
dual annealing qol for best points, better tutorail visuals and agent…
MTakahashi-KWH Jun 25, 2026
59f24cb
3.11 fix v2
MTakahashi-KWH Jun 25, 2026
7bfb29e
juytext fix?
MTakahashi-KWH Jun 25, 2026
965af43
attempt doc fixes
MTakahashi-KWH Jun 25, 2026
2203342
3.11 fix
MTakahashi-KWH Jun 25, 2026
b9a8f35
fix for 3.11 tests
MTakahashi-KWH Jun 25, 2026
cb5ccbc
seperated scipy and optimizer tests
MTakahashi-KWH Jun 25, 2026
7ec7761
ruff
MTakahashi-KWH Jun 25, 2026
e0f1ecf
edit to default resiliance in agent and cleaning of active queue with…
MTakahashi-KWH Jun 25, 2026
5eb1021
ruff
MTakahashi-KWH Jun 25, 2026
d969aff
cheeky shift to dual annealing for code cov
MTakahashi-KWH Jun 25, 2026
66b416a
ruff woke up angry this morning
MTakahashi-KWH Jul 22, 2026
5e427f4
added lot more optimizers
MTakahashi-KWH Jul 22, 2026
1666ebc
added SHGO
MTakahashi-KWH Jul 22, 2026
ca6818f
python 3.11 again doing its complaints (fix?)
MTakahashi-KWH Jul 22, 2026
c1487ba
Apply suggestion from @thopkins32
MTakahashi-KWH Jul 28, 2026
563f328
renaming and reorganizing of additions
MTakahashi-KWH Jul 29, 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
1 change: 1 addition & 0 deletions docs/source/tutorials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Tutorials
:maxdepth: 1

tutorials/simple-experiment.md
tutorials/gradient-optimization.md
tutorials/queueserver.md
tutorials/xrt-demo.md
tutorials/xrt-kb-mirrors.md
246 changes: 246 additions & 0 deletions docs/source/tutorials/gradient-optimization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.17.3
kernelspec:
display_name: dev
language: python
name: python3
---

# Your first Scipy optimization with Blop

In this tutorial, you will learn the three core concepts of Blop: **DOFs** (the parameters you can adjust), **objectives** (what you want to optimize), and the **Agent** (which coordinates the optimization). We'll optimize a simple mathematical function using simulated devices—the same patterns apply to real hardware.

## Setup

First, let's import what we need and start the data infrastructure:

```{code-cell} ipython3
import logging
import time
import warnings
from typing import Any

from bluesky.protocols import HasHints, HasParent, Hints, NamedMovable, Readable, Status
from bluesky.run_engine import RunEngine
from bluesky_tiled_plugins import TiledWriter
from tiled.client import from_uri
from tiled.client.container import Container
from tiled.server import SimpleTiledServer

from blop.ax import Objective, RangeDOF
from blop.gradient import SCP, Scipy, ScipyCFG

# Suppress noisy logs from httpx
logging.getLogger("httpx").setLevel(logging.WARNING)
```

```{code-cell} ipython3
# Start a local Tiled server for data storage
tiled_server = SimpleTiledServer()

# Set up the Bluesky RunEngine and connect it to Tiled
RE = RunEngine({})
tiled_client = from_uri(tiled_server.uri)
tiled_writer = TiledWriter(tiled_client)
RE.subscribe(tiled_writer)
```

## Creating simulated devices

Bluesky controls devices through protocols. For this tutorial, we create simple simulated "movable" devices. In real experiments, you would use [Ophyd](https://blueskyproject.io/ophyd-async) devices or similar—the code below is just boilerplate to simulate hardware:

```{code-cell} ipython3
class AlwaysSuccessfulStatus(Status):
def add_callback(self, callback) -> None:
callback(self)

def exception(self, timeout=0.0):
return None

@property
def done(self) -> bool:
return True

@property
def success(self) -> bool:
return True


class ReadableSignal(Readable, HasHints, HasParent):
def __init__(self, name: str) -> None:
self._name = name
self._value = 0.0

@property
def name(self) -> str:
return self._name

@property
def hints(self) -> Hints:
return {"fields": [self._name], "dimensions": [], "gridding": "rectilinear"}

@property
def parent(self) -> Any | None:
return None

def read(self):
return {self._name: {"value": self._value, "timestamp": time.time()}}

def describe(self):
return {self._name: {"source": self._name, "dtype": "number", "shape": []}}


class MovableSignal(ReadableSignal, NamedMovable):
def __init__(self, name: str, initial_value: float = 0.0) -> None:
super().__init__(name)
self._value: float = initial_value

def set(self, value: float) -> Status:
self._value = value
return AlwaysSuccessfulStatus()
```

## Defining DOFs and objectives

**DOFs** (degrees of freedom) are the parameters the optimizer can adjust. **Objectives** are what you want to optimize. Here we define two DOFs (`x1` and `x2`) that can range from -5 to 5, and one objective (the Himmelblau function) that we want to minimize:

```{code-cell} ipython3
x1 = MovableSignal("x1", initial_value=0.1)
x2 = MovableSignal("x2", initial_value=0.23)

dofs = [
RangeDOF(actuator=x1, bounds=(-5, 5), parameter_type="float"),
RangeDOF(actuator=x2, bounds=(-5, 5), parameter_type="float"),
]
objectives = [
Objective(name="himmelblau_2d", minimize=True),
]
sensors = []
```

## Writing the evaluation function

The **evaluation function** computes objective values from experimental data. After each run, Blop calls this function with the run's unique ID and the suggestions that were tried. It returns the computed objective values:

```{code-cell} ipython3
class Himmelblau2DEvaluation:
def __init__(self, tiled_client: Container):
self.tiled_client = tiled_client

def __call__(self, uid: str, suggestions: list[dict]) -> list[dict]:
run = self.tiled_client[uid]
outcomes = []
reordered_suggestions = run.start["blop_suggestions"]
x1_data = run["primary/x1"].read()
x2_data = run["primary/x2"].read()

print(
"[Himmelblau] evaluating suggestions: ",
[s["_id"] for s in suggestions],
" reordered to: ",
[s["_id"] for s in reordered_suggestions],
)
for index, suggestion in enumerate(reordered_suggestions):
# Special key to identify a suggestion
suggestion_id = suggestion["_id"]
x1 = x1_data[index]
x2 = x2_data[index]
# Himmelblau function: has four global minima where value = 0
outcomes.append({"himmelblau_2d": (x1**2 + x2 - 11) ** 2 + (x1 + x2**2 - 7) ** 2, "_id": suggestion_id})

return outcomes
```

## Running the optimization

The **Agent** brings everything together. Create one with your DOFs, objectives, and evaluation function, then run the optimization:

```{code-cell} ipython3
agent = Scipy.Agent(
sensors=sensors,
dofs=dofs,
objectives=objectives,
evaluation_function=Himmelblau2DEvaluation(tiled_client=tiled_client),
name="simple-experiment",
description="A simple experiment optimizing the Himmelblau function",
)

RE(agent.optimize(10))
```

## Configuring the optimization

Sometimes a default **Agent** optimization may not do all that you'd like. We expose a configuration object called ScipyCFG and a pure scipy interface so that the classic parameters of scipy minimize can be tweaked (and some multipoint sampling can be used).

```{code-cell} ipython3
config = ScipyCFG(dofs=dofs, objective=objectives[0], optimizer=SCP.Dual_Annealing, threads=4, max_iter=2, eps=0.1)
agent = Scipy(
sensors=sensors,
config=config,
evaluation_function=Himmelblau2DEvaluation(tiled_client=tiled_client),
name="test_experiment",
)
res_uid = RE(agent.optimize(20, n_points=2))
```

## Viewing the results

Scipy is a local optimizer so it doesn't have internal point tracking, but we can to grab it from our datastore.

```{code-cell} ipython3
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

res_client = tiled_client[res_uid[0]]
data = res_client["primary/internal"].read()
cols = ["suggestion_ids", "x1", "x2", "himmelblau_2d"]
vec = data[cols]
res = []
for _, row in vec.iterrows():
vic = [row.suggestion_ids, row.x1, row.x2, row.himmelblau_2d]
vic = [x.strip("[]").split() for x in vic]
for id, x, y, obj in zip(*vic, strict=True):
if id != "''":
res.append([int(id.strip("'")), float(x), float(y), float(obj)])
res = np.array(res)

fig, ax = plt.subplots(figsize=(12, 8))

xb, yb = np.random.uniform(-5, 5, (2, 1000))
ax.tripcolor(xb, yb, (xb**2 + yb - 11) ** 2 + (xb + yb**2 - 7) ** 2, shading="gouraud")

i, x, y, z = res.T
ps = ax.scatter(x, y, c=range(len(x)), cmap="plasma", s=50)
plt.colorbar(ps).set_label("sample index")
plt.title("Visualizing Scipy's traversal of Himmelblau")
```

Seeing the sample history

```{code-cell} ipython3
pd.DataFrame(data=res, columns=cols)
```

```{code-cell} ipython3
print(agent.get_best_points())
```

The Himmelblau function has four global minima (all with value 0). The `summarize` output shows which one(s) the optimizer found.

## What you learned

You now understand the three core concepts of Blop:

- **DOFs**: The parameters the optimizer adjusts (here, `x1` and `x2` with bounds)
- **Objectives**: What you're optimizing (here, minimizing the Himmelblau function)
- **Agent**: Coordinates the optimization loop between Bluesky and the evaluation function

## Next steps

For a more comprehensive tutorial with multiple objectives and diagnostic tools, see [Optimizing KB Mirrors](./xrt-kb-mirrors.md).
6 changes: 6 additions & 0 deletions src/blop/scipy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Scipy Backend for Pertubative gradient and in house global optimizers."""
Comment thread
thopkins32 marked this conversation as resolved.

from .optimizer import SCP, ScipyCFG, ScipyOptimizer
from .scipy import Scipy

__all__ = ["SCP", "ScipyCFG", "Scipy", "ScipyOptimizer"]
Loading
Loading