Skip to content

Commit 14dc973

Browse files
Merge pull request #277 from ISISComputingGroup/Ticket152
Make run_number a metadata key in period-per-point mode & document/make it easier to add scan-specific metadata
2 parents 76d89e2 + 07732ab commit 14dc973

File tree

5 files changed

+117
-3
lines changed

5 files changed

+117
-3
lines changed

doc/tutorial/overview.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,50 @@ As this is fairly common functionality for most plans, we have created a "standa
190190
For more information on callbacks, see
191191
[bluesky callbacks documentation](https://blueskyproject.io/bluesky/main/callbacks.html).
192192

193+
## Metadata
194+
195+
Bluesky provides a number of mechanisms for inserting metadata into a scan. In general, metadata may be any JSON-serialisable object including numbers, strings, lists, dictionaries, and nested combinations of those. The bluesky documentation has an {external+bluesky:doc}`extensive description of metadata mechanisms <metadata>`, but the main options are summarised below.
196+
197+
**Persistently (for this python session)**
198+
```python
199+
RE.md["user"] = "Tom"
200+
RE.md["sample"] = "unobtainium"
201+
```
202+
203+
**For one `RE` call**:
204+
```python
205+
RE(some_plan(), sample="unobtainium", user="Tom")
206+
```
207+
208+
**Dynamically, within a plan (using {external+bluesky:py:obj}`bluesky.preprocessors.inject_md_wrapper`)**
209+
```python
210+
import bluesky.plan_stubs as bps
211+
from bluesky.preprocessors import inject_md_wrapper
212+
213+
214+
def some_plan(dae):
215+
run_number = yield from bps.rd(dae.current_or_next_run_number_str)
216+
return (yield from inject_md_wrapper(subplan(), {"run_number": run_number}))
217+
```
218+
219+
**Dynamically, within a plan (using {external+bluesky:py:obj}`bluesky.preprocessors.inject_md_decorator`)**
220+
```python
221+
import bluesky.plan_stubs as bps
222+
from bluesky.preprocessors import inject_md_decorator
223+
224+
225+
def some_plan(dae):
226+
run_number = yield from bps.rd(dae.current_or_next_run_number_str)
227+
228+
@inject_md_decorator({"run_number": run_number})
229+
def _inner():
230+
yield from subplan()
231+
232+
yield from _inner()
233+
```
234+
235+
In addition to the above mechanisms, many built-in bluesky plans (such as {external+bluesky:py:obj}`bluesky.plans.scan` and {py:obj}`ibex_bluesky_core.plans.scan`) take an `md` keyword argument, which can also be used to insert additional metadata for one scan.
236+
193237
## See also
194238

195239
**Plans & plan-stubs**

src/ibex_bluesky_core/plans/__init__.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
]
3232

3333

34+
def _get_additional_md(
35+
dae: "SimpleDae", *, periods: bool, save_run: bool
36+
) -> Generator[Msg, None, dict[str, Any]]:
37+
if periods and save_run:
38+
run_number = yield from bps.rd(dae.current_or_next_run_number_str)
39+
return {"run_number": run_number}
40+
else:
41+
yield from bps.null()
42+
return {}
43+
44+
3445
def scan( # noqa: PLR0913
3546
dae: "SimpleDae",
3647
block: NamedMovable[float],
@@ -42,6 +53,7 @@ def scan( # noqa: PLR0913
4253
periods: bool = True,
4354
save_run: bool = False,
4455
rel: bool = False,
56+
md: dict[Any, Any] | None = None,
4557
) -> Generator[Msg, None, ISISCallbacks]:
4658
"""Scan the DAE against a Movable.
4759
@@ -55,6 +67,7 @@ def scan( # noqa: PLR0913
5567
periods: whether or not to use software periods.
5668
save_run: whether or not to save run.
5769
rel: whether or not to scan around the current position or use absolute positions.
70+
md: Arbitrary metadata to include in this scan.
5871
5972
"""
6073
yield from ensure_connected(dae, block) # type: ignore
@@ -66,13 +79,15 @@ def scan( # noqa: PLR0913
6679

6780
icc = _set_up_fields_and_icc(block, dae, model, periods, save_run, ax)
6881

82+
additional_md = yield from _get_additional_md(dae, periods=periods, save_run=save_run)
83+
6984
@icc
7085
def _inner() -> Generator[Msg, None, None]:
7186
if rel:
7287
plan = bp.rel_scan
7388
else:
7489
plan = bp.scan
75-
yield from plan([dae], block, start, stop, num=num)
90+
yield from plan([dae], block, start, stop, num=num, md=additional_md | (md or {}))
7691

7792
yield from _inner()
7893

@@ -116,6 +131,7 @@ def adaptive_scan( # noqa: PLR0913, PLR0917
116131
periods: bool = True,
117132
save_run: bool = False,
118133
rel: bool = False,
134+
md: dict[Any, Any] | None = None,
119135
) -> Generator[Msg, None, ISISCallbacks]:
120136
"""Scan the DAE against a block using an adaptive scan.
121137
@@ -133,6 +149,7 @@ def adaptive_scan( # noqa: PLR0913, PLR0917
133149
periods: whether or not to use software periods.
134150
save_run: whether or not to save run.
135151
rel: whether or not to scan around the current position or use absolute positions.
152+
md: Arbitrary metadata to include in this scan.
136153
137154
Returns:
138155
an :obj:`ibex_bluesky_core.callbacks.ISISCallbacks` instance.
@@ -148,6 +165,8 @@ def adaptive_scan( # noqa: PLR0913, PLR0917
148165

149166
icc = _set_up_fields_and_icc(block, dae, model, periods, save_run, ax)
150167

168+
additional_md = yield from _get_additional_md(dae, periods=periods, save_run=save_run)
169+
151170
@icc
152171
def _inner() -> Generator[Msg, None, None]:
153172
if rel:
@@ -164,6 +183,7 @@ def _inner() -> Generator[Msg, None, None]:
164183
max_step=max_step,
165184
target_delta=target_delta,
166185
backstep=True,
186+
md=additional_md | (md or {}),
167187
) # type: ignore
168188

169189
yield from _inner()
@@ -185,6 +205,7 @@ def motor_scan( # noqa: PLR0913
185205
periods: bool = True,
186206
save_run: bool = False,
187207
rel: bool = False,
208+
md: dict[Any, Any] | None = None,
188209
) -> Generator[Msg, None, ISISCallbacks]:
189210
"""Wrap our scan() plan and create a block_rw and a DAE object.
190211
@@ -205,6 +226,7 @@ def motor_scan( # noqa: PLR0913
205226
periods: whether or not to use software periods.
206227
save_run: whether or not to save run.
207228
rel: whether or not to scan around the current position or use absolute positions.
229+
md: Arbitrary metadata to include in this scan.
208230
209231
Returns:
210232
an :obj:`ibex_bluesky_core.callbacks.ISISCallbacks` instance.
@@ -231,6 +253,7 @@ def motor_scan( # noqa: PLR0913
231253
save_run=save_run,
232254
periods=periods,
233255
rel=rel,
256+
md=md,
234257
)
235258
)
236259

@@ -251,6 +274,7 @@ def motor_adaptive_scan( # noqa: PLR0913
251274
periods: bool = True,
252275
save_run: bool = False,
253276
rel: bool = False,
277+
md: dict[Any, Any] | None = None,
254278
) -> Generator[Msg, None, ISISCallbacks]:
255279
"""Wrap adaptive_scan() plan and create a block_rw and a DAE object.
256280
@@ -273,6 +297,7 @@ def motor_adaptive_scan( # noqa: PLR0913
273297
periods: whether or not to use software periods.
274298
save_run: whether or not to save run.
275299
rel: whether or not to scan around the current position or use absolute positions.
300+
md: Arbitrary metadata to include in this scan.
276301
277302
Returns:
278303
an :obj:`ibex_bluesky_core.callbacks.ISISCallbacks` instance.
@@ -300,5 +325,6 @@ def motor_adaptive_scan( # noqa: PLR0913
300325
model=model,
301326
save_run=save_run,
302327
rel=rel,
328+
md=md,
303329
)
304330
)

src/ibex_bluesky_core/plans/reflectometry/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Plans specific to Reflectometry beamlines."""
22

33
from collections.abc import Generator
4+
from typing import Any
45

56
from bluesky import Msg
67

@@ -44,6 +45,7 @@ def refl_scan( # noqa: PLR0913
4445
periods: bool = True,
4546
save_run: bool = False,
4647
rel: bool = False,
48+
md: dict[Any, Any] | None = None,
4749
) -> Generator[Msg, None, ISISCallbacks]:
4850
"""Scan over a reflectometry parameter.
4951
@@ -62,6 +64,7 @@ def refl_scan( # noqa: PLR0913
6264
periods: whether to use periods.
6365
save_run: whether to save the run of the scan.
6466
rel: whether to use a relative scan around the current position.
67+
md: Arbitrary metadata to include in this scan.
6568
6669
Returns:
6770
an :obj:`ibex_bluesky_core.callbacks.ISISCallbacks` instance.
@@ -84,6 +87,7 @@ def refl_scan( # noqa: PLR0913
8487
save_run=save_run,
8588
periods=periods,
8689
rel=rel,
90+
md=md,
8791
)
8892
)
8993

@@ -104,6 +108,7 @@ def refl_adaptive_scan( # noqa: PLR0913
104108
periods: bool = True,
105109
save_run: bool = False,
106110
rel: bool = False,
111+
md: dict[Any, Any] | None = None,
107112
) -> Generator[Msg, None, ISISCallbacks]:
108113
"""Perform an adaptive scan over a reflectometry parameter.
109114
@@ -124,6 +129,7 @@ def refl_adaptive_scan( # noqa: PLR0913
124129
periods: whether to use periods.
125130
save_run: whether to save the run of the scan.
126131
rel: whether to use a relative scan around the current position.
132+
md: Arbitrary metadata to include in this scan.
127133
128134
Returns:
129135
an :obj:`ibex_bluesky_core.callbacks.ISISCallbacks` instance.
@@ -147,5 +153,6 @@ def refl_adaptive_scan( # noqa: PLR0913
147153
model=model,
148154
save_run=save_run,
149155
rel=rel,
156+
md=md,
150157
)
151158
)

src/ibex_bluesky_core/plans/reflectometry/_det_map_align.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def angle_scan_plan(
172172
*,
173173
angle_map: npt.NDArray[np.float64],
174174
flood: sc.Variable | None = None,
175+
md: dict[Any, Any] | None = None,
175176
) -> Generator[Msg, None, ModelResult | None]:
176177
"""Reflectometry detector-mapping angle alignment plan.
177178
@@ -187,6 +188,7 @@ def angle_scan_plan(
187188
This array should be aligned along a "spectrum" dimension; counts are
188189
divided by this array before being used in fits. This is used to
189190
normalise the intensities detected by each detector pixel.
191+
md: Arbitrary metadata to include in this scan.
190192
191193
"""
192194
logger.info("Starting angle scan")
@@ -212,7 +214,7 @@ def angle_scan_plan(
212214
]
213215
)
214216
def _inner() -> Generator[Msg, None, None]:
215-
yield from bp.count([dae])
217+
yield from bp.count([dae], md=md)
216218

217219
yield from _inner()
218220

@@ -247,6 +249,7 @@ def height_and_angle_scan_plan( # noqa PLR0913
247249
angle_map: npt.NDArray[np.float64],
248250
rel: bool = False,
249251
flood: sc.Variable | None = None,
252+
md: dict[Any, Any] | None = None,
250253
) -> Generator[Msg, None, DetMapAlignResult]:
251254
"""Reflectometry detector-mapping simultaneous height & angle alignment plan.
252255
@@ -266,6 +269,7 @@ def height_and_angle_scan_plan( # noqa PLR0913
266269
This array should be aligned along a "spectrum" dimension; counts are
267270
divided by this array before being used in fits. This is used to
268271
normalise the intensities detected by each detector pixel.
272+
md: Arbitrary metadata to include in this scan.
269273
270274
Returns:
271275
A dictionary containing the fit results from gaussian height and angle fits.
@@ -309,7 +313,7 @@ def _inner() -> Generator[Msg, None, None]:
309313
nonlocal start, stop, num
310314
yield from bps.mv(dae.number_of_periods, num)
311315
plan = bp.rel_scan if rel else bp.scan
312-
yield from plan([dae], height, start, stop, num=num)
316+
yield from plan([dae], height, start, stop, num=num, md=md)
313317

314318
yield from _inner()
315319

tests/plans/test_init.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# pyright: reportMissingParameterType=false
2+
import functools
3+
from typing import Any
24
from unittest.mock import patch
35

6+
import bluesky.utils
47
import pytest
58
from bluesky.preprocessors import run_decorator
69
from ophyd_async.plan_stubs import ensure_connected
@@ -121,6 +124,36 @@ def test_scan_does_normal_scan_when_relative_false(RE, dae, block):
121124
assert count == bp_scan.call_args[1]["num"]
122125

123126

127+
@pytest.mark.parametrize(
128+
"scan_func",
129+
[
130+
functools.partial(scan, start=1, stop=2, num=2),
131+
functools.partial(adaptive_scan, start=1, stop=2, min_step=1, max_step=2, target_delta=1),
132+
],
133+
)
134+
def test_if_in_periods_mode_and_run_saved_then_scan_start_doc_contains_run_number(
135+
RE, dae, block, scan_func
136+
):
137+
set_mock_value(dae.current_or_next_run_number_str, "12345678")
138+
139+
start_doc: dict[str, Any] | None = None
140+
141+
def _cb(typ, doc):
142+
if typ == "start":
143+
nonlocal start_doc
144+
start_doc = doc
145+
146+
with (
147+
patch("ibex_bluesky_core.plans.ensure_connected"),
148+
):
149+
# Scan fails because DAE isn't set up right... but it still emits a start doc so that's fine
150+
with pytest.raises(bluesky.utils.FailedStatus):
151+
RE(scan_func(dae, block, rel=False, periods=True, save_run=True), _cb)
152+
153+
assert start_doc is not None
154+
assert start_doc.get("run_number") == "12345678"
155+
156+
124157
def test_scan_does_relative_scan_when_relative_true(RE, dae, block):
125158
start = 0
126159
stop = 2

0 commit comments

Comments
 (0)