Skip to content

Commit 279e583

Browse files
dan-fernandescallumforresterDiamondRCrootroot
authored
230 bimorph optimisation plan (#1027)
* Change BimorphMirror.on_off datatype to BimorphMirrorOnOff rather than StrictEnum * Add docstrings * Remove uneccesary signals from BimorphMirror * Add missing attribute to docstring * Remove duplicate signal decleration * Remove uneccesary BimorphMirror signal * Change BimorphMirrorChannel.status to datatype BimorphMirrorOnOff * Add BimorphMirrorStatus, set as BimorphMirror.status datatype * Change BimorphMirror.channels datatype to str * Change BimorphMirrorChannel to declare all signals declaratively * Remove uneccesary argument * Move all PV delimiters from suffix into prefix * Fix incorrect use of PVSuffix * Make BimorphMirrorChannel inherit from EpicsDevice * Remove :CHANNELS signal from BimorphMirror * Rename BimorphMirror.channel_list to .channels * Remove unused import * Replace BimorphMirror.set wait on rbv with wait=True * Add bimorph test_set_channels * Organise imports * Make wait_for_value in BimorphMirror.set use DEFAULT_TIMEOUT instead of None * Rename test_set_channels, fix said test, and add test_set_channels_triggers_alltrgt_proc * Remove whitespace * Remove unused import * Add comments * Parametrize test_set_channels_wait_for_readback * Abstract setting vout mock values into fixture * Parametrize test_set_channels_wait_for_readback * Add whitespace * Renam set_vout_mock_values to set_vout_mock_and_return_value * Add test_set_channels_waits_for_vout_readback unimplemented * Add test_read, and BIMORPH_NAME global variable * Make mirror fixture take number_of_channels argument * Correct BimorphMirrorChannel Format signal types * Add valid_bimorph_values fixture * Change test parametrization from bimorph input to number of channels * Add format changes generated by tox pre-commit * Change explcit channel number parameter to reference to global list of channel numbers * Implement test_set_channels_waits_for_vout_readback * Add test_set_invalid_chanel_throws_error * Add Raises section to BimorphMirror.set docstring * Make BiorphMirror.__init__ number_of_channels have no default value * Replace all asserts inside for loops, to comprehension and comparison * Remove BIMORPH_NAME global variable * Remove prng from test input generation * Add BimorphMirror.__init__ number_of_channels validation * Add test_init_mirror_with_invalid_channels_throws_error * Add BimorphMirror.__init__ docstring * Replace assertion in BimorphMirror.set arg validation with raise ValueError * Make test_set_invalid_channel_throws_error expect correct error type * Replace real PV prefix with fake * Add mock_vtrgt_vout_propogation, remove set_vout_mock fixtures * Remove unused import * Fix import for ophyd async 0.9.0a1 * Update src/dodal/devices/bimorph_mirror.py Co-authored-by: Callum Forrester <callum.forrester@diamond.ac.uk> * Update src/dodal/devices/bimorph_mirror.py Co-authored-by: Callum Forrester <callum.forrester@diamond.ac.uk> * Update src/dodal/devices/bimorph_mirror.py Co-authored-by: Callum Forrester <callum.forrester@diamond.ac.uk> * Remove BimorphMirror.number_of_channels * Rename BimorphMirror.alltrgt_proc, .on_off for readability * Rename BimorphMirrorChannel.vtrgt, .vout for readbility * Change BimorphMirror.set type hinting * Replace BimorphMirror.set sequential input validation with collection Co-authored-by: Callum Forrester <callum.forrester@diamond.ac.uk> * Add test_init_mirror_with_zero_channels * Ensure bimorph has finished moving after set * wait for value * Update test_bimorph_mirror.py * Crop PV name for BUSY toggle * Crop PV name for BUSY toggle * await IDLE status * Update test to account for idling * Make BimorphMirrorChannel Movable, add set method * Remove unnecessary fixture from test_bimorph_mirror_channel_set * Write bimorph_optimisation plan * Make BimorphMirror.set set to target_voltage in serial, and wait check for mirror status. * Add wait for BimorphMirrorStatus.IDLE before trigger in BimorphMirror.set * Refactor mock_vtrgt_vout_propogation to use callback_on_mock_put * Add mock_bimorph_mirror_status_functionality * Aggregate mock_vtrgt_vout_propogation, mock_bimorph_mirror_status_functionality, into single bimorph_functionality fixture * Make test_set_channels_waits_for_vout_readback check subset of call_args rather than equality * Add bimorph_functionality fixture to test_set_channels_allows_tolerance * Remove tolerance in BimorphMirror.set * Remove unused argument in BimorphMirror.set * Add comment to BimorphMirror.set explaining serial set * Refactor test_bimorph_mirror to use mirror_with_mocked_put fixture * Fix SlitDimension X datatype * Fix move_slits dimension comparison * Add test_birmorph.py with test_move_slits * Add docstring to slits fixture * Add oav fixture, test_bimorph_optimisaiton skeleton * Add parameters to test_bimorph_optimisation, add initial_voltage_list fixture * Fix not awaiting coroutine * Add plan execution to test_bimorph_optimisation * Add mock velocity value to slits fixture, remove breakpoint * Fix outer func decorators * Instantiate SimDetector in DeviceCollector context * Fix bps.trigger_and_read parameters * Fix oav fixture, add move_slits_message_generator, inner_scan_message_generator * Add mirror fixture * Add setup_message_generator, outer_message_generator * Add mirror_with_mocked_put fixture * Refactor capture_bimorph_state, restore_bimorph_state out of plan * Remove unnecessary comment * Add test_save_and_restore * Add test_inner_scan * Comlete test_bimorph_optimisation test * Change test parameters * Add whitespace * Add typing to mirror_with_mocked_put status function * Add type hinting to mirror_with_mocked_put vout_propogation_and_status function * Rename all ..vout.. references to ..output_voltage.. * Make BimorphMirror.set use set_and_wait_for_other_value rather than two seperate calls * Rewrite mirror_with_mocked_put to not use default arguments in helper functions, add docstring * Renamed functions in mirror_with_mocked_put fixture for readability * Refactor test_bimorph_optimisation to be method in TestBimorphOptimisation class * Refactor mocks in TestBimorphOptimisation to be class parametrization. Add run_scan class fixture. * Add start_state and mock_capture_bimorph_state fixtures, add test_settle_time_called and test_bimorph_state_restored tests * Add TestOptimisation.test_bimorph_puts * Rename TestOptimisation.test_bimorph_puts -> .test_plan_puts_to_bimorph * Add TestBimorphOptimisation.test_plan_calls_inner_scan * Add TestBimorphOptimisation.test_plan_sets_mirror_start_position * Rename TestBimorphOptimisation.test -> .test_bimorph_state_captured * Change placement of initial_voltage_list logic in multiple tests * Refactor TestBimorphOptimisation.mock_capture_bimorph_state for readability * Add docstring to TestBimorphOptimisation * Add type hinting where lacking * Set DEFAULT_TIMEOUT to 60 * Change BimorphMirror.set to write to output_voltage and to not trigger commit_target_voltages * Remove BimorphMirror.commit_target_voltages * Remove test_set_channels_triggers_alltrgt_proc * Change test_set_channels_waits_for_readback to read from output_voltage * Make test_set_one_channel read from output_voltage * Rewrite inner_scan, outer docstrings * Replace oav parameter with detectors readable list * Add detectors fixture, make test_inner_scan use fixture * Have test_inner_scan assert new dd mock_move_slitcall_args_list rather than mock device * Refactor test_inner_scan to TestInnerScan.test_inner_scan_moves_slits * Add TestInnerScan.test_inner_scan_triggers_and_reads * Fix @stage_deecorator parameters * Make all TestBimorphOptimisation tests use detectors fixture rather than oav * Add run_metadata param to inner_scan * Replace all DeviceCollector with init_devices * Correct import * Add missing * Add close_run calls * Move open_run to start of plan, fix indentation error * Replace plans wrapping inner_scan with pure inner_scan as it is now a full plan * In outer() use new dict for each run_metadata * Fix inner_run parameters * Add TestInnerScan.test_inner_scan_writes_run_metadata, and run_metadata fixture * Reduce fixture permutations * Add TestIntegration * Make inner and outer create nested runs * Remove mock=True from oav fixture * Add prepare detectors in inner_scan * Add mock=True to oav fixture * Remove mock=True from OAV fixture * Set number_of_triggers in detector preperation to 1 * Remove outer run * Use different stream for each pencil beam scan * Add stream_name * Remove commented out code * Update inner_scan docstring * Replace TestInnerScan fixture and params from scan_metadata to stream_name * Fix TestInnerScan.test_inner_scan_triggers_and_reads * Remove uneccesary variable assignment * Prepare detectors before first declare_stream * Remove run_metadata * Add metadata to outer_scan run * Add TestBimorphOptimisation.test_metadata * Add validate_bimorph_plan, check_valid_bimorph_state * Add bimorph plan validation * Add status wait to channel set * Make BimorphMirrorChannel.set literally just set target voltage * Change bimorph_optimisation to move bimorph via full mirror, rather than each channel * Change BimorphMirrorChannel.set to set output_voltage, not target_voltage * Have restore_bimorph_state move bimorph via mirror, not individual channels * Change BimorphMirror.set to accept array param rather than mapping (cherry picked from commit 38f3a904e323fe4f85bca38ae31c9283c082024e) * Change BimorphMirror.set to set in parallel * Add BimorphMirror.commit_target_voltages * Update tests to reflect BimorphMirror.set interface change (cherry picked from commit 602d07265d50429f74f30cf25d449a2fa78f802b) * Replace DeviceCollector with init_devices * Remove test_set_one_channel * Fix syntax * Make bimorph_optimisation move mirror with array rather than dict * Mak restore_bimorph_state write to mirror with list rather than dict * Fix test_save_and_restore fixture * Add configurable slit settle time * Add move to start voltage list before first inner_scan * Add slit settle time after move to initial position * Add bimorph settle time after moving into pos * Move bimorph starting position out of outer scan * Move all start position movements into outer_scan * Add test_bimorph_position_generator, fix TestBimorphOptimisation.test_settle_time_called * Add bimorph_position_generator * Make bimorph_position_generator return copy of internal list * Fix bimorph_position_generator returning internal list * Add TestBimorphPositionGenerator * Rework bimorph_position_generator to reduce copying mutables * Remove BimorphMirrorChannel.set * Add TestPlanValidation * Fix bimorph_optimisation type ignore * Fix type checking * Replace PatternDetector with SimBlobDetector * Fix BimorphMirror inheritance * Improve test speeds * Fix (badly) initial_voltage_list * Fix test_copies_list positions * Export bimorph_optimisation plan * Remove uneccesary default argument * Make SlitDimension inherit from Enum and str * Remove merge conflict, fix BimorphMirror signature * Change Enum to StrEnum * Fix message in test_move_slits * Replace StrEnum with str, Enum * Make bimorph_optimisation accept metadata * Mark test_full_plan as skip * Remove skipped test * Reimplement plan metadata to match standard * Fix test_metadata * Fix async issue in test_metadata * Fix bimorph_optimisation docstring * Change I22's vertical focusing mirror to 32 channel * Revert "Change I22's vertical focusing mirror to 32 channel" This reverts commit 3f83c78. * Remove uneccesary type ignores * Remove bimorph_optimisation from plan __init__ __all__ * Change validate_bimorph_plan exception to ValueError * Rename test_invalid_plan to test_invalid_initial_voltage_list * Add TestPlanValidation.test_late_invalid_plan * Change TestPlanValidation.test_invalid_initial_voltage_list to use paramtrized variables --------- Co-authored-by: Callum Forrester <callum.forrester@diamond.ac.uk> Co-authored-by: Richard Cunningham <richard.cunningham@diamond.ac.uk> Co-authored-by: root <root@ws540.diamond.ac.uk> Co-authored-by: root <root@i22-ws001.diamond.ac.uk>
1 parent 5ed3646 commit 279e583

File tree

2 files changed

+1126
-0
lines changed

2 files changed

+1126
-0
lines changed

src/dodal/plans/bimorph.py

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
from collections.abc import Generator
2+
from dataclasses import dataclass
3+
from enum import Enum
4+
from typing import Any
5+
6+
import bluesky.plan_stubs as bps
7+
import bluesky.preprocessors as bpp
8+
from bluesky.protocols import Preparable, Readable
9+
from bluesky.utils import MsgGenerator
10+
from numpy import linspace
11+
from ophyd_async.core import TriggerInfo
12+
13+
from dodal.devices.bimorph_mirror import BimorphMirror
14+
from dodal.devices.slits import Slits
15+
16+
17+
class SlitDimension(str, Enum):
18+
"""Enum representing the dimensions of a 2d slit
19+
20+
Used to describe which dimension the pencil beam scan should move across.
21+
The other dimension will be held constant.
22+
23+
Attributes:
24+
X: Represents X dimension
25+
Y: Represents Y dimension
26+
"""
27+
28+
X = "X"
29+
Y = "Y"
30+
31+
32+
def move_slits(slits: Slits, dimension: SlitDimension, gap: float, center: float):
33+
"""Moves ones dimension of Slits object to given position.
34+
35+
Args:
36+
slits: Slits to move
37+
dimension: SlitDimension (X or Y)
38+
gap: float size of gap
39+
center: float position of center
40+
"""
41+
if dimension == SlitDimension.X:
42+
yield from bps.mv(slits.x_gap, gap)
43+
yield from bps.mv(slits.x_centre, center)
44+
else:
45+
yield from bps.mv(slits.y_gap, gap)
46+
yield from bps.mv(slits.y_centre, center)
47+
48+
49+
def check_valid_bimorph_state(
50+
voltage_list: list[float], abs_range: float, abs_diff: float
51+
) -> bool:
52+
"""Checks that a set of bimorph voltages is valid.
53+
Args:
54+
voltage_list: float amount each actuator will be increased by per scan
55+
abs_range: float absolute value of maximum possible voltage of each actuator
56+
abs_diff: float absolute maximum difference between two consecutive actuators
57+
58+
Returns:
59+
Bool representing state validity
60+
"""
61+
for voltage in voltage_list:
62+
if abs(voltage) > abs_range:
63+
return False
64+
65+
for i in range(len(voltage_list) - 1):
66+
if abs(voltage_list[i] - voltage_list[i - 1]) > abs_diff:
67+
return False
68+
69+
return True
70+
71+
72+
def validate_bimorph_plan(
73+
initial_voltage_list: list[float],
74+
voltage_increment: float,
75+
abs_range: float,
76+
abs_diff: float,
77+
) -> bool:
78+
"""Checks that every position the bimorph will move through will not error.
79+
80+
Args:
81+
initial_voltage_list: float list starting position
82+
voltage_increment: float amount each actuator will be increased by per scan
83+
abs_range: float absolute value of maximum possible voltage of each actuator
84+
abs_diff: float absolute maximum difference between two consecutive actuators
85+
86+
Raises:
87+
Exception if the plan will lead to an error state"""
88+
voltage_list = initial_voltage_list.copy()
89+
90+
if not check_valid_bimorph_state(voltage_list, abs_range, abs_diff):
91+
raise ValueError(f"Bimorph plan reaches invalid state at: {voltage_list}")
92+
93+
for i in range(len(initial_voltage_list)):
94+
voltage_list[i] += voltage_increment
95+
96+
if not check_valid_bimorph_state(voltage_list, abs_range, abs_diff):
97+
raise ValueError(f"Bimorph plan reaches invalid state at: {voltage_list}")
98+
99+
return True
100+
101+
102+
@dataclass
103+
class BimorphState:
104+
"""Data class containing positions of BimorphMirror and Slits"""
105+
106+
voltages: list[float]
107+
x_gap: float
108+
y_gap: float
109+
x_center: float
110+
y_center: float
111+
112+
113+
def capture_bimorph_state(mirror: BimorphMirror, slits: Slits):
114+
"""Plan stub that captures current position of BimorphMirror and Slits.
115+
116+
Args:
117+
mirror: BimorphMirror to read from
118+
slits: Slits to read from
119+
120+
Returns:
121+
A BimorphState containing BimorphMirror and Slits positions"""
122+
original_voltage_list = []
123+
124+
for channel in mirror.channels.values():
125+
position = yield from bps.rd(channel.output_voltage)
126+
original_voltage_list.append(position)
127+
128+
original_x_gap = yield from bps.rd(slits.x_gap)
129+
original_y_gap = yield from bps.rd(slits.y_gap)
130+
original_x_center = yield from bps.rd(slits.x_centre)
131+
original_y_center = yield from bps.rd(slits.y_centre)
132+
return BimorphState(
133+
original_voltage_list,
134+
original_x_gap,
135+
original_y_gap,
136+
original_x_center,
137+
original_y_center,
138+
)
139+
140+
141+
def restore_bimorph_state(mirror: BimorphMirror, slits: Slits, state: BimorphState):
142+
"""Moves BimorphMirror and Slits to state given in BirmophState.
143+
144+
Args:
145+
mirror: BimorphMirror to move
146+
slits: Slits to move
147+
state: BimorphState to move to.
148+
"""
149+
yield from move_slits(slits, SlitDimension.X, state.x_gap, state.x_center)
150+
yield from move_slits(slits, SlitDimension.Y, state.y_gap, state.y_center)
151+
152+
yield from bps.mv(mirror, state.voltages) # type: ignore
153+
154+
155+
def bimorph_position_generator(
156+
initial_voltage_list: list[float], voltage_increment: float
157+
) -> Generator[list[float], None, None]:
158+
"""Generator that produces bimorph positions, starting with the initial_voltage_list.
159+
160+
Args:
161+
initial_voltage_list: list starting position for bimorph
162+
voltage_increment: float amount to increase each actuator by in turn
163+
164+
Yields:
165+
List bimorph positions, starting with initial_voltage_list
166+
"""
167+
voltage_list = initial_voltage_list.copy()
168+
169+
for i in range(-1, len(initial_voltage_list)):
170+
yield [
171+
voltage + voltage_increment if i >= j else voltage
172+
for (j, voltage) in enumerate(voltage_list)
173+
]
174+
175+
176+
def bimorph_optimisation(
177+
detectors: list[Readable],
178+
mirror: BimorphMirror,
179+
slits: Slits,
180+
voltage_increment: float,
181+
active_dimension: SlitDimension,
182+
active_slit_center_start: float,
183+
active_slit_center_end: float,
184+
active_slit_size: float,
185+
inactive_slit_center: float,
186+
inactive_slit_size: float,
187+
number_of_slit_positions: int,
188+
bimorph_settle_time: float,
189+
slit_settle_time: float,
190+
initial_voltage_list: list | None = None,
191+
metadata: dict[str, Any] | None = None,
192+
) -> MsgGenerator:
193+
"""Plan for performing bimorph mirror optimisation.
194+
195+
Bluesky plan that performs a series of pencil beam scans across one axis of a
196+
bimorph mirror, of using a 2-dimensional slit.
197+
198+
Args:
199+
detectors: list[Readable] detectors
200+
bimorph: BimorphMirror to move
201+
slit: Slits
202+
voltage_increment: float voltage increment applied to each bimorph electrode
203+
active_dimension: SlitDimension that slit will move in (X or Y)
204+
active_slit_center_start: float start position of center of slit in active dimension
205+
active_slit_center_end: float final position of center of slit in active dimension
206+
active_slit_size: float size of slit in active dimension
207+
inactive_slit_center: float center of slit in inactive dimension
208+
inactive_slit_size: float size of slit in inactive dimension
209+
number_of_slit_positions: int number of slit positions per pencil beam scan
210+
bimorph_settle_time: float time in seconds to wait after bimorph move
211+
slit_settle_time: float time in seconds to wait after slit move
212+
initial_voltage_list: optional list[float] starting voltages for bimorph (defaults to current voltages)
213+
metadata: optional dict[str, Any] metadata to add to start document
214+
"""
215+
216+
_metadata = {
217+
"plan_args": {
218+
"detectors": {det.name for det in detectors},
219+
"mirror": mirror.name,
220+
"slits": slits.name,
221+
"voltage_increment": voltage_increment,
222+
"active_dimension": active_dimension,
223+
"active_slit_center_start": active_slit_center_start,
224+
"active_slit_center_end": active_slit_center_end,
225+
"active_slit_size": active_slit_size,
226+
"inactive_slit_center": inactive_slit_center,
227+
"inactive_slit_size": inactive_slit_size,
228+
"number_of_slit_positions": number_of_slit_positions,
229+
"bimorph_settle_time": bimorph_settle_time,
230+
"slit_settle_time": slit_settle_time,
231+
"initial_voltage_list": initial_voltage_list,
232+
},
233+
"plan_name": "bimorph_optimisation",
234+
"shape": [len(mirror.channels), number_of_slit_positions],
235+
**(metadata or {}),
236+
}
237+
238+
state = yield from capture_bimorph_state(mirror, slits)
239+
240+
# If a starting set of voltages is not provided, default to current:
241+
initial_voltage_list = initial_voltage_list or state.voltages
242+
243+
bimorph_positions = bimorph_position_generator(
244+
initial_voltage_list, voltage_increment
245+
)
246+
247+
validate_bimorph_plan(initial_voltage_list, voltage_increment, 1000, 500)
248+
249+
inactive_dimension = (
250+
SlitDimension.Y if active_dimension == SlitDimension.X else SlitDimension.X
251+
)
252+
253+
@bpp.run_decorator(md=_metadata)
254+
@bpp.stage_decorator((*detectors, mirror, slits))
255+
def outer_scan():
256+
"""Outer plan stub, which moves mirror and calls inner_scan."""
257+
for detector in detectors:
258+
if isinstance(detector, Preparable):
259+
yield from bps.prepare(detector, TriggerInfo(), wait=True)
260+
261+
stream_name = "0"
262+
yield from bps.declare_stream(*detectors, mirror, slits, name=stream_name)
263+
264+
# Move slits into starting position:
265+
yield from move_slits(
266+
slits, active_dimension, active_slit_size, active_slit_center_start
267+
)
268+
yield from move_slits(
269+
slits, inactive_dimension, inactive_slit_size, inactive_slit_center
270+
)
271+
yield from bps.sleep(slit_settle_time)
272+
273+
for bimorph_position in bimorph_positions:
274+
yield from bps.mv(
275+
mirror, # type: ignore
276+
bimorph_position, # type: ignore
277+
)
278+
yield from bps.sleep(bimorph_settle_time)
279+
280+
yield from bps.declare_stream(*detectors, mirror, slits, name=stream_name)
281+
282+
yield from inner_scan(
283+
detectors,
284+
mirror,
285+
slits,
286+
active_dimension,
287+
active_slit_center_start,
288+
active_slit_center_end,
289+
active_slit_size,
290+
number_of_slit_positions,
291+
slit_settle_time,
292+
stream_name,
293+
)
294+
295+
stream_name = str(int(stream_name) + 1)
296+
297+
yield from outer_scan()
298+
299+
yield from restore_bimorph_state(mirror, slits, state)
300+
301+
302+
def inner_scan(
303+
detectors: list[Readable],
304+
mirror: BimorphMirror,
305+
slits: Slits,
306+
active_dimension: SlitDimension,
307+
active_slit_center_start: float,
308+
active_slit_center_end: float,
309+
active_slit_size: float,
310+
number_of_slit_positions: int,
311+
slit_settle_time: float,
312+
stream_name: str,
313+
):
314+
"""Inner plan stub, which moves Slits and performs a read.
315+
316+
Args:
317+
mirror: BimorphMirror to move
318+
slit: Slits
319+
oav: oav on-axis viewer
320+
active_dimension: SlitDimension that slit will move in (X or Y)
321+
active_slit_center_start: float start position of center of slit in active dimension
322+
active_slit_center_end: float final position of center of slit in active dimension
323+
active_slit_size: float size of slit in active dimension
324+
number_of_slit_positions: int number of slit positions per pencil beam scan
325+
slit_settle_time: float time in seconds to wait after slit move
326+
stream_name: str name to pass to trigger_and_read
327+
"""
328+
for value in linspace(
329+
active_slit_center_start, active_slit_center_end, number_of_slit_positions
330+
):
331+
yield from move_slits(slits, active_dimension, active_slit_size, value)
332+
yield from bps.sleep(slit_settle_time)
333+
yield from bps.trigger_and_read([*detectors, mirror, slits], name=stream_name)

0 commit comments

Comments
 (0)