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/samples.py ADDED
@@ -0,0 +1,130 @@
1
+ from .beamline import GLOBAL_BEAMLINE
2
+ from .help import add_to_func_list, add_to_plan_list
3
+ from nbs_bl.plans.plan_stubs import sampleholder_set_sample, sampleholder_move_sample
4
+
5
+
6
+ @add_to_plan_list
7
+ def set_sample(sample_id):
8
+ """Set the current sample without moving the sample holder.
9
+
10
+ Parameters
11
+ ----------
12
+ sample_id : str
13
+ Identifier of the sample to select
14
+ """
15
+ yield from sampleholder_set_sample(GLOBAL_BEAMLINE.primary_sampleholder, sample_id)
16
+
17
+
18
+ @add_to_plan_list
19
+ def move_sample(sample_id, **position):
20
+ """Move to a sample position and set it as the current sample.
21
+
22
+ Parameters
23
+ ----------
24
+ sample_id : str
25
+ Identifier of the sample to move to
26
+ **position : dict
27
+ Additional offset position parameters (x, y, r, etc.)
28
+ """
29
+ yield from sampleholder_move_sample(
30
+ GLOBAL_BEAMLINE.primary_sampleholder, sample_id, **position
31
+ )
32
+
33
+
34
+ @add_to_func_list
35
+ def load_samples(filename):
36
+ """Load sample definitions from a file.
37
+
38
+ Parameters
39
+ ----------
40
+ filename : str
41
+ Path to the sample definition file
42
+ """
43
+ sampleholder = GLOBAL_BEAMLINE.primary_sampleholder
44
+ sampleholder.load_sample_file(filename)
45
+
46
+
47
+ @add_to_func_list
48
+ def list_samples():
49
+ """Print a list of all samples currently loaded in the sampleholder."""
50
+ print("Samples loaded in sampleholder")
51
+ for sample_id, sample in GLOBAL_BEAMLINE.primary_sampleholder.samples.items():
52
+ print(f"{sample['name']}: id {sample_id}")
53
+
54
+
55
+ @add_to_func_list
56
+ def print_selected_sample():
57
+ """Print information about the currently selected sample."""
58
+ sample = GLOBAL_BEAMLINE.primary_sampleholder.current_sample
59
+ if sample is not None:
60
+ print(f"Current sample id: {sample['sample_id']}")
61
+ print(f"Current sample name: {sample.get('name', '')}")
62
+ else:
63
+ print("No sample currently selected")
64
+
65
+
66
+ @add_to_func_list
67
+ def add_sample_absolute(name, sample_id, coordinates, description=None, **kwargs):
68
+ """Add a sample at a set of absolute coordinates.
69
+
70
+ Parameters
71
+ ----------
72
+ name : str
73
+ Name of the sample
74
+ sample_id : str
75
+ Unique identifier for the sample
76
+ coordinates : list
77
+ List of coordinates for sample position
78
+ description : str, optional
79
+ Description of the sample
80
+ **kwargs : dict
81
+ Additional sample metadata
82
+ """
83
+ sample_id = str(sample_id)
84
+ GLOBAL_BEAMLINE.primary_sampleholder.add_sample(
85
+ name,
86
+ sample_id,
87
+ {"coordinates": coordinates},
88
+ description=description,
89
+ origin="absolute",
90
+ **kwargs,
91
+ )
92
+
93
+
94
+ @add_to_func_list
95
+ def add_current_position_as_sample(name, sample_id, description=None, **kwargs):
96
+ """Add a sample at the current sampleholder position.
97
+
98
+ Parameters
99
+ ----------
100
+ name : str
101
+ Name of the sample
102
+ sample_id : str
103
+ Unique identifier for the sample
104
+ description : str, optional
105
+ Description of the sample
106
+ **kwargs : dict
107
+ Additional sample metadata
108
+ """
109
+ sample_id = str(sample_id)
110
+ GLOBAL_BEAMLINE.primary_sampleholder.add_current_position_as_sample(
111
+ name, sample_id, description=description, **kwargs
112
+ )
113
+
114
+
115
+ @add_to_func_list
116
+ def clear_samples():
117
+ """Remove all samples from the sampleholder."""
118
+ GLOBAL_BEAMLINE.primary_sampleholder.clear_samples()
119
+
120
+
121
+ @add_to_func_list
122
+ def remove_sample(sample_id):
123
+ """Remove a specific sample from the sampleholder.
124
+
125
+ Parameters
126
+ ----------
127
+ sample_id : str
128
+ Identifier of the sample to remove
129
+ """
130
+ GLOBAL_BEAMLINE.primary_sampleholder.remove_sample(sample_id)
nbs_bl/settings.py ADDED
@@ -0,0 +1,68 @@
1
+ from os.path import exists
2
+ from .status import StatusDict
3
+ from .queueserver import add_status
4
+
5
+ try:
6
+ import tomllib
7
+ except ModuleNotFoundError:
8
+ import tomli as tomllib
9
+
10
+ """
11
+ Example Beamline Settings:
12
+ What we need to capture
13
+
14
+ [settings]
15
+ modules = ['ucal.startup']
16
+ regions = ['regions.toml']
17
+
18
+ [settings.redis]
19
+ host = "redis"
20
+ prefix = "nexafs-"
21
+
22
+ """
23
+ GLOBAL_SETTINGS = StatusDict()
24
+ add_status("SETTINGS", GLOBAL_SETTINGS)
25
+
26
+
27
+ _default_settings = {
28
+ "device_filename": "devices.toml",
29
+ "beamline_filename": "beamline.toml",
30
+ }
31
+
32
+
33
+ def load_settings(settings_file):
34
+ """
35
+ Load settings from a TOML file.
36
+
37
+ Parameters
38
+ ----------
39
+ settings_file : str
40
+ The path to the settings file.
41
+
42
+ Returns
43
+ -------
44
+ dict
45
+ The settings loaded from the file.
46
+ """
47
+ # Things that are currently in ucal configuration/settings
48
+ settings_dict = {}
49
+ settings_dict.update(_default_settings)
50
+ if not exists(settings_file):
51
+ print("No settings found, using defaults")
52
+ config = {}
53
+ else:
54
+ with open(settings_file, "rb") as f:
55
+ config = tomllib.load(f)
56
+ settings_dict.update(config.get("settings", {}))
57
+ GLOBAL_SETTINGS.update(settings_dict)
58
+
59
+
60
+ """
61
+ s BeamlineSettings:
62
+ device_filename = "devices.toml"
63
+ beamline_filename = "beamline.toml"
64
+ beamline_prefix = "sst"
65
+ modules = []
66
+
67
+ settings = BeamlineSettings()
68
+ """
nbs_bl/shutters.py ADDED
@@ -0,0 +1,39 @@
1
+ from .beamline import GLOBAL_BEAMLINE
2
+ from .help import add_to_plan_list, add_to_func_list
3
+ from bluesky.plan_stubs import rd
4
+
5
+
6
+ @add_to_plan_list
7
+ def open_shutter():
8
+ """Opens all default shutters, does not check any other shutters!"""
9
+ shutter = GLOBAL_BEAMLINE.default_shutter
10
+ if shutter is not None:
11
+ yield from shutter.open()
12
+
13
+
14
+ @add_to_plan_list
15
+ def close_shutter():
16
+ """Closes all default shutters"""
17
+ shutter = GLOBAL_BEAMLINE.default_shutter
18
+ if shutter is not None:
19
+ yield from shutter.close()
20
+
21
+
22
+ @add_to_plan_list
23
+ def is_shutter_open():
24
+ states = []
25
+ for s in GLOBAL_BEAMLINE.shutters.values():
26
+ state = yield from rd(s.state)
27
+ states.append(state == s.openval)
28
+ return all(states)
29
+
30
+
31
+ @add_to_func_list
32
+ def list_shutters():
33
+ def textFunction(key, device):
34
+ name = device.name
35
+ state = "Open" if device.state.get() == device.openval else "Closed"
36
+ text = f"{key}: {name}; {state}"
37
+ return text
38
+
39
+ GLOBAL_BEAMLINE.shutters.describe(textFunction=textFunction)
nbs_bl/sim/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .utils import DummyObject
2
+ from .energy import EnPosFactory
Binary file
nbs_bl/sim/energy.py ADDED
@@ -0,0 +1,403 @@
1
+ from ophyd import Component as Cpt, EpicsMotor, PseudoSingle
2
+ from ophyd import PseudoPositioner, Signal, PVPositioner, EpicsSignal, EpicsSignalRO
3
+ from ophyd.pseudopos import pseudo_position_argument, real_position_argument
4
+ import pathlib
5
+ import numpy as np
6
+ import xarray as xr
7
+ from nbs_bl.devices import DeadbandEpicsMotor, DeadbandMixin
8
+
9
+
10
+ class FMB_Mono_Grating_Type(PVPositioner):
11
+ setpoint = Cpt(EpicsSignal, "_TYPE_SP", string=True)
12
+ readback = Cpt(EpicsSignal, "_TYPE_MON", string=True)
13
+ actuate = Cpt(EpicsSignal, "_DCPL_CALC.PROC")
14
+ enable = Cpt(EpicsSignal, "_ENA_CMD.PROC")
15
+ kill = Cpt(EpicsSignal, "_KILL_CMD.PROC")
16
+ home = Cpt(EpicsSignal, "_HOME_CMD.PROC")
17
+ clear_encoder_loss = Cpt(EpicsSignal, "_ENC_LSS_CLR_CMD.PROC")
18
+ done = Cpt(EpicsSignal, "_AXIS_STS")
19
+
20
+
21
+ class Monochromator(DeadbandMixin, PVPositioner):
22
+ gratingx = Cpt(FMB_Mono_Grating_Type, "GrtX}Mtr", kind="config")
23
+ cff = Cpt(EpicsSignal, ":CFF_SP", name="Mono CFF", kind="config", auto_monitor=True)
24
+ setpoint = Cpt(EpicsSignal, ":ENERGY_SP", kind="normal")
25
+ readback = Cpt(EpicsSignalRO, ":ENERGY_MON", kind="hinted")
26
+ done = Cpt(EpicsSignal, ":ERDY_STS", kind="hinted")
27
+
28
+
29
+ def EnPosFactory(prefix, *, name, beamline=None, **kwargs):
30
+ if beamline is not None:
31
+ rotation_motor = beamline.devices.get("manipr", None)
32
+ else:
33
+ rotation_motor = None
34
+ return EnPos(prefix, rotation_motor=rotation_motor, name=name, **kwargs)
35
+
36
+
37
+ class EnPos(PseudoPositioner):
38
+ """Energy pseudopositioner class.
39
+ Parameters:
40
+ -----------
41
+ """
42
+
43
+ # synthetic axis
44
+ energy = Cpt(PseudoSingle, kind="hinted", limits=(71, 2250), name="Beamline Energy")
45
+ polarization = Cpt(
46
+ PseudoSingle, kind="config", limits=(-1, 180), name="X-ray Polarization"
47
+ )
48
+ sample_polarization = Cpt(
49
+ PseudoSingle, kind="config", name="Sample X-ray polarization"
50
+ )
51
+ # real motors
52
+
53
+ monoen = Cpt(Monochromator, "MonoMtr", kind="hinted", name="Mono Energy")
54
+ epugap = Cpt(DeadbandEpicsMotor, "GapMtr", kind="config", name="EPU Gap")
55
+ epuphase = Cpt(DeadbandEpicsMotor, "PhaseMtr", kind="config", name="EPU Phase")
56
+ # mir3Pitch = Cpt(FMBHexapodMirrorAxisStandAlonePitch,
57
+ # "XF:07ID1-OP{Mir:M3ABC", kind="normal",
58
+ # name="M3Pitch")
59
+ epumode = Cpt(EpicsMotor, "ModeMtr", name="EPU Mode", kind="config")
60
+ # _real = ['monoen'] # uncomment to cut EPU out of the real positioners and just use mono
61
+
62
+ sim_epu_mode = Cpt(
63
+ Signal, value=0, name="dont interact with the real EPU", kind="config"
64
+ )
65
+ scanlock = Cpt(
66
+ Signal, value=0, name="Lock Harmonic, Pitch, Grating for scan", kind="config"
67
+ )
68
+ harmonic = Cpt(Signal, value=1, name="EPU Harmonic", kind="config")
69
+ m3offset = Cpt(Signal, value=7.91, name="EPU Harmonic", kind="config")
70
+ rotation_motor = None
71
+
72
+ def __init__(
73
+ self,
74
+ a,
75
+ rotation_motor=None,
76
+ configpath=pathlib.Path(__file__).parent.absolute() / "config",
77
+ **kwargs
78
+ ):
79
+ self.gap_fit = np.zeros((10, 10))
80
+ self.gap_fit[0][:] = [
81
+ 889.981,
82
+ 222.966,
83
+ -0.945368,
84
+ 0.00290731,
85
+ -5.87973e-06,
86
+ 7.80556e-09,
87
+ -6.69661e-12,
88
+ 3.56679e-15,
89
+ -1.07195e-18,
90
+ 1.39775e-22,
91
+ ]
92
+ self.gap_fit[1][:] = [
93
+ -51.6545,
94
+ -1.60757,
95
+ 0.00914746,
96
+ -2.65003e-05,
97
+ 4.46303e-08,
98
+ -4.8934e-11,
99
+ 3.51531e-14,
100
+ -1.4802e-17,
101
+ 2.70647e-21,
102
+ 0,
103
+ ]
104
+ self.gap_fit[2][:] = [
105
+ 9.74128,
106
+ 0.0528884,
107
+ -0.000270428,
108
+ 6.71135e-07,
109
+ -6.68204e-10,
110
+ 2.71974e-13,
111
+ -2.82766e-17,
112
+ -3.77566e-21,
113
+ 0,
114
+ 0,
115
+ ]
116
+ self.gap_fit[3][:] = [
117
+ -2.94165,
118
+ -0.00110173,
119
+ 3.13309e-06,
120
+ -1.21787e-08,
121
+ 1.21638e-11,
122
+ -4.27216e-15,
123
+ 3.59552e-19,
124
+ 0,
125
+ 0,
126
+ 0,
127
+ ]
128
+ self.gap_fit[4][:] = [
129
+ 0.19242,
130
+ 2.19545e-05,
131
+ 6.11159e-08,
132
+ 4.21707e-11,
133
+ -6.84942e-14,
134
+ 1.84302e-17,
135
+ 0,
136
+ 0,
137
+ 0,
138
+ 0,
139
+ ]
140
+ self.gap_fit[5][:] = [
141
+ -0.00615458,
142
+ -9.55015e-07,
143
+ -1.28929e-09,
144
+ 4.28363e-13,
145
+ 3.26302e-17,
146
+ 0,
147
+ 0,
148
+ 0,
149
+ 0,
150
+ 0,
151
+ ]
152
+ self.gap_fit[6][:] = [
153
+ 0.000113341,
154
+ 1.90112e-08,
155
+ 6.92088e-12,
156
+ -1.87659e-15,
157
+ 0,
158
+ 0,
159
+ 0,
160
+ 0,
161
+ 0,
162
+ 0,
163
+ ]
164
+ self.gap_fit[7][:] = [
165
+ -1.22095e-06,
166
+ -1.5686e-10,
167
+ -1.09857e-14,
168
+ 0,
169
+ 0,
170
+ 0,
171
+ 0,
172
+ 0,
173
+ 0,
174
+ 0,
175
+ ]
176
+ self.gap_fit[8][:] = [7.13593e-09, 4.69949e-13, 0, 0, 0, 0, 0, 0, 0, 0]
177
+ self.gap_fit[9][:] = [-1.74622e-11, 0, 0, 0, 0, 0, 0, 0, 0, 0]
178
+
179
+ self.polphase = xr.load_dataarray(configpath / "polphase.nc")
180
+ self.phasepol = xr.DataArray(
181
+ data=self.polphase.pol,
182
+ coords={"phase": self.polphase.values},
183
+ dims={"phase"},
184
+ )
185
+ self.rotation_motor = rotation_motor
186
+ super().__init__(a, **kwargs)
187
+
188
+ self.epugap.tolerance.set(3)
189
+ self.epuphase.tolerance.set(10)
190
+ self.monoen.tolerance.set(0.01)
191
+ self._ready_to_fly = False
192
+ self._fly_move_st = None
193
+ self._default_time_resolution = 0.05
194
+ self._flyer_lag_ev = 0.1
195
+ self._flyer_gap_lead = 0.0
196
+ self._time_resolution = None
197
+ self._flying = False
198
+
199
+ @pseudo_position_argument
200
+ def forward(self, pseudo_pos):
201
+ """Run a forward (pseudo -> real) calculation"""
202
+ # print('In forward')
203
+ epu_sim = self.sim_epu_mode.get()
204
+ if epu_sim:
205
+ ret = self.RealPosition(monoen=pseudo_pos.energy)
206
+ else:
207
+ ret = self.RealPosition(
208
+ epugap=self.gap(
209
+ pseudo_pos.energy,
210
+ pseudo_pos.polarization,
211
+ self.scanlock.get(),
212
+ epu_sim,
213
+ ),
214
+ monoen=pseudo_pos.energy,
215
+ epuphase=abs(
216
+ self.phase(pseudo_pos.energy, pseudo_pos.polarization, epu_sim)
217
+ ),
218
+ # mir3Pitch=self.m3pitchcalc(pseudo_pos.energy, self.scanlock.get()),
219
+ epumode=self.mode(pseudo_pos.polarization, epu_sim),
220
+ # harmonic=self.choose_harmonic(pseudo_pos.energy,pseudo_pos.polarization,self.scanlock.get())
221
+ )
222
+ # print('finished forward')
223
+ return ret
224
+
225
+ @real_position_argument
226
+ def inverse(self, real_pos):
227
+ """Run an inverse (real -> pseudo) calculation"""
228
+ # print('in Inverse')
229
+ epu_sim = self.sim_epu_mode.get()
230
+ if epu_sim:
231
+ ret = self.PseudoPosition(
232
+ energy=real_pos.monoen,
233
+ polarization=self.pol(self.epuphase.position, self.epumode.position),
234
+ sample_polarization=self.sample_pol(
235
+ self.pol(self.epuphase.position, self.epumode.position)
236
+ ),
237
+ )
238
+ else:
239
+ ret = self.PseudoPosition(
240
+ energy=real_pos.monoen,
241
+ polarization=self.pol(real_pos.epuphase, real_pos.epumode),
242
+ sample_polarization=self.sample_pol(
243
+ self.pol(real_pos.epuphase, real_pos.epumode)
244
+ ),
245
+ )
246
+ # print('Finished inverse')
247
+ return ret
248
+
249
+ def gap(self, energy, pol, locked, sim=0):
250
+ if sim:
251
+ return (
252
+ self.epugap.get()
253
+ ) # never move the gap if we are in simulated gap mode
254
+ # this might cause problems if someone else is moving the gap, we might move it back
255
+ # but I think this is not a common reason for this mode
256
+
257
+ # self.harmonic.set(self.choose_harmonic(energy, pol, locked))
258
+ energy = energy / self.harmonic.get()
259
+
260
+ if pol == -1:
261
+ encalc = energy - 105.002
262
+ gap = 13979.0
263
+ gap += 82.857 * encalc**1
264
+ gap += -0.26294 * encalc**2
265
+ gap += 0.00090199 * encalc**3
266
+ gap += -2.3176e-06 * encalc**4
267
+ gap += 4.205e-09 * encalc**5
268
+ gap += -5.139e-12 * encalc**6
269
+ gap += 4.0034e-15 * encalc**7
270
+ gap += -1.7862e-18 * encalc**8
271
+ gap += 3.4687e-22 * encalc**9
272
+ return max(14000.0, min(100000.0, gap))
273
+ elif pol == -0.5:
274
+ encalc = energy - 104.996
275
+ gap = 14013.0
276
+ gap += 82.76 * encalc**1
277
+ gap += -0.26128 * encalc**2
278
+ gap += 0.00088353 * encalc**3
279
+ gap += -2.2149e-06 * encalc**4
280
+ gap += 3.8919e-09 * encalc**5
281
+ gap += -4.5887e-12 * encalc**6
282
+ gap += 3.4467e-15 * encalc**7
283
+ gap += -1.4851e-18 * encalc**8
284
+ gap += 2.795e-22 * encalc**9
285
+ return max(14000.0, min(100000.0, gap))
286
+ elif 0 <= pol <= 90:
287
+ return max(14000.0, min(100000.0, self.epu_gap(energy, pol)))
288
+ elif 90 < pol <= 180:
289
+ return max(14000.0, min(100000.0, self.epu_gap(energy, 180.0 - pol)))
290
+ else:
291
+ return np.nan
292
+
293
+ def epu_gap(self, en, pol):
294
+ """
295
+ calculate the epu gap from the energy and polarization, using a 2D polynomial fit
296
+ @param en: energy (valid between ~70 and 1300
297
+ @param pol: polarization (valid between 0 and 90)
298
+ @return: gap in microns
299
+ """
300
+ x = float(en)
301
+ y = float(pol)
302
+ z = 0.0
303
+ for i in np.arange(self.gap_fit.shape[0]):
304
+ for j in np.arange(self.gap_fit.shape[1]):
305
+ z += self.gap_fit[j, i] * (x**i) * (y**j)
306
+ return z
307
+
308
+ def phase(self, en, pol, sim=0):
309
+ if sim:
310
+ return (
311
+ self.epuphase.get()
312
+ ) # never move the gap if we are in simulated gap mode
313
+ # this might cause problems if someone else is moving the gap, we might move it back
314
+ # but I think this is not a common reason for this mode
315
+ if pol == -1:
316
+ return 15000
317
+ elif pol == -0.5:
318
+ return 15000
319
+ elif 90 < pol <= 180:
320
+ return -min(
321
+ 29500.0,
322
+ max(0.0, float(self.polphase.interp(pol=180 - pol, method="cubic"))),
323
+ )
324
+ else:
325
+ return min(
326
+ 29500.0, max(0.0, float(self.polphase.interp(pol=pol, method="cubic")))
327
+ )
328
+
329
+ def pol(self, phase, mode):
330
+ if mode == 0:
331
+ return -1
332
+ elif mode == 1:
333
+ return -0.5
334
+ elif mode == 2:
335
+ return float(self.phasepol.interp(phase=np.abs(phase), method="cubic"))
336
+ elif mode == 3:
337
+ return 180 - float(
338
+ self.phasepol.interp(phase=np.abs(phase), method="cubic")
339
+ )
340
+
341
+ def mode(self, pol, sim=0):
342
+ """
343
+ @param pol:
344
+ @return:
345
+ """
346
+ if sim:
347
+ return (
348
+ self.epumode.get()
349
+ ) # never move the gap if we are in simulated gap mode
350
+ # this might cause problems if someone else is moving the gap, we might move it back
351
+ # but I think this is not a common reason for this mode
352
+ if pol == -1:
353
+ return 0
354
+ elif pol == -0.5:
355
+ return 1
356
+ elif 90 < pol <= 180:
357
+ return 3
358
+ else:
359
+ return 2
360
+
361
+ def sample_pol(self, pol):
362
+ if self.rotation_motor is not None:
363
+ th = self.rotation_motor.user_setpoint.get()
364
+ else:
365
+ th = 0
366
+ return (
367
+ np.arccos(np.cos(pol * np.pi / 180) * np.sin(th * np.pi / 180))
368
+ * 180
369
+ / np.pi
370
+ )
371
+
372
+ def m3pitchcalc(self, energy, locked):
373
+ pitch = self.mir3Pitch.setpoint.get()
374
+ if locked:
375
+ return pitch
376
+ elif "1200" in self.monoen.gratingx.readback.get():
377
+ pitch = (
378
+ self.m3offset.get()
379
+ + 0.038807 * np.exp(-(energy - 100) / 91.942)
380
+ + 0.050123 * np.exp(-(energy - 100) / 1188.9)
381
+ )
382
+ elif "250l/mm" in self.monoen.gratingx.readback.get():
383
+ pitch = (
384
+ self.m3offset.get()
385
+ + 0.022665 * np.exp(-(energy - 90) / 37.746)
386
+ + 0.024897 * np.exp(-(energy - 90) / 450.9)
387
+ )
388
+ elif "RSoXS" in self.monoen.gratingx.readback.get():
389
+ pitch = (
390
+ self.m3offset.get()
391
+ - 0.017669 * np.exp(-(energy - 100) / 41.742)
392
+ - 0.068631 * np.exp(-(energy - 100) / 302.75)
393
+ )
394
+
395
+ return round(100 * pitch) / 100
396
+
397
+ def choose_harmonic(self, energy, pol, locked):
398
+ if locked:
399
+ return self.harmonic.get()
400
+ elif energy < 1200:
401
+ return 1
402
+ else:
403
+ return 3
@@ -0,0 +1,14 @@
1
+ from nbs_bl.devices.sampleholders import Manipulator4AxBase, Manipulator1AxBase
2
+ from nbs_bl.geometry.bars import Standard4SidedBar, Bar1d
3
+ from nbs_bl.devices.sampleholders import manipulatorFactory4Ax
4
+
5
+
6
+ def ManipulatorBuilder(prefix, *, name, **kwargs):
7
+ holder = Standard4SidedBar(24.5, 215)
8
+ origin = (0, 0, 464)
9
+ Manipulator = manipulatorFactory4Ax(
10
+ "SampX}Mtr", "SampY}Mtr", "SampZ}Mtr", "SampTh}Mtr"
11
+ )
12
+ return Manipulator(
13
+ prefix, name=name, attachment_point=origin, holder=holder, **kwargs
14
+ )
nbs_bl/sim/utils.py ADDED
@@ -0,0 +1,36 @@
1
+ from ophyd import Device
2
+
3
+
4
+ class DummyObject(Device):
5
+ """
6
+ A dummy device that creates child DummyObjects on attribute access.
7
+
8
+ Parameters
9
+ ----------
10
+ name : str
11
+ Name of the dummy device
12
+
13
+ Examples
14
+ --------
15
+ >>> manip = DummyObject(name="Manipulator")
16
+ >>> manip.x # Returns DummyObject named "Manipulator_x"
17
+ >>> manip.x.readback # Returns DummyObject named "Manipulator_x_readback"
18
+ """
19
+
20
+ def __init__(self, *args, name, **kwargs):
21
+ super().__init__(*args, name=name)
22
+ self._dummy_children = {}
23
+
24
+ def __getattr__(self, attr):
25
+ """Create and return a new DummyObject when an unknown attribute is accessed."""
26
+ if attr.startswith("_"):
27
+ return super().__getattr__(attr)
28
+
29
+ # Create child name by appending the attribute
30
+ child_name = f"{self.name}_{attr}"
31
+
32
+ # Cache child objects to return the same instance on subsequent access
33
+ if child_name not in self._dummy_children:
34
+ self._dummy_children[child_name] = DummyObject(name=child_name)
35
+
36
+ return self._dummy_children[child_name]