nbs-bl 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nbs_bl/__init__.py +15 -0
- nbs_bl/beamline.py +450 -0
- nbs_bl/configuration.py +838 -0
- nbs_bl/detectors.py +89 -0
- nbs_bl/devices/__init__.py +12 -0
- nbs_bl/devices/detectors.py +154 -0
- nbs_bl/devices/motors.py +242 -0
- nbs_bl/devices/sampleholders.py +360 -0
- nbs_bl/devices/shutters.py +120 -0
- nbs_bl/devices/slits.py +51 -0
- nbs_bl/gGrEqns.py +171 -0
- nbs_bl/geometry/__init__.py +0 -0
- nbs_bl/geometry/affine.py +197 -0
- nbs_bl/geometry/bars.py +189 -0
- nbs_bl/geometry/frames.py +534 -0
- nbs_bl/geometry/linalg.py +138 -0
- nbs_bl/geometry/polygons.py +56 -0
- nbs_bl/help.py +126 -0
- nbs_bl/hw.py +270 -0
- nbs_bl/load.py +113 -0
- nbs_bl/motors.py +19 -0
- nbs_bl/planStatus.py +5 -0
- nbs_bl/plans/__init__.py +8 -0
- nbs_bl/plans/batches.py +174 -0
- nbs_bl/plans/conditions.py +77 -0
- nbs_bl/plans/flyscan_base.py +180 -0
- nbs_bl/plans/groups.py +55 -0
- nbs_bl/plans/maximizers.py +423 -0
- nbs_bl/plans/metaplans.py +179 -0
- nbs_bl/plans/plan_stubs.py +246 -0
- nbs_bl/plans/preprocessors.py +160 -0
- nbs_bl/plans/scan_base.py +58 -0
- nbs_bl/plans/scan_decorators.py +524 -0
- nbs_bl/plans/scans.py +145 -0
- nbs_bl/plans/suspenders.py +87 -0
- nbs_bl/plans/time_estimation.py +168 -0
- nbs_bl/plans/xas.py +123 -0
- nbs_bl/printing.py +221 -0
- nbs_bl/qt/models/beamline.py +11 -0
- nbs_bl/qt/models/energy.py +53 -0
- nbs_bl/qt/widgets/energy.py +225 -0
- nbs_bl/queueserver.py +249 -0
- nbs_bl/redisDevice.py +96 -0
- nbs_bl/run_engine.py +63 -0
- nbs_bl/samples.py +130 -0
- nbs_bl/settings.py +68 -0
- nbs_bl/shutters.py +39 -0
- nbs_bl/sim/__init__.py +2 -0
- nbs_bl/sim/config/polphase.nc +0 -0
- nbs_bl/sim/energy.py +403 -0
- nbs_bl/sim/manipulator.py +14 -0
- nbs_bl/sim/utils.py +36 -0
- nbs_bl/startup.py +27 -0
- nbs_bl/status.py +114 -0
- nbs_bl/tests/__init__.py +0 -0
- nbs_bl/tests/modify_regions.py +160 -0
- nbs_bl/tests/test_frames.py +99 -0
- nbs_bl/tests/test_panels.py +69 -0
- nbs_bl/utils.py +235 -0
- nbs_bl-0.2.0.dist-info/METADATA +71 -0
- nbs_bl-0.2.0.dist-info/RECORD +64 -0
- nbs_bl-0.2.0.dist-info/WHEEL +4 -0
- nbs_bl-0.2.0.dist-info/entry_points.txt +2 -0
- nbs_bl-0.2.0.dist-info/licenses/LICENSE +13 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
from ophyd import PseudoPositioner, Device, Component as Cpt
|
|
2
|
+
from ophyd.pseudopos import (
|
|
3
|
+
pseudo_position_argument,
|
|
4
|
+
real_position_argument,
|
|
5
|
+
PseudoSingle,
|
|
6
|
+
)
|
|
7
|
+
from nbs_bl.geometry.affine import Frame, find_rotation
|
|
8
|
+
from nbs_bl.devices import FlyableMotor
|
|
9
|
+
import numpy as np
|
|
10
|
+
import copy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SampleHolderBase(Device):
|
|
14
|
+
def __init__(self, *args, attachment_point, holder=None, use_redis=True, **kwargs):
|
|
15
|
+
"""
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
holder : GeometryBase, optional
|
|
19
|
+
A GeometryBase subclass that defines the geometry of a samplebar.
|
|
20
|
+
If None, a holder will need to be set later via set_holder
|
|
21
|
+
attachment_point : tuple
|
|
22
|
+
Coordinates of the attachment point in beam coordinates.
|
|
23
|
+
I.e, the motor coordinates that will bring the attachment point into the beam.
|
|
24
|
+
use_redis : bool, optional
|
|
25
|
+
If True, uses Redis-backed status containers where appropriate
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
self.manip_frame = Frame(origin=np.zeros_like(attachment_point))
|
|
29
|
+
self.attachment_frame = self.manip_frame.make_child_frame(
|
|
30
|
+
origin=attachment_point
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Request status containers from global manager
|
|
34
|
+
self.samples = {}
|
|
35
|
+
# GLOBAL_USER_STATUS.request_status_dict(
|
|
36
|
+
# f"{self.name.upper()}_SAMPLES", use_redis=use_redis
|
|
37
|
+
# )
|
|
38
|
+
# current_sample contains metadata, can use Redis
|
|
39
|
+
self.current_sample = {}
|
|
40
|
+
# GLOBAL_USER_STATUS.request_status_dict(
|
|
41
|
+
# f"{self.name.upper()}_CURRENT", use_redis=use_redis
|
|
42
|
+
# )
|
|
43
|
+
|
|
44
|
+
self.sample_frames = {}
|
|
45
|
+
self.holder_md = {}
|
|
46
|
+
self.holder_frames = {}
|
|
47
|
+
self.current_frame = self.attachment_frame
|
|
48
|
+
self.set_holder(holder)
|
|
49
|
+
|
|
50
|
+
def set_holder(self, holder):
|
|
51
|
+
self.holder = holder
|
|
52
|
+
if holder is not None:
|
|
53
|
+
self.holder.attach_manipulator(self.attachment_frame)
|
|
54
|
+
holder_md, holder_frames = self.holder.get_geometry()
|
|
55
|
+
self.holder_md = holder_md
|
|
56
|
+
self.holder_frames = holder_frames
|
|
57
|
+
else:
|
|
58
|
+
self.holder_md = {}
|
|
59
|
+
self.holder_frames = {}
|
|
60
|
+
|
|
61
|
+
def clear_holder(self):
|
|
62
|
+
self.set_holder(None)
|
|
63
|
+
self.samples.clear()
|
|
64
|
+
self.sample_frames.clear()
|
|
65
|
+
self.current_sample.clear()
|
|
66
|
+
|
|
67
|
+
def clear_samples(self):
|
|
68
|
+
self.samples.clear()
|
|
69
|
+
self.sample_frames.clear()
|
|
70
|
+
self.current_sample.clear()
|
|
71
|
+
|
|
72
|
+
def add_sample(self, name, id, position, description="", origin="holder", **kwargs):
|
|
73
|
+
if origin == "absolute":
|
|
74
|
+
sample_frame = position
|
|
75
|
+
else:
|
|
76
|
+
sample_frame = self.holder.make_sample_frame(position)
|
|
77
|
+
|
|
78
|
+
self.samples[id] = {
|
|
79
|
+
"name": name,
|
|
80
|
+
"description": description,
|
|
81
|
+
"position": position,
|
|
82
|
+
"sample_id": id,
|
|
83
|
+
"origin": origin,
|
|
84
|
+
**kwargs,
|
|
85
|
+
}
|
|
86
|
+
self.sample_frames[id] = sample_frame
|
|
87
|
+
|
|
88
|
+
def remove_sample(self, sample_id):
|
|
89
|
+
self.samples.pop(sample_id, None)
|
|
90
|
+
self.sample_frames.pop(sample_id, None)
|
|
91
|
+
if self.current_sample.get("sample_id") == sample_id:
|
|
92
|
+
self.current_sample.clear()
|
|
93
|
+
|
|
94
|
+
def set_sample(self, sample_id):
|
|
95
|
+
if sample_id in self.sample_frames:
|
|
96
|
+
self.current_frame = self.sample_frames[sample_id]
|
|
97
|
+
self.current_sample.clear()
|
|
98
|
+
self.current_sample.update(self.samples[sample_id])
|
|
99
|
+
elif sample_id in self.holder_frames:
|
|
100
|
+
self.current_frame = self.holder_frames[sample_id]
|
|
101
|
+
self.current_sample.clear()
|
|
102
|
+
self.current_sample.update(self.holder_md[sample_id])
|
|
103
|
+
else:
|
|
104
|
+
raise KeyError(
|
|
105
|
+
f"Sample ID {sample_id} not found in sample frames or holder frames"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def load_sample_dict(self, samples, clear=True):
|
|
109
|
+
"""
|
|
110
|
+
Create a sample dictionary into a sampleholder
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
samples : Dict
|
|
115
|
+
A dictionary whose keys are sample_id keys, and entries that are dictionaries containing
|
|
116
|
+
at least 'name' and 'position' keys, and optionally a 'description' key. The format of
|
|
117
|
+
the 'position' dictionary depends on the sampleholder that is being used. Additional items in the
|
|
118
|
+
dictionary will be passed to the sampleholder add_sample function.description
|
|
119
|
+
clear : Bool
|
|
120
|
+
If True, clear existing samples from the sampleholder.
|
|
121
|
+
"""
|
|
122
|
+
if clear:
|
|
123
|
+
self.clear_samples()
|
|
124
|
+
for sample_id, s in samples.items():
|
|
125
|
+
sdict = copy.deepcopy(s)
|
|
126
|
+
name = sdict.pop("name")
|
|
127
|
+
description = sdict.pop("description", name)
|
|
128
|
+
position = sdict.pop("position")
|
|
129
|
+
# add_sample_to_globals(
|
|
130
|
+
# sample_id, name, position, side, thickness, description, **sdict
|
|
131
|
+
# )
|
|
132
|
+
self.add_sample(
|
|
133
|
+
name,
|
|
134
|
+
sample_id,
|
|
135
|
+
position,
|
|
136
|
+
description=description,
|
|
137
|
+
**sdict,
|
|
138
|
+
)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
def load_sample_file(self, filename, clear=True):
|
|
142
|
+
samples = self.holder.read_sample_file(filename)
|
|
143
|
+
self.load_sample_dict(samples, clear=clear)
|
|
144
|
+
|
|
145
|
+
def reload_sample_frames(self):
|
|
146
|
+
"""Reload sample frames from the persisted samples dictionary.
|
|
147
|
+
|
|
148
|
+
This is useful when samples are loaded from a persistent database but
|
|
149
|
+
the corresponding frames need to be reconstructed, such as when
|
|
150
|
+
creating a new SampleHolder instance.
|
|
151
|
+
|
|
152
|
+
Note: This requires a holder to be set and will skip any samples
|
|
153
|
+
with origin="absolute" since those frames are stored directly.
|
|
154
|
+
"""
|
|
155
|
+
self.sample_frames.clear()
|
|
156
|
+
for sample_id, sample in self.samples.items():
|
|
157
|
+
if self.holder is None and sample["origin"] != "absolute":
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
if sample["origin"] == "absolute":
|
|
161
|
+
self.sample_frames[sample_id] = sample["position"]
|
|
162
|
+
else:
|
|
163
|
+
self.sample_frames[sample_id] = self.holder.make_sample_frame(
|
|
164
|
+
sample["position"]
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def move_sample(self, sample_id, **positions):
|
|
168
|
+
position = self.get_sample_position(sample_id, **positions)
|
|
169
|
+
return self.move(position)
|
|
170
|
+
|
|
171
|
+
def get_sample_position(self, sample_id, **positions):
|
|
172
|
+
raise NotImplementedError("This method should be implemented by the subclass")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Manipulator1AxBase(PseudoPositioner, SampleHolderBase):
|
|
176
|
+
sx = Cpt(PseudoSingle)
|
|
177
|
+
|
|
178
|
+
def __init__(self, *args, origin: float = 0, **kwargs):
|
|
179
|
+
super().__init__(*args, attachment_point=[origin], **kwargs)
|
|
180
|
+
|
|
181
|
+
def add_current_position_as_sample(self, name, id, description, **kwargs):
|
|
182
|
+
coordinates = tuple(self.real_position)
|
|
183
|
+
origin = "absolute"
|
|
184
|
+
self.add_sample(
|
|
185
|
+
name, id, {"coordinates": coordinates}, description, origin=origin, **kwargs
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@pseudo_position_argument
|
|
189
|
+
def forward(self, pp):
|
|
190
|
+
"""
|
|
191
|
+
Takes a sample frame position and converts it into real manipulator coordinates
|
|
192
|
+
"""
|
|
193
|
+
if isinstance(self.current_frame, Frame):
|
|
194
|
+
# If current_frame is a Frame object, use the existing conversion
|
|
195
|
+
(position,) = self.current_frame.to_frame(pp, self.manip_frame)
|
|
196
|
+
else:
|
|
197
|
+
# If current_frame is a coordinate dict, add its value to pp
|
|
198
|
+
frame_coords = self.current_frame.get("coordinates", [0])
|
|
199
|
+
position = pp.sx + frame_coords[0]
|
|
200
|
+
|
|
201
|
+
return self.RealPosition(position)
|
|
202
|
+
|
|
203
|
+
@real_position_argument
|
|
204
|
+
def inverse(self, rp):
|
|
205
|
+
if isinstance(self.current_frame, Frame):
|
|
206
|
+
# If current_frame is a Frame object, use the existing conversion
|
|
207
|
+
(position,) = self.manip_frame.to_frame(rp, self.current_frame)
|
|
208
|
+
else:
|
|
209
|
+
# If current_frame is a coordinate dict, subtract its value from rp
|
|
210
|
+
frame_coords = self.current_frame.get("coordinates", [0])
|
|
211
|
+
position = rp.sx - frame_coords[0]
|
|
212
|
+
return self.PseudoPosition(position)
|
|
213
|
+
|
|
214
|
+
def get_sample_position(self, sample_id=None, position=0):
|
|
215
|
+
if sample_id is not None:
|
|
216
|
+
self.set_sample(sample_id)
|
|
217
|
+
return position
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class Manipulator4AxBase(PseudoPositioner, SampleHolderBase):
|
|
221
|
+
sx = Cpt(PseudoSingle)
|
|
222
|
+
sy = Cpt(PseudoSingle)
|
|
223
|
+
sz = Cpt(PseudoSingle)
|
|
224
|
+
sr = Cpt(PseudoSingle)
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
An X, Y, Z, R sample positioner
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
# Really need a discrete manipulator that can be set to
|
|
231
|
+
# one of several sample positions. May just be a sampleholder
|
|
232
|
+
# Good argument for having sampleholder contain the motors, not
|
|
233
|
+
# the other way around...?
|
|
234
|
+
def __init__(self, *args, beam_direction=(0, -1, 0), rotation_ax=2, **kwargs):
|
|
235
|
+
super().__init__(*args, **kwargs)
|
|
236
|
+
self.beam_direction = beam_direction
|
|
237
|
+
self.rotation_ax = rotation_ax
|
|
238
|
+
self.ax1 = (rotation_ax + 1) % 3
|
|
239
|
+
self.ax2 = (rotation_ax + 2) % 3
|
|
240
|
+
self.default_coords = [0, 0, 0, 45]
|
|
241
|
+
|
|
242
|
+
@pseudo_position_argument
|
|
243
|
+
def forward(self, pp):
|
|
244
|
+
"""
|
|
245
|
+
Takes a sample frame position and converts it into real manipulator coordinates
|
|
246
|
+
|
|
247
|
+
bp = self.holder.frame_to_beam(*pp)
|
|
248
|
+
if isinstance(bp, (float, int)):
|
|
249
|
+
bp = (bp,)
|
|
250
|
+
rp = self.beam_to_manip_frame(*bp)
|
|
251
|
+
return self.RealPosition(*rp)
|
|
252
|
+
"""
|
|
253
|
+
sample_coords = pp[:-1]
|
|
254
|
+
r = pp[-1]
|
|
255
|
+
|
|
256
|
+
if isinstance(self.current_frame, Frame):
|
|
257
|
+
# If current_frame is a Frame object, use the existing conversion
|
|
258
|
+
xp, yp, zp = self.current_frame.to_frame(
|
|
259
|
+
sample_coords,
|
|
260
|
+
self.manip_frame,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
r = self.sample_rotation_to_manip_rotation(r)
|
|
264
|
+
x, y, z = self.manip_frame.rotate_in_plane(
|
|
265
|
+
(xp, yp, zp), r * np.pi / 180.0, ax1=self.ax1, ax2=self.ax2
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
# If current_frame is a coordinate dict, add its values to sample_coords
|
|
269
|
+
frame_coords = self.current_frame.get("coordinates", [0, 0, 0, 0])
|
|
270
|
+
x = sample_coords[0] + frame_coords[0]
|
|
271
|
+
y = sample_coords[1] + frame_coords[1]
|
|
272
|
+
z = sample_coords[2] + frame_coords[2]
|
|
273
|
+
r += frame_coords[3]
|
|
274
|
+
|
|
275
|
+
return self.RealPosition(x, y, z, r)
|
|
276
|
+
|
|
277
|
+
@real_position_argument
|
|
278
|
+
def inverse(self, rp):
|
|
279
|
+
"""
|
|
280
|
+
Takes a real manipulator position and converts into frame coordinates
|
|
281
|
+
|
|
282
|
+
bp = self.manip_to_beam_frame(*rp)
|
|
283
|
+
pp = self.holder.beam_to_frame(*bp)
|
|
284
|
+
if isinstance(pp, (float, int)):
|
|
285
|
+
pp = (pp,)
|
|
286
|
+
return self.PseudoPosition(*pp)
|
|
287
|
+
"""
|
|
288
|
+
real_coords = rp[:-1]
|
|
289
|
+
|
|
290
|
+
if isinstance(self.current_frame, Frame):
|
|
291
|
+
r = self.manip_rotation_to_sample_rotation(rp[-1])
|
|
292
|
+
xp, yp, zp = self.manip_frame.rotate_in_plane(
|
|
293
|
+
real_coords, -rp[-1] * np.pi / 180.0, ax1=self.ax1, ax2=self.ax2
|
|
294
|
+
)
|
|
295
|
+
# If current_frame is a Frame object, use the existing conversion
|
|
296
|
+
x, y, z = self.manip_frame.to_frame((xp, yp, zp), self.current_frame)
|
|
297
|
+
else:
|
|
298
|
+
# If current_frame is a coordinate dict, subtract its values from xp, yp, zp, r
|
|
299
|
+
frame_coords = self.current_frame.get("coordinates", [0, 0, 0, 0])
|
|
300
|
+
x = rp[0] - frame_coords[0]
|
|
301
|
+
y = rp[1] - frame_coords[1]
|
|
302
|
+
z = rp[2] - frame_coords[2]
|
|
303
|
+
r = rp[3] - frame_coords[3]
|
|
304
|
+
|
|
305
|
+
return self.PseudoPosition(x, y, z, r)
|
|
306
|
+
|
|
307
|
+
def add_current_position_as_sample(self, name, id, description, **kwargs):
|
|
308
|
+
coordinates = tuple(self.real_position)
|
|
309
|
+
origin = "absolute"
|
|
310
|
+
self.add_sample(
|
|
311
|
+
name, id, {"coordinates": coordinates}, description, origin=origin, **kwargs
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def get_sample_position(self, sample_id=None, **positions):
|
|
315
|
+
if sample_id is not None:
|
|
316
|
+
self.set_sample(sample_id)
|
|
317
|
+
if isinstance(self.current_frame, Frame):
|
|
318
|
+
position = [p for p in self.default_coords]
|
|
319
|
+
else:
|
|
320
|
+
position = [0, 0, 0, 0]
|
|
321
|
+
if "x" in positions:
|
|
322
|
+
position[0] = positions["x"]
|
|
323
|
+
if "y" in positions:
|
|
324
|
+
position[1] = positions["y"]
|
|
325
|
+
if "z" in positions:
|
|
326
|
+
position[2] = positions["z"]
|
|
327
|
+
if "r" in positions:
|
|
328
|
+
position[3] = positions["r"]
|
|
329
|
+
return position
|
|
330
|
+
|
|
331
|
+
def sample_rotation_to_manip_rotation(self, r):
|
|
332
|
+
# Assumes that z-axis is the surface normal!!
|
|
333
|
+
grazing = find_rotation(
|
|
334
|
+
self.current_frame,
|
|
335
|
+
(1, 0, 0),
|
|
336
|
+
self.manip_frame,
|
|
337
|
+
self.beam_direction,
|
|
338
|
+
self.rotation_ax,
|
|
339
|
+
)
|
|
340
|
+
return grazing * 180.0 / np.pi + r
|
|
341
|
+
|
|
342
|
+
def manip_rotation_to_sample_rotation(self, r):
|
|
343
|
+
grazing = find_rotation(
|
|
344
|
+
self.current_frame,
|
|
345
|
+
(1, 0, 0),
|
|
346
|
+
self.manip_frame,
|
|
347
|
+
self.beam_direction,
|
|
348
|
+
self.rotation_ax,
|
|
349
|
+
)
|
|
350
|
+
return r - grazing * 180.0 / np.pi
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def manipulatorFactory4Ax(xPV, yPV, zPV, rPV):
|
|
354
|
+
class Manipulator(Manipulator4AxBase):
|
|
355
|
+
x = Cpt(FlyableMotor, xPV, name="x", kind="hinted")
|
|
356
|
+
y = Cpt(FlyableMotor, yPV, name="y", kind="hinted")
|
|
357
|
+
z = Cpt(FlyableMotor, zPV, name="z", kind="hinted")
|
|
358
|
+
r = Cpt(FlyableMotor, rPV, name="r", kind="hinted")
|
|
359
|
+
|
|
360
|
+
return Manipulator
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from ophyd import Device, EpicsSignal, Component as Cpt, PVPositionerPC
|
|
2
|
+
from ophyd.status import SubscriptionStatus
|
|
3
|
+
import bluesky.plan_stubs as bps
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EPS_Shutter(Device):
|
|
8
|
+
state = Cpt(EpicsSignal, "Pos-Sts")
|
|
9
|
+
cls = Cpt(EpicsSignal, "Cmd:Cls-Cmd")
|
|
10
|
+
opn = Cpt(EpicsSignal, "Cmd:Opn-Cmd")
|
|
11
|
+
error = Cpt(EpicsSignal, "Err-Sts")
|
|
12
|
+
maxcount = 3
|
|
13
|
+
# openval = 1 # normal shutter values, FS1 is reversed
|
|
14
|
+
# closeval = 0
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, openval=1, closeval=0, shutter_type=None, **kwargs):
|
|
17
|
+
super().__init__(*args, **kwargs)
|
|
18
|
+
self.openval = openval
|
|
19
|
+
self.closeval = closeval
|
|
20
|
+
self.shutter_type = shutter_type
|
|
21
|
+
|
|
22
|
+
def status(self):
|
|
23
|
+
if self.state.get() == self.closeval:
|
|
24
|
+
return "closed"
|
|
25
|
+
else:
|
|
26
|
+
return "open"
|
|
27
|
+
|
|
28
|
+
def open(self):
|
|
29
|
+
count = 0
|
|
30
|
+
while self.state.get() != self.openval:
|
|
31
|
+
count += 1
|
|
32
|
+
print("\u231b", end=" ", flush=True)
|
|
33
|
+
yield from bps.mv(self.opn, 1)
|
|
34
|
+
if count >= self.maxcount:
|
|
35
|
+
print(
|
|
36
|
+
"tried %d times and failed to open %s %s" % (count, self.name, ":(")
|
|
37
|
+
) # u'\u2639' unicode frown
|
|
38
|
+
return (yield from bps.null())
|
|
39
|
+
yield from bps.sleep(1.5)
|
|
40
|
+
print("Opened {}".format(self.name))
|
|
41
|
+
|
|
42
|
+
def close(self):
|
|
43
|
+
count = 0
|
|
44
|
+
while self.state.get() != self.closeval:
|
|
45
|
+
count += 1
|
|
46
|
+
print("\u231b", end=" ", flush=True)
|
|
47
|
+
yield from bps.mv(self.cls, 1)
|
|
48
|
+
if count >= self.maxcount:
|
|
49
|
+
print(
|
|
50
|
+
"tried %d times and failed to close %s %s"
|
|
51
|
+
% (count, self.name, ":(")
|
|
52
|
+
)
|
|
53
|
+
return (yield from bps.null())
|
|
54
|
+
yield from bps.sleep(1.5)
|
|
55
|
+
print("Closed {}".format(self.name))
|
|
56
|
+
|
|
57
|
+
def open_nonplan(self):
|
|
58
|
+
self.read()
|
|
59
|
+
if self.state.get() != self.openval:
|
|
60
|
+
count = 0
|
|
61
|
+
while self.state.get() != self.openval:
|
|
62
|
+
count += 1
|
|
63
|
+
print("\u231b", end=" ", flush=True)
|
|
64
|
+
self.opn.set(1)
|
|
65
|
+
if count >= self.maxcount:
|
|
66
|
+
print(
|
|
67
|
+
"tried %d times and failed to open %s %s"
|
|
68
|
+
% (count, self.name, ":(")
|
|
69
|
+
)
|
|
70
|
+
return
|
|
71
|
+
time.sleep(1.5)
|
|
72
|
+
self.read()
|
|
73
|
+
print(" Opened {}".format(self.name))
|
|
74
|
+
else:
|
|
75
|
+
print("{} is open".format(self.name))
|
|
76
|
+
|
|
77
|
+
def close_nonplan(self):
|
|
78
|
+
self.read()
|
|
79
|
+
if self.state.get() != self.closeval:
|
|
80
|
+
count = 0
|
|
81
|
+
while self.state.get() != self.closeval:
|
|
82
|
+
count += 1
|
|
83
|
+
print("\u231b", end=" ", flush=True)
|
|
84
|
+
self.cls.set(1)
|
|
85
|
+
if count >= self.maxcount:
|
|
86
|
+
print(
|
|
87
|
+
"tried %d times and failed to close %s %s"
|
|
88
|
+
% (count, self.name, ":(")
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
time.sleep(1.5)
|
|
92
|
+
self.read()
|
|
93
|
+
print(" Closed {}".format(self.name))
|
|
94
|
+
else:
|
|
95
|
+
print("{} is closed".format(self.name))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ShutterSet(PVPositionerPC):
|
|
99
|
+
"""
|
|
100
|
+
Used for set of Diode box shutters
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
readback = Cpt(EpicsSignal, "-RB")
|
|
104
|
+
setpoint = Cpt(EpicsSignal, "-SP")
|
|
105
|
+
|
|
106
|
+
def set(self, value, *args, **kwargs):
|
|
107
|
+
if value is None:
|
|
108
|
+
saw_rise = False
|
|
109
|
+
|
|
110
|
+
def watcher(*, old_value, value, **kwargs):
|
|
111
|
+
nonlocal saw_rise
|
|
112
|
+
if value == 1:
|
|
113
|
+
saw_rise = True
|
|
114
|
+
return False
|
|
115
|
+
if value == 0 and saw_rise:
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
return SubscriptionStatus(self.readback, watcher)
|
|
119
|
+
else:
|
|
120
|
+
return super().set(value, *args, **kwargs)
|
nbs_bl/devices/slits.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from ophyd import EpicsMotor, PseudoPositioner, PseudoSingle, Component as Cpt
|
|
2
|
+
from ophyd.pseudopos import pseudo_position_argument, real_position_argument
|
|
3
|
+
from nbs_bl.printing import boxed_text
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Slits(PseudoPositioner):
|
|
7
|
+
def __init__(self, *args, **kwargs):
|
|
8
|
+
super().__init__(*args, **kwargs)
|
|
9
|
+
|
|
10
|
+
def where(self):
|
|
11
|
+
print("%s:" % self.name)
|
|
12
|
+
text1 = " vertical size = %7.3f mm\n" % (self.vsize.position)
|
|
13
|
+
text1 += " vertical center = %7.3f mm\n" % (self.vcenter.position)
|
|
14
|
+
text2 = " horizontal size = %7.3f mm\n" % (self.hsize.position)
|
|
15
|
+
text2 += " horizontal center = %7.3f mm\n" % (self.hcenter.position)
|
|
16
|
+
return text1 + text2
|
|
17
|
+
|
|
18
|
+
def wh(self):
|
|
19
|
+
boxed_text(self.name, self.where(), "cyan") # bruce's
|
|
20
|
+
|
|
21
|
+
# The pseudo positioner axes:
|
|
22
|
+
vsize = Cpt(PseudoSingle, limits=(-1, 20), kind="hinted")
|
|
23
|
+
vcenter = Cpt(PseudoSingle, limits=(-10, 10), kind="normal")
|
|
24
|
+
hsize = Cpt(PseudoSingle, limits=(-1, 20), kind="hinted")
|
|
25
|
+
hcenter = Cpt(PseudoSingle, limits=(-10, 10), kind="normal")
|
|
26
|
+
|
|
27
|
+
# The real (or physical) positioners:
|
|
28
|
+
top = Cpt(EpicsMotor, "T}Mtr", kind="normal")
|
|
29
|
+
bottom = Cpt(EpicsMotor, "B}Mtr", kind="normal")
|
|
30
|
+
inboard = Cpt(EpicsMotor, "I}Mtr", kind="normal")
|
|
31
|
+
outboard = Cpt(EpicsMotor, "O}Mtr", kind="normal")
|
|
32
|
+
|
|
33
|
+
@pseudo_position_argument
|
|
34
|
+
def forward(self, pseudo_pos):
|
|
35
|
+
"""Run a forward (pseudo -> real) calculation"""
|
|
36
|
+
return self.RealPosition(
|
|
37
|
+
top=pseudo_pos.vcenter + pseudo_pos.vsize / 2,
|
|
38
|
+
bottom=pseudo_pos.vcenter - pseudo_pos.vsize / 2,
|
|
39
|
+
outboard=pseudo_pos.hcenter + pseudo_pos.hsize / 2,
|
|
40
|
+
inboard=pseudo_pos.hcenter - pseudo_pos.hsize / 2,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@real_position_argument
|
|
44
|
+
def inverse(self, real_pos):
|
|
45
|
+
"""Run an inverse (real -> pseudo) calculation"""
|
|
46
|
+
return self.PseudoPosition(
|
|
47
|
+
hsize=real_pos.outboard - real_pos.inboard,
|
|
48
|
+
hcenter=(real_pos.outboard + real_pos.inboard) / 2,
|
|
49
|
+
vsize=real_pos.top - real_pos.bottom,
|
|
50
|
+
vcenter=(real_pos.top + real_pos.bottom) / 2,
|
|
51
|
+
)
|