0
# Motors and Positioners
1
2
Positioning devices including EPICS motors, software positioners, and pseudo positioners for coordinate transformations and multi-axis control. These classes provide standardized interfaces for moving hardware to specific positions and tracking movement status.
3
4
## Capabilities
5
6
### EPICS Motors
7
8
Interface to EPICS motor records providing comprehensive motor control functionality.
9
10
```python { .api }
11
class EpicsMotor(Device):
12
"""
13
EPICS motor record interface with comprehensive positioning capabilities.
14
15
Parameters:
16
- prefix (str): EPICS motor record PV prefix
17
- name (str): Motor name for identification
18
- settle_time (float): Time to wait after moves complete
19
- timeout (float): Default timeout for operations
20
"""
21
def __init__(self, prefix='', *, name, settle_time=0.0, timeout=None, **kwargs): ...
22
23
def move(self, position, *, wait=False, timeout=None, **kwargs):
24
"""
25
Move motor to absolute position.
26
27
Parameters:
28
- position (float): Target position in user units
29
- wait (bool): Whether to wait for move completion
30
- timeout (float): Move timeout
31
32
Returns:
33
MoveStatus: Status object tracking move progress
34
"""
35
36
def set(self, position, **kwargs):
37
"""
38
Set motor position (alias for move).
39
40
Parameters:
41
- position (float): Target position
42
43
Returns:
44
MoveStatus: Move status object
45
"""
46
47
def home(self, direction, *, wait=False, timeout=None):
48
"""
49
Home motor in specified direction.
50
51
Parameters:
52
- direction (str or HomeEnum): 'forward', 'reverse', or HomeEnum value
53
- wait (bool): Wait for homing completion
54
- timeout (float): Homing timeout
55
56
Returns:
57
StatusBase: Homing status
58
"""
59
60
def stop(self, *, success=False):
61
"""
62
Stop motor motion.
63
64
Parameters:
65
- success (bool): Whether stop should be considered successful
66
"""
67
68
@property
69
def position(self):
70
"""
71
Current motor position.
72
73
Returns:
74
float: Current position in user units
75
"""
76
77
@property
78
def user_readback(self):
79
"""
80
User readback position.
81
82
Returns:
83
EpicsSignalRO: Readback position signal
84
"""
85
86
@property
87
def user_setpoint(self):
88
"""
89
User setpoint position.
90
91
Returns:
92
EpicsSignal: Setpoint position signal
93
"""
94
95
@property
96
def limits(self):
97
"""
98
Motor soft limits.
99
100
Returns:
101
tuple: (low_limit, high_limit)
102
"""
103
104
@property
105
def moving(self):
106
"""
107
Whether motor is currently moving.
108
109
Returns:
110
bool: True if motor is in motion
111
"""
112
113
@property
114
def settle_time(self):
115
"""
116
Time to wait after moves complete.
117
118
Returns:
119
float: Settle time in seconds
120
"""
121
122
class MotorBundle(Device):
123
"""
124
Collection of motors that can be moved together.
125
"""
126
def __init__(self, *args, **kwargs): ...
127
128
def move(self, position_dict, **kwargs):
129
"""
130
Move multiple motors simultaneously.
131
132
Parameters:
133
- position_dict (dict): Mapping of motor names to positions
134
135
Returns:
136
AndStatus: Combined status for all motor moves
137
"""
138
139
# Motor-related enums
140
class HomeEnum(IntEnum):
141
"""Enumeration for motor homing directions."""
142
forward = 1
143
reverse = 0
144
```
145
146
### Base Positioners
147
148
Abstract base classes for positioning devices.
149
150
```python { .api }
151
class PositionerBase(Device):
152
"""
153
Base class for all positioner devices.
154
"""
155
def __init__(self, *, name=None, **kwargs): ...
156
157
def move(self, position, **kwargs):
158
"""
159
Move to target position.
160
161
Parameters:
162
- position: Target position
163
164
Returns:
165
StatusBase: Move status
166
"""
167
168
def set(self, position, **kwargs):
169
"""
170
Set position (alias for move).
171
172
Parameters:
173
- position: Target position
174
175
Returns:
176
StatusBase: Move status
177
"""
178
179
@property
180
def position(self):
181
"""
182
Current position.
183
184
Returns:
185
Current position value
186
"""
187
188
def stop(self):
189
"""Stop positioner motion."""
190
191
class SoftPositioner(PositionerBase):
192
"""
193
Software-based positioner without hardware backing.
194
195
Parameters:
196
- name (str): Positioner name
197
- init_pos (float): Initial position value
198
"""
199
def __init__(self, *, name=None, init_pos=0, **kwargs): ...
200
201
def move(self, position, **kwargs):
202
"""
203
Move to position (instantaneous for software positioner).
204
205
Parameters:
206
- position (float): Target position
207
208
Returns:
209
Status: Completed status
210
"""
211
212
@property
213
def position(self):
214
"""
215
Current software position.
216
217
Returns:
218
float: Current position
219
"""
220
```
221
222
### Pseudo Positioners
223
224
Positioners that transform between different coordinate systems.
225
226
```python { .api }
227
class PseudoPositioner(Device):
228
"""
229
Multi-axis pseudo positioner with coordinate transformations.
230
231
Transforms between pseudo (user) coordinates and real (motor) coordinates
232
using forward() and inverse() transformation methods.
233
"""
234
def __init__(self, prefix='', *, configuration_attrs=None, read_attrs=None, **kwargs): ...
235
236
def move(self, position, **kwargs):
237
"""
238
Move to pseudo position.
239
240
Parameters:
241
- position: Target pseudo position (single value or dict/tuple for multi-axis)
242
243
Returns:
244
StatusBase: Combined move status for all real motors
245
"""
246
247
def set(self, position, **kwargs):
248
"""
249
Set pseudo position.
250
251
Parameters:
252
- position: Target pseudo position
253
254
Returns:
255
StatusBase: Move status
256
"""
257
258
def forward(self, pseudo_pos):
259
"""
260
Transform pseudo coordinates to real coordinates.
261
262
Parameters:
263
- pseudo_pos: Pseudo position(s)
264
265
Returns:
266
Real position(s) for underlying motors
267
268
Note: Must be implemented by subclasses
269
"""
270
raise NotImplementedError()
271
272
def inverse(self, real_pos):
273
"""
274
Transform real coordinates to pseudo coordinates.
275
276
Parameters:
277
- real_pos: Real motor position(s)
278
279
Returns:
280
Pseudo position(s)
281
282
Note: Must be implemented by subclasses
283
"""
284
raise NotImplementedError()
285
286
@property
287
def position(self):
288
"""
289
Current pseudo position.
290
291
Returns:
292
Current pseudo coordinates
293
"""
294
295
@property
296
def real_position(self):
297
"""
298
Current real motor positions.
299
300
Returns:
301
tuple: Current real motor coordinates
302
"""
303
304
class PseudoSingle(PseudoPositioner):
305
"""
306
Single-axis pseudo positioner.
307
308
Simplified pseudo positioner for single pseudo axis transformations.
309
"""
310
def __init__(self, prefix='', *, name=None, **kwargs): ...
311
312
@property
313
def position(self):
314
"""
315
Current pseudo position.
316
317
Returns:
318
float: Single pseudo position value
319
"""
320
```
321
322
### PV-Based Positioners
323
324
Positioners controlled through individual EPICS process variables.
325
326
```python { .api }
327
class PVPositioner(Device):
328
"""
329
Positioner controlled via separate setpoint and readback PVs.
330
331
Parameters:
332
- prefix (str): PV prefix
333
- name (str): Positioner name
334
- limits (tuple): Software limits (low, high)
335
- egu (str): Engineering units
336
"""
337
def __init__(self, prefix='', *, limits=None, name=None, egu='', **kwargs): ...
338
339
def move(self, position, **kwargs):
340
"""
341
Move to target position.
342
343
Parameters:
344
- position (float): Target position
345
346
Returns:
347
MoveStatus: Move completion status
348
"""
349
350
def stop(self):
351
"""Stop positioner movement."""
352
353
@property
354
def position(self):
355
"""
356
Current position from readback PV.
357
358
Returns:
359
float: Current position
360
"""
361
362
class PVPositionerPC(PVPositioner):
363
"""
364
PV positioner with position compare functionality.
365
366
Uses EPICS position compare feature for precise positioning.
367
"""
368
def __init__(self, prefix='', **kwargs): ...
369
370
class PVPositionerIsClose(PVPositioner):
371
"""
372
PV positioner that uses proximity checking for move completion.
373
374
Parameters:
375
- atol (float): Absolute tolerance for position comparison
376
- rtol (float): Relative tolerance for position comparison
377
"""
378
def __init__(self, prefix='', *, atol=1e-6, rtol=1e-6, **kwargs): ...
379
380
class PVPositionerDone(PVPositioner):
381
"""
382
PV positioner that uses a separate "done" PV for move completion.
383
"""
384
def __init__(self, prefix='', **kwargs): ...
385
```
386
387
## Usage Examples
388
389
### Basic EPICS Motor Usage
390
391
```python
392
from ophyd import EpicsMotor
393
from ophyd.status import wait
394
395
# Create motor instance
396
motor_x = EpicsMotor('XF:28IDC:MOT:01', name='motor_x')
397
motor_x.wait_for_connection()
398
399
# Check current position
400
print(f"Current position: {motor_x.position}")
401
print(f"Limits: {motor_x.limits}")
402
print(f"Moving: {motor_x.moving}")
403
404
# Move to absolute position
405
status = motor_x.move(10.5)
406
wait(status) # Wait for move to complete
407
408
# Move relative to current position
409
current_pos = motor_x.position
410
status = motor_x.move(current_pos + 2.0)
411
wait(status)
412
413
# Home motor
414
status = motor_x.home('forward')
415
wait(status)
416
417
# Stop motor if needed
418
if motor_x.moving:
419
motor_x.stop()
420
```
421
422
### Motor Bundle for Coordinated Movement
423
424
```python
425
from ophyd import EpicsMotor, MotorBundle
426
427
# Create individual motors
428
motor_x = EpicsMotor('XF:28IDC:MOT:01', name='motor_x')
429
motor_y = EpicsMotor('XF:28IDC:MOT:02', name='motor_y')
430
motor_z = EpicsMotor('XF:28IDC:MOT:03', name='motor_z')
431
432
# Create motor bundle
433
xyz_stage = MotorBundle(motor_x, motor_y, motor_z, name='xyz_stage')
434
xyz_stage.wait_for_connection()
435
436
# Move all motors simultaneously
437
target_positions = {
438
'motor_x': 5.0,
439
'motor_y': -2.5,
440
'motor_z': 10.0
441
}
442
443
status = xyz_stage.move(target_positions)
444
wait(status) # All motors will move simultaneously
445
446
print("All motors have reached target positions")
447
```
448
449
### Software Positioner
450
451
```python
452
from ophyd import SoftPositioner
453
454
# Create software positioner
455
virtual_motor = SoftPositioner(name='virtual_motor', init_pos=0.0)
456
457
# Move instantly (no real hardware)
458
status = virtual_motor.move(25.0)
459
print(f"Status done: {status.done}") # True immediately
460
print(f"Position: {virtual_motor.position}") # 25.0
461
462
# Useful for testing or simulation
463
```
464
465
### Pseudo Positioner Example
466
467
```python
468
from ophyd import PseudoPositioner, EpicsMotor, Component
469
import numpy as np
470
471
class TwoThetaStage(PseudoPositioner):
472
"""Pseudo positioner for 2-theta diffractometer geometry."""
473
474
# Real motors
475
theta = Component(EpicsMotor, 'THETA')
476
two_theta = Component(EpicsMotor, 'TWO_THETA')
477
478
# Pseudo axes
479
h = Cpt(PseudoSingle, '')
480
k = Cpt(PseudoSingle, '')
481
482
def forward(self, pseudo_pos):
483
"""Convert (h,k) to (theta, two_theta)."""
484
h, k = pseudo_pos
485
486
# Simple example: assume direct relationship
487
theta_angle = np.arctan2(k, h) * 180 / np.pi
488
two_theta_angle = 2 * theta_angle
489
490
return (theta_angle, two_theta_angle)
491
492
def inverse(self, real_pos):
493
"""Convert (theta, two_theta) to (h,k)."""
494
theta_angle, two_theta_angle = real_pos
495
496
# Convert back to reciprocal space coordinates
497
theta_rad = theta_angle * np.pi / 180
498
h = np.cos(theta_rad)
499
k = np.sin(theta_rad)
500
501
return (h, k)
502
503
# Use pseudo positioner
504
diffractometer = TwoThetaStage('XF:28IDC:DIFF:', name='diffractometer')
505
diffractometer.wait_for_connection()
506
507
# Move in reciprocal space coordinates
508
status = diffractometer.move((1.0, 0.5)) # Move to h=1.0, k=0.5
509
wait(status)
510
511
print(f"Pseudo position (h,k): {diffractometer.position}")
512
print(f"Real positions (theta, 2theta): {diffractometer.real_position}")
513
```
514
515
### PV Positioner
516
517
```python
518
from ophyd import PVPositioner, Component, EpicsSignal, EpicsSignalRO
519
520
class SlitPositioner(PVPositioner):
521
"""Custom slit positioner with separate setpoint and readback."""
522
523
# Define required PVs
524
setpoint = Component(EpicsSignal, 'SP')
525
readback = Component(EpicsSignalRO, 'RBV')
526
actuate = Component(EpicsSignal, 'GO')
527
528
# Optional: done signal
529
done = Component(EpicsSignalRO, 'DONE')
530
531
# Create slit positioner
532
slit_gap = SlitPositioner('XF:28IDC:SLIT:GAP:', name='slit_gap', limits=(0, 20))
533
slit_gap.wait_for_connection()
534
535
# Move slit gap
536
status = slit_gap.move(5.5)
537
wait(status)
538
539
print(f"Slit gap: {slit_gap.position} mm")
540
```
541
542
### Position Monitoring
543
544
```python
545
from ophyd import EpicsMotor
546
547
motor = EpicsMotor('XF:28IDC:MOT:01', name='motor')
548
motor.wait_for_connection()
549
550
# Subscribe to position changes
551
def position_changed(value=None, **kwargs):
552
print(f"Motor position changed to: {value}")
553
554
motor.user_readback.subscribe(position_changed)
555
556
# Move motor - will trigger callbacks
557
status = motor.move(15.0)
558
559
# Monitor move progress
560
def move_progress(fraction=None, **kwargs):
561
print(f"Move progress: {fraction * 100:.1f}%")
562
563
status.add_callback(move_progress)
564
wait(status)
565
566
# Clear subscription
567
motor.user_readback.clear_sub(position_changed)
568
```