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
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
|
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]
|