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.
Files changed (64) hide show
  1. nbs_bl/__init__.py +15 -0
  2. nbs_bl/beamline.py +450 -0
  3. nbs_bl/configuration.py +838 -0
  4. nbs_bl/detectors.py +89 -0
  5. nbs_bl/devices/__init__.py +12 -0
  6. nbs_bl/devices/detectors.py +154 -0
  7. nbs_bl/devices/motors.py +242 -0
  8. nbs_bl/devices/sampleholders.py +360 -0
  9. nbs_bl/devices/shutters.py +120 -0
  10. nbs_bl/devices/slits.py +51 -0
  11. nbs_bl/gGrEqns.py +171 -0
  12. nbs_bl/geometry/__init__.py +0 -0
  13. nbs_bl/geometry/affine.py +197 -0
  14. nbs_bl/geometry/bars.py +189 -0
  15. nbs_bl/geometry/frames.py +534 -0
  16. nbs_bl/geometry/linalg.py +138 -0
  17. nbs_bl/geometry/polygons.py +56 -0
  18. nbs_bl/help.py +126 -0
  19. nbs_bl/hw.py +270 -0
  20. nbs_bl/load.py +113 -0
  21. nbs_bl/motors.py +19 -0
  22. nbs_bl/planStatus.py +5 -0
  23. nbs_bl/plans/__init__.py +8 -0
  24. nbs_bl/plans/batches.py +174 -0
  25. nbs_bl/plans/conditions.py +77 -0
  26. nbs_bl/plans/flyscan_base.py +180 -0
  27. nbs_bl/plans/groups.py +55 -0
  28. nbs_bl/plans/maximizers.py +423 -0
  29. nbs_bl/plans/metaplans.py +179 -0
  30. nbs_bl/plans/plan_stubs.py +246 -0
  31. nbs_bl/plans/preprocessors.py +160 -0
  32. nbs_bl/plans/scan_base.py +58 -0
  33. nbs_bl/plans/scan_decorators.py +524 -0
  34. nbs_bl/plans/scans.py +145 -0
  35. nbs_bl/plans/suspenders.py +87 -0
  36. nbs_bl/plans/time_estimation.py +168 -0
  37. nbs_bl/plans/xas.py +123 -0
  38. nbs_bl/printing.py +221 -0
  39. nbs_bl/qt/models/beamline.py +11 -0
  40. nbs_bl/qt/models/energy.py +53 -0
  41. nbs_bl/qt/widgets/energy.py +225 -0
  42. nbs_bl/queueserver.py +249 -0
  43. nbs_bl/redisDevice.py +96 -0
  44. nbs_bl/run_engine.py +63 -0
  45. nbs_bl/samples.py +130 -0
  46. nbs_bl/settings.py +68 -0
  47. nbs_bl/shutters.py +39 -0
  48. nbs_bl/sim/__init__.py +2 -0
  49. nbs_bl/sim/config/polphase.nc +0 -0
  50. nbs_bl/sim/energy.py +403 -0
  51. nbs_bl/sim/manipulator.py +14 -0
  52. nbs_bl/sim/utils.py +36 -0
  53. nbs_bl/startup.py +27 -0
  54. nbs_bl/status.py +114 -0
  55. nbs_bl/tests/__init__.py +0 -0
  56. nbs_bl/tests/modify_regions.py +160 -0
  57. nbs_bl/tests/test_frames.py +99 -0
  58. nbs_bl/tests/test_panels.py +69 -0
  59. nbs_bl/utils.py +235 -0
  60. nbs_bl-0.2.0.dist-info/METADATA +71 -0
  61. nbs_bl-0.2.0.dist-info/RECORD +64 -0
  62. nbs_bl-0.2.0.dist-info/WHEEL +4 -0
  63. nbs_bl-0.2.0.dist-info/entry_points.txt +2 -0
  64. nbs_bl-0.2.0.dist-info/licenses/LICENSE +13 -0
nbs_bl/detectors.py ADDED
@@ -0,0 +1,89 @@
1
+ from .help import add_to_func_list
2
+ from .beamline import GLOBAL_BEAMLINE
3
+
4
+
5
+ def get_active_detectors():
6
+ return GLOBAL_BEAMLINE.detectors.active
7
+
8
+
9
+ @add_to_func_list
10
+ def activate_detector(det_or_name, role=None):
11
+ """Activate a detector so that is is measured by default
12
+
13
+ Parameters
14
+ ----------
15
+ det_or_name : device or str
16
+ Either a device, or the name of a device in the global
17
+ detector buffer
18
+
19
+ """
20
+ # Todo: take a list
21
+ GLOBAL_BEAMLINE.detectors.activate(det_or_name, role)
22
+
23
+
24
+ @add_to_func_list
25
+ def list_detectors(verbose=False):
26
+ """List all global detectors, optionally provide text descriptions
27
+
28
+ Parameters
29
+ ----------
30
+ describe : Bool
31
+ If True, print the text description of each detector
32
+
33
+ """
34
+
35
+ def textFunction(group, key, device):
36
+ text = f"{key}: {device.name}; {group.status[key]}"
37
+ return text
38
+
39
+ GLOBAL_BEAMLINE.detectors.describe(verbose, textFunction)
40
+
41
+
42
+ @add_to_func_list
43
+ def disable_detector(det_or_name):
44
+ """Disable a detector so that it is not measured, and will not be activated
45
+
46
+ Parameters
47
+ ----------
48
+ det_or_name : device or str
49
+ Either a device, or the name of a device in the global
50
+ detector buffer
51
+
52
+ """
53
+ GLOBAL_BEAMLINE.detectors.disable(det_or_name)
54
+
55
+
56
+ @add_to_func_list
57
+ def enable_detector(det_or_name, activate=True):
58
+ """Enable a detector so that it can be activated
59
+
60
+ Parameters
61
+ ----------
62
+ det_or_name : device or str
63
+ Either a device, or the name of a device in the global
64
+ detector buffer
65
+ activate : Bool
66
+ If True, also activate the detector when it is enabled
67
+ """
68
+
69
+ GLOBAL_BEAMLINE.detectors.enable(det_or_name, activate)
70
+
71
+
72
+ @add_to_func_list
73
+ def deactivate_detector(det_or_name):
74
+ """Deactivate a detector so that it is not measured by default
75
+
76
+ Parameters
77
+ ----------
78
+ det_or_name : device or str
79
+ Either a device, or the name of a device in the global
80
+ detector buffer
81
+
82
+ """
83
+
84
+ GLOBAL_BEAMLINE.detectors.deactivate(det_or_name)
85
+
86
+
87
+ @add_to_func_list
88
+ def activate_detector_set(name):
89
+ GLOBAL_BEAMLINE.detectors.activate_set(name)
@@ -0,0 +1,12 @@
1
+ from .detectors import ophScalar
2
+ from .motors import (
3
+ FlyableMotor,
4
+ DeadbandEpicsMotor,
5
+ DeadbandPVPositioner,
6
+ DeadbandMixin,
7
+ PseudoSingle,
8
+ FlyerMixin,
9
+ )
10
+ from .shutters import EPS_Shutter, ShutterSet
11
+ from .slits import Slits
12
+ from .sampleholders import Manipulator1AxBase, Manipulator4AxBase
@@ -0,0 +1,154 @@
1
+ from ophyd import Device, Component as Cpt, EpicsSignal, Signal
2
+ from ophyd.status import DeviceStatus
3
+ import threading
4
+ from queue import Queue, Empty
5
+ import time
6
+ import numpy as np
7
+
8
+
9
+ class ScalarBase(Device):
10
+ exposure_time = Cpt(Signal, value=1, name="exposure_time", kind="config")
11
+ mean = Cpt(Signal, name="", value=0, kind="hinted")
12
+ median = Cpt(Signal, name="median", value=0, kind="omitted")
13
+ std = Cpt(Signal, name="std", value=0, kind="omitted")
14
+ npts = Cpt(Signal, name="points", value=0, kind="omitted")
15
+ sum = Cpt(Signal, name="sum", value=0, kind="omitted")
16
+ rescale = Cpt(Signal, value=1, name="rescale", kind="config")
17
+ offset = Cpt(Signal, value=0, name="offset", kind="config")
18
+ gain = Cpt(Signal, value=1, name="gain", kind="config")
19
+
20
+ def __init__(self, *args, rescale=1, **kwargs):
21
+ self._flying = False
22
+ self._measuring = False
23
+ self._reading = False
24
+ self._flyer_buffer = []
25
+ self._flyer_time_buffer = []
26
+ super().__init__(*args, **kwargs)
27
+ self.mean.name = self.name
28
+ self.rescale.set(rescale).wait(timeout=60)
29
+ if "gain" in kwargs:
30
+ self.gain.set(kwargs["gain"]).wait(timeout=60)
31
+
32
+ def kickoff(self):
33
+ self._flyer_buffer = []
34
+ self._flyer_time_buffer = []
35
+ self._flyer_timestamp_buffer = []
36
+ self._flyer_queue = Queue()
37
+ kickoff_st = DeviceStatus(device=self)
38
+ kickoff_st.set_finished()
39
+ self._flying = True
40
+ if not self._measuring:
41
+ self.target.subscribe(self._aggregate, run=False)
42
+ self._measuring = True
43
+
44
+ return kickoff_st
45
+
46
+ def stage(self):
47
+ self._secret_buffer = []
48
+ self._secret_time_buffer = []
49
+ self._buffer = []
50
+ self._time_buffer = []
51
+ self._reading = True
52
+ if not self._measuring:
53
+ self.target.subscribe(self._aggregate, run=False)
54
+ self._measuring = True
55
+ return super().stage()
56
+
57
+ def unstage(self):
58
+ if self._measuring:
59
+ self.target.clear_sub(self._aggregate)
60
+ self._measuring = False
61
+ self._reading = False
62
+ return super().unstage()
63
+
64
+ def set_exposure(self, exp_time):
65
+ self.exposure_time.set(exp_time).wait(timeout=60)
66
+
67
+ def _aggregate(self, value, **kwargs):
68
+ scale_value = value * self.rescale.get() - self.offset.get()
69
+ t = time.time()
70
+ if self._reading:
71
+ self._buffer.append(scale_value)
72
+ self._time_buffer.append(t)
73
+ if self._flying:
74
+ event = dict()
75
+ event["time"] = t
76
+ event["data"] = dict()
77
+ event["timestamps"] = dict()
78
+ event["data"][self.name] = scale_value
79
+ event["timestamps"][self.name] = kwargs.get("timestamp", t)
80
+ self._flyer_buffer.append(scale_value)
81
+ self._flyer_time_buffer.append(t)
82
+ self._flyer_timestamp_buffer.append(kwargs.get("timestamp", t))
83
+ self._flyer_queue.put(event)
84
+
85
+ def _acquire(self, status):
86
+ self._buffer = []
87
+ self._time_buffer = []
88
+ time.sleep(self.exposure_time.get())
89
+ if len(self._buffer) == 0:
90
+ ntry = 10
91
+ n = 0
92
+ while len(self._buffer) < 1:
93
+ time.sleep(0.1 * self.exposure_time.get())
94
+ n += 1
95
+ if n > ntry:
96
+ break
97
+ buf = np.array(self._buffer)
98
+ tbuf = np.array(self._time_buffer[: len(buf)])
99
+ if len(buf) == 0:
100
+ self.mean.put(np.nan)
101
+ self.median.put(np.nan)
102
+ self.std.put(np.nan)
103
+ self.npts.put(0)
104
+ self.sum.put(np.nan)
105
+
106
+ else:
107
+ self.mean.put(np.mean(buf))
108
+ self.median.put(np.median(buf))
109
+ self.std.put(np.std(buf))
110
+ self.npts.put(len(buf))
111
+ self.sum.put(np.sum(buf))
112
+ self._secret_buffer.append(buf)
113
+ self._secret_time_buffer.append(tbuf)
114
+ status.set_finished()
115
+ return
116
+
117
+ def trigger(self):
118
+ status = DeviceStatus(self)
119
+ threading.Thread(target=self._acquire, args=(status,), daemon=True).start()
120
+ return status
121
+
122
+ def collect(self):
123
+ events = []
124
+ while True:
125
+ try:
126
+ e = self._flyer_queue.get_nowait()
127
+ events.append(e)
128
+ except Empty:
129
+ break
130
+ yield from events
131
+
132
+ def complete(self):
133
+ self._flying = False
134
+ if self._measuring:
135
+ self.target.clear_sub(self._aggregate)
136
+ self._measuring = False
137
+ completion_status = DeviceStatus(self)
138
+ completion_status.set_finished()
139
+ return completion_status
140
+
141
+ def describe_collect(self):
142
+ dd = dict(
143
+ {self.name: {"source": self.target.pvname, "dtype": "number", "shape": []}}
144
+ )
145
+ return {self.name + "_monitor": dd}
146
+
147
+ def get_plot_hints(self):
148
+ return [self.mean.name]
149
+
150
+
151
+ class ophScalar(ScalarBase):
152
+ """Generic Scalar. Give full path to target PV during object creation"""
153
+
154
+ target = Cpt(EpicsSignal, "", kind="omitted")
@@ -0,0 +1,242 @@
1
+ from queue import Queue, Empty
2
+ import time
3
+ from datetime import datetime
4
+ import threading
5
+
6
+ from ophyd import EpicsMotor, Signal, PositionerBase, Device
7
+ from ophyd.pv_positioner import PVPositioner
8
+ from ophyd import Component as Cpt
9
+ from ophyd.status import wait as status_wait, DeviceStatus
10
+ from ophyd import PseudoSingle as _PS
11
+
12
+
13
+ class FlyerMixin:
14
+
15
+ def __init__(self, *args, **kwargs):
16
+ self._ready_to_fly = False
17
+ self._fly_move_st = None
18
+ self._default_time_resolution = 0.1
19
+ self._time_resolution = None
20
+ super().__init__(*args, **kwargs)
21
+
22
+ # Flyer motor methods
23
+ def preflight(self, start, stop, speed=None, time_resolution=None):
24
+ self._old_velocity = self.velocity.get()
25
+ self._flyer_stop = stop
26
+ st = self.move(start)
27
+ if speed is None:
28
+ speed = self._old_velocity
29
+ self.velocity.set(speed).wait()
30
+ if time_resolution is not None:
31
+ self._time_resolution = time_resolution
32
+ else:
33
+ self._time_resolution = self._default_time_resolution
34
+ self._last_readback_value = start
35
+ self._ready_to_fly = True
36
+ return st
37
+
38
+ def fly(self):
39
+ """
40
+ Should be called after all detectors start flying, so that we don't lose data
41
+ """
42
+ if not self._ready_to_fly:
43
+ self._fly_move_st = DeviceStatus(device=self)
44
+ self._fly_move_st.set_finished(success=False)
45
+ else:
46
+ self._fly_move_st = self.move(self._flyer_stop, wait=False)
47
+ self._flying = True
48
+ self._ready_to_fly = False
49
+ return self._fly_move_st
50
+
51
+ def land(self):
52
+ if self._fly_move_st.done:
53
+ self.velocity.set(self._old_velocity).wait()
54
+ self._flying = False
55
+ self._time_resolution = None
56
+
57
+ # Flyer detector methods for readback
58
+ def kickoff(self):
59
+ kickoff_st = DeviceStatus(device=self)
60
+ self._flyer_queue = Queue()
61
+ self._measuring = True
62
+ self._flyer_buffer = []
63
+ threading.Thread(target=self._aggregate, daemon=True).start()
64
+ # self.user_readback.subscribe(self._aggregate, run=False)
65
+ kickoff_st.set_finished()
66
+ return kickoff_st
67
+
68
+ def _aggregate(self):
69
+ name = self.user_readback.name
70
+ while self._measuring:
71
+ t = time.time()
72
+ rb = self.user_readback.read()
73
+ value = rb[name]["value"]
74
+ ts = rb[name]["timestamp"]
75
+ self._flyer_buffer.append(value)
76
+ event = dict()
77
+ event["time"] = t
78
+ event["data"] = dict()
79
+ event["timestamps"] = dict()
80
+ event["data"][name] = value
81
+ event["timestamps"][name] = ts
82
+ self._flyer_queue.put(event)
83
+ time.sleep(self._time_resolution)
84
+ return
85
+
86
+ def collect(self):
87
+ events = []
88
+ while True:
89
+ try:
90
+ e = self._flyer_queue.get_nowait()
91
+ events.append(e)
92
+ except Empty:
93
+ break
94
+ yield from events
95
+
96
+ def complete(self):
97
+ if self._measuring:
98
+ # self.user_readback.clear_sub(self._aggregate)
99
+ self._measuring = False
100
+ completion_status = DeviceStatus(self)
101
+ completion_status.set_finished()
102
+ self._time_resolution = None
103
+ return completion_status
104
+
105
+ def describe_collect(self):
106
+ if hasattr(self, "user_readback"):
107
+ readback = self.user_readback
108
+ else:
109
+ readback = self.readback
110
+
111
+ dd = dict(
112
+ {
113
+ readback.name: {
114
+ "source": readback.pvname,
115
+ "dtype": "number",
116
+ "shape": [],
117
+ }
118
+ }
119
+ )
120
+ return {self.name + "_monitor": dd}
121
+
122
+
123
+ class DeadbandMixin(Device, PositionerBase):
124
+ """
125
+ Should be the leftmost class in the inheritance list so that it grabs move first!
126
+
127
+ Must be combined with either EpicsMotor or PVPositioner, or some other class
128
+ that has a done_value attribute
129
+
130
+ An EpicsMotor subclass that has an absolute tolerance for moves.
131
+ If the readback is within tolerance of the setpoint, the MoveStatus
132
+ is marked as finished, even if the motor is still settling.
133
+
134
+ This prevents motors with long, but irrelevant, settling times from
135
+ adding overhead to scans.
136
+ """
137
+
138
+ tolerance = Cpt(Signal, value=-1, kind="config")
139
+ move_latch = Cpt(Signal, value=0, kind="omitted")
140
+
141
+ def __init__(self, *args, tolerance=None, **kwargs):
142
+ super().__init__(*args, **kwargs)
143
+ if tolerance is not None:
144
+ self.tolerance.put(tolerance)
145
+
146
+ def _done_moving(self, success=True, timestamp=None, value=None, **kwargs):
147
+ """Call when motion has completed. Runs ``SUB_DONE`` subscription."""
148
+ if self.move_latch.get():
149
+ #print(f"{timestamp} {datetime.today().time().isoformat()} [{self.name}]: marked done")
150
+ if success:
151
+ self._run_subs(sub_type=self.SUB_DONE, timestamp=timestamp, value=value)
152
+
153
+ self._run_subs(
154
+ sub_type=self._SUB_REQ_DONE, success=success, timestamp=timestamp
155
+ )
156
+ self._reset_sub(self._SUB_REQ_DONE)
157
+ self.move_latch.put(0)
158
+
159
+ def move(self, position, wait=True, **kwargs):
160
+ tolerance = self.tolerance.get()
161
+
162
+ if tolerance < 0:
163
+ self.move_latch.put(1)
164
+ return super().move(position, wait=wait, **kwargs)
165
+ else:
166
+ status = super().move(position, wait=False, **kwargs)
167
+ setpoint = position
168
+ done_value = getattr(self, "done_value", 1)
169
+
170
+ def check_deadband(value, timestamp, **kwargs):
171
+ if abs(value - setpoint) < tolerance:
172
+ #print(f"{timestamp} {datetime.today().time().isoformat()} [{self.name}]: {value} within {tolerance} of {setpoint}")
173
+ self._done_moving(
174
+ timestamp=timestamp, success=True, value=done_value
175
+ )
176
+ else:
177
+ pass
178
+ # print(f"{timestamp}: {self.name}, {value} not within {tolerance} of {setpoint}")
179
+
180
+ def clear_deadband(*args, **kwargs):
181
+ # print(f"{timestamp}: Ran deadband clear for {self.name}")
182
+ self.clear_sub(check_deadband, event_type=self.SUB_READBACK)
183
+
184
+ self.subscribe(clear_deadband, event_type=self._SUB_REQ_DONE, run=False)
185
+ self.move_latch.put(1)
186
+ self.subscribe(check_deadband, event_type=self.SUB_READBACK, run=True)
187
+
188
+ try:
189
+ if wait:
190
+ status_wait(status)
191
+ except KeyboardInterrupt:
192
+ self.stop()
193
+ raise
194
+
195
+ return status
196
+
197
+
198
+ class DeadbandEpicsMotor(DeadbandMixin, EpicsMotor):
199
+ """
200
+ An EpicsMotor subclass that has an absolute tolerance for moves.
201
+ If the readback is within tolerance of the setpoint, the MoveStatus
202
+ is marked as finished, even if the motor is still settling.
203
+
204
+ This prevents motors with long, but irrelevant, settling times from
205
+ adding overhead to scans.
206
+
207
+ This class is designed to be subclassed.
208
+ """
209
+
210
+ pass
211
+
212
+
213
+ class DeadbandPVPositioner(DeadbandMixin, PVPositioner):
214
+ """
215
+ A PVPositioner subclass that has an absolute tolerance for moves.
216
+ If the readback is within tolerance of the setpoint, the MoveStatus
217
+ is marked as finished, even if the motor is still settling.
218
+
219
+ This prevents motors with long, but irrelevant, settling times from
220
+ adding overhead to scans.
221
+
222
+ This class is designed to be subclassed.
223
+ """
224
+
225
+ pass
226
+
227
+
228
+ class FlyableMotor(EpicsMotor, FlyerMixin):
229
+ pass
230
+
231
+
232
+ class PseudoSingle(_PS):
233
+ """
234
+ A PseudoSingle subclass that casts positions to a float in order to ensure
235
+ that motor values don't get interpreted as int by Bluesky/Tiled.
236
+
237
+ If the first motor value is an int (i.e, 700 eV for the mono), Tiled will
238
+ try to cast all subsequent motor positions to an int as well.
239
+ """
240
+
241
+ def move(self, position, *args, **kwargs):
242
+ return super().move(float(position), *args, **kwargs)