Skip to content

Commit 5bfccaf

Browse files
committed
Merge remote-tracking branch 'origin/main' into redefine
2 parents 4f3666f + b36f8f1 commit 5bfccaf

File tree

9 files changed

+702
-9
lines changed

9 files changed

+702
-9
lines changed
-42.1 KB
Binary file not shown.

doc/plan_stubs/user_input.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# User input helpers
2+
3+
## `prompt_user_for_choice`
4+
5+
The {py:obj}`ibex_bluesky_core.plan_stubs.prompt_user_for_choice` plan stub can be used to ask
6+
the user for a constrained set of input choices. This will continue asking if a choice is not valid.
7+
8+
For example:
9+
10+
```python
11+
from ibex_bluesky_core.plan_stubs import prompt_user_for_choice
12+
13+
14+
def plan():
15+
...
16+
answer = yield from prompt_user_for_choice(prompt="Do a or b?", choices=["a", "b"])
17+
18+
if answer == "a":
19+
yield from plan_a()
20+
elif answer == "b":
21+
yield from plan_b()
22+
```
23+
24+
If a user types "c" in this example as their answer, they will be prompted again.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Auto-alignment
2+
3+
For reflectometers, we provide a few generic plans which can be used to align beamlines.
4+
5+
## Full-Auto Alignment Plan
6+
7+
The {py:obj}`ibex_bluesky_core.plans.reflectometry.optimise_axis_against_intensity` plan is designed to scan over a movable against beam intensity, and given which fitting parameter is chosen to be optimised and the fitting method, it will aim to find an optimum value. See [`standard fits`](../../fitting/standard_fits.md) for the fitting parameters for each fitting model you can use. At this stage it will check if the value is 'sensible'- there are some default checks but the user is encouraged to provide their own checks. If found to be sensible, the motor will be moved to this value, otherwise it will optionally run a callback that can be provided, and then ask the user if they want to rescan or conttinue to move the movable.
8+
9+
The idea for how we expect the main auto-alignment plan to be used is that at the top / instrument level, you will move all other movables to some position ready to align, yield from this plan and then re-zero the motor.
10+
The following is how you would do this for a reflectometry parameter as your movable:
11+
12+
```python
13+
14+
def full_autoalign_plan() -> Generator[Msg, None, None]:
15+
16+
det_pixels = centred_pixel(DEFAULT_DET, PIXEL_RANGE)
17+
dae = monitor_normalising_dae(
18+
det_pixels=det_pixels,
19+
frames=FRAMES,
20+
periods=PERIODS,
21+
save_run=SAVE_RUN,
22+
monitor=DEFAULT_MON,
23+
)
24+
25+
s1vg = ReflParameter(prefix=PREFIX, name="S1VG", changing_timeout_s=60)
26+
# Interfaces with the reflectometry server
27+
28+
yield from ensure_connected(s1vg, dae)
29+
30+
print("Starting auto-alignment...")
31+
32+
# S1VG
33+
34+
yield from bps.mv(s1vg, -0.1)
35+
yield from optimise_axis_against_intensity(
36+
dae=dae,
37+
alignment_param=s1vg,
38+
fit_method=SlitScan.fit(), # What form of data do you expect
39+
fit_param="inflection0", # Which fitting paramater do you want to optimise
40+
rel_scan_ranges=[0.3, 0.05], # Scan with range of 0.3, then 0.05
41+
num_points=10, # Number of points in a scan.
42+
)
43+
yield from bps.mv(s1vg.redefine, 0.0) # Redefine current motor position to be 0
44+
45+
# Other params
46+
# ....
47+
48+
print("Auto alignment complete.")
49+
50+
```
51+
52+
As mentioned prior, it is recommended that for each movable you want to align, you should provide a checking function, to make sure that for the value you receive for the chosen fitting paramater e.g centre of a gaussian, is sensible. If the optimised value is not found to be sensible then you will have the option to either type 1 and restart the alignment for this movable, or press 2 and continue with moving the movable to the found value. The following is how you would make a check function with the right signature and pass it to {py:obj}`ibex_bluesky_core.plans.reflectometry.optimise_axis_against_intensity`:
53+
54+
```python
55+
56+
def s1vg_checks(result: ModelResult, alignment_param_value: float) -> str | None: # Must take a ModelResult and a float
57+
"""Check for optimised S1VG value. Returns True if sensible."""
58+
rsquared_confidence = 0.9
59+
expected_param_value = 1.0
60+
expected_param_value_tol = 0.1
61+
max_peak_factor = 5.0
62+
63+
# Check that r-squared is above a tolerance
64+
if result.rsquared < rsquared_confidence:
65+
return "R-squared below confidence level."
66+
67+
# For S1VG, provide a value you would expect for it,
68+
# assert if its not close by a provided factor
69+
if not isclose(expected_param_value, alignment_param_value, abs_tol=expected_param_value_tol):
70+
return "Optimised value is not close to expected value."
71+
72+
# Is the peak above the background by some factor (optional because param may not be for
73+
# a peak, or background may not be a parameter in the model).
74+
if result.values["background"] / result.model.func(alignment_param_value) <= max_peak_factor:
75+
return "Peak was not above the background by factor."
76+
77+
# Everything is fine so return None
78+
return None
79+
80+
# ...
81+
82+
def plan():
83+
yield from optimise_axis_against_intensity(
84+
dae=dae,
85+
alignment_param=s1vg,
86+
fit_method=SlitScan.fit(),
87+
fit_param="inflection0",
88+
rel_scan_ranges=[0.3, 0.05], # Scan with range of 0.3, then 0.05
89+
num_points=10,
90+
is_good_fit=s1vg_checks # Pass s1vg_checks
91+
)
92+
93+
```
94+
95+
To determine what to do in the event of a value being "invalid" you can use a plan like so:
96+
97+
```python
98+
from ibex_bluesky_core.plans.reflectometry import optimise_axis_against_intensity
99+
from ibex_bluesky_core.callbacks.fitting.fitting_utils import SlitScan
100+
from typing import Generator
101+
from bluesky.utils import Msg
102+
import bluesky.plan_stubs as bps
103+
104+
105+
def problem_found_plan() -> Generator[Msg, None, None]:
106+
# This must be a plan, if it doesn't otherwise yield, make it a plan
107+
# by yielding from bps.null()
108+
yield from bps.null()
109+
print("There was a problem - oh no!")
110+
111+
def plan():
112+
yield from optimise_axis_against_intensity(
113+
dae=dae,
114+
alignment_param=s1vg,
115+
fit_method=SlitScan.fit(),
116+
fit_param="inflection0",
117+
rel_scan_ranges=[0.1],
118+
num_points=1, # Fit will never converge with just 1 point which should cause a "problem".
119+
problem_found_plan=problem_found_plan
120+
)
121+
```

src/ibex_bluesky_core/plan_stubs/__init__.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@
2121
CALL_QT_AWARE_MSG_KEY = "ibex_bluesky_core_call_qt_aware"
2222

2323

24-
__all__ = ["call_qt_aware", "call_sync", "redefine_motor", "redefine_refl_parameter"]
24+
__all__ = [
25+
"call_qt_aware",
26+
"call_sync",
27+
"prompt_user_for_choice",
28+
"redefine_motor",
29+
"redefine_refl_parameter",
30+
]
2531

2632

2733
def call_sync(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Generator[Msg, None, T]:
@@ -124,3 +130,21 @@ def redefine_refl_parameter(
124130
"""
125131
logger.info("Redefining refl parameter %s to %s", parameter.name, position)
126132
yield from bps.mv(parameter.redefine, position)
133+
134+
135+
def prompt_user_for_choice(*, prompt: str, choices: list[str]) -> Generator[Msg, None, str]:
136+
"""Prompt the user to choose between a limited set of options.
137+
138+
Args:
139+
prompt: The user prompt string.
140+
choices: A list of allowable choices.
141+
142+
Returns:
143+
One of the entries in the choices list.
144+
145+
"""
146+
choice = yield from call_sync(input, prompt)
147+
while choice not in choices:
148+
choice = yield from call_sync(input, prompt)
149+
150+
return choice

src/ibex_bluesky_core/plans/reflectometry/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
adaptive_scan,
1212
scan,
1313
)
14+
from ibex_bluesky_core.plans.reflectometry._autoalign import optimise_axis_against_intensity
1415
from ibex_bluesky_core.plans.reflectometry._det_map_align import (
1516
DetMapAlignResult,
1617
angle_scan_plan,
@@ -22,6 +23,7 @@
2223
"DetMapAlignResult",
2324
"angle_scan_plan",
2425
"height_and_angle_scan_plan",
26+
"optimise_axis_against_intensity",
2527
"refl_adaptive_scan",
2628
"refl_scan",
2729
]

0 commit comments

Comments
 (0)