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/configuration.py
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
from os.path import join
|
|
2
|
+
from importlib.util import find_spec
|
|
3
|
+
from importlib.metadata import entry_points
|
|
4
|
+
from .beamline import GLOBAL_BEAMLINE, BeamlineModel
|
|
5
|
+
from .queueserver import request_update, get_status, GLOBAL_USER_STATUS
|
|
6
|
+
from .run_engine import create_run_engine
|
|
7
|
+
from .hw import loadDevices
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from os.path import join, exists
|
|
10
|
+
try:
|
|
11
|
+
import tomllib
|
|
12
|
+
except ModuleNotFoundError:
|
|
13
|
+
import tomli as tomllib
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_startup_dir():
|
|
17
|
+
"""
|
|
18
|
+
Get the IPython startup directory.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
str
|
|
23
|
+
The path to the IPython startup directory.
|
|
24
|
+
"""
|
|
25
|
+
ip = get_ipython()
|
|
26
|
+
startup_dir = ip.profile_dir.startup_dir
|
|
27
|
+
return startup_dir
|
|
28
|
+
|
|
29
|
+
def get_default_initializer():
|
|
30
|
+
ip = get_ipython()
|
|
31
|
+
if ip.user_ns.get("beamline_initializer") is None:
|
|
32
|
+
ip.user_ns["beamline_initializer"] = BeamlineInitializer(GLOBAL_BEAMLINE)
|
|
33
|
+
return ip.user_ns["beamline_initializer"]
|
|
34
|
+
|
|
35
|
+
def load_and_configure_everything(startup_dir=None, initializer=None):
|
|
36
|
+
"""
|
|
37
|
+
Load and configure all necessary hardware and settings for the beamline.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
startup_dir : str, optional
|
|
42
|
+
The directory from which to load configuration files.
|
|
43
|
+
If not specified, uses the IPython startup directory.
|
|
44
|
+
"""
|
|
45
|
+
if startup_dir is None:
|
|
46
|
+
startup_dir = get_startup_dir()
|
|
47
|
+
|
|
48
|
+
ip = get_ipython()
|
|
49
|
+
ip.user_ns["get_status"] = get_status
|
|
50
|
+
ip.user_ns["request_update"] = request_update
|
|
51
|
+
|
|
52
|
+
if initializer is None:
|
|
53
|
+
initializer = BeamlineInitializer(GLOBAL_BEAMLINE)
|
|
54
|
+
ip.user_ns["beamline_initializer"] = initializer
|
|
55
|
+
initializer.initialize(startup_dir, ip.user_ns)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class InitializationStep(ABC):
|
|
59
|
+
"""
|
|
60
|
+
Base class for initialization steps.
|
|
61
|
+
|
|
62
|
+
Each step is independent and can be extended or customized.
|
|
63
|
+
Steps execute in sequence and share a context dictionary.
|
|
64
|
+
Steps can declare dependencies on other step classes.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
69
|
+
"""
|
|
70
|
+
Execute this initialization step.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
beamline : BeamlineModel
|
|
75
|
+
The beamline model to initialize
|
|
76
|
+
context : dict
|
|
77
|
+
Shared context between steps (startup_dir, namespace, etc.)
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
dict
|
|
82
|
+
Updated context (may add new keys for subsequent steps)
|
|
83
|
+
"""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def name(self) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Human-readable name for this step.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
str
|
|
95
|
+
Step name for logging and identification
|
|
96
|
+
"""
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def depends_on(self) -> list[type]:
|
|
101
|
+
"""
|
|
102
|
+
List of step classes that must be completed before this step can run.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
list[type]
|
|
107
|
+
List of InitializationStep subclasses that are dependencies.
|
|
108
|
+
Default is an empty list (no dependencies).
|
|
109
|
+
"""
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
class BlockStep(InitializationStep):
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def name(self) -> str:
|
|
116
|
+
return "Blocking"
|
|
117
|
+
|
|
118
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
119
|
+
raise RuntimeError("Stopping initialization for testing")
|
|
120
|
+
|
|
121
|
+
class LoadSettingsStep(InitializationStep):
|
|
122
|
+
"""Load settings from beamline.toml"""
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def name(self) -> str:
|
|
126
|
+
return "Load Settings"
|
|
127
|
+
|
|
128
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
129
|
+
_default_settings = {
|
|
130
|
+
"device_filename": "devices.toml",
|
|
131
|
+
"beamline_filename": "beamline.toml",
|
|
132
|
+
}
|
|
133
|
+
startup_dir = context["startup_dir"]
|
|
134
|
+
settings_file = join(startup_dir, context.get("beamline_filename", _default_settings["beamline_filename"]))
|
|
135
|
+
|
|
136
|
+
settings_dict = {}
|
|
137
|
+
settings_dict.update(_default_settings)
|
|
138
|
+
|
|
139
|
+
if not exists(settings_file):
|
|
140
|
+
print("No settings found, using defaults")
|
|
141
|
+
config = {}
|
|
142
|
+
else:
|
|
143
|
+
with open(settings_file, "rb") as f:
|
|
144
|
+
config = tomllib.load(f)
|
|
145
|
+
|
|
146
|
+
settings_dict.update(config.get("settings", {}))
|
|
147
|
+
beamline.settings.update(settings_dict)
|
|
148
|
+
beamline.settings["startup_dir"] = startup_dir
|
|
149
|
+
|
|
150
|
+
print(f" Settings: {beamline.settings}")
|
|
151
|
+
return context
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class LoadConfigurationFilesStep(InitializationStep):
|
|
155
|
+
"""Load and merge device and beamline configuration files"""
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def name(self) -> str:
|
|
159
|
+
return "Load Configuration Files"
|
|
160
|
+
|
|
161
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
162
|
+
startup_dir = context["startup_dir"]
|
|
163
|
+
device_file = join(startup_dir, beamline.settings["device_filename"])
|
|
164
|
+
beamline_file = join(startup_dir, beamline.settings["beamline_filename"])
|
|
165
|
+
|
|
166
|
+
with open(beamline_file, "rb") as f:
|
|
167
|
+
beamline_config = tomllib.load(f)
|
|
168
|
+
|
|
169
|
+
with open(device_file, "rb") as f:
|
|
170
|
+
device_config = tomllib.load(f)
|
|
171
|
+
|
|
172
|
+
beamline.config.update(beamline_config)
|
|
173
|
+
beamline.config["devices"] = device_config
|
|
174
|
+
|
|
175
|
+
context["device_config"] = device_config
|
|
176
|
+
return context
|
|
177
|
+
|
|
178
|
+
class UserInitializationHook(InitializationStep):
|
|
179
|
+
"""User initialization hook"""
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def name(self) -> str:
|
|
183
|
+
return "User Initialization Hook"
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def depends_on(self) -> list[type]:
|
|
187
|
+
return [LoadSettingsStep]
|
|
188
|
+
|
|
189
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
190
|
+
modules = beamline.settings.get("initialization_modules", [])
|
|
191
|
+
ip = get_ipython()
|
|
192
|
+
|
|
193
|
+
for module_name in modules:
|
|
194
|
+
module_path = find_spec(module_name).origin
|
|
195
|
+
print(f"Trying to import {module_name} from {module_path}")
|
|
196
|
+
ip.run_line_magic("run", module_path)
|
|
197
|
+
|
|
198
|
+
return context
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class InitializeRedisStep(InitializationStep):
|
|
202
|
+
"""Initialize Redis connections"""
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def name(self) -> str:
|
|
206
|
+
return "Initialize Redis"
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def depends_on(self) -> list[type]:
|
|
210
|
+
return [LoadConfigurationFilesStep]
|
|
211
|
+
|
|
212
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
213
|
+
redis_settings = (
|
|
214
|
+
beamline.config.get("settings", {})
|
|
215
|
+
.get("redis", {})
|
|
216
|
+
.get("info", {})
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
beamline.redis_settings = redis_settings
|
|
220
|
+
|
|
221
|
+
if redis_settings:
|
|
222
|
+
GLOBAL_USER_STATUS.init_redis(
|
|
223
|
+
host=redis_settings["host"],
|
|
224
|
+
port=redis_settings.get("port", None),
|
|
225
|
+
db=redis_settings.get("db", 0),
|
|
226
|
+
global_prefix=redis_settings.get("prefix", ""),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
tmp_settings = GLOBAL_USER_STATUS.request_status_dict(
|
|
230
|
+
"SETTINGS", use_redis=True
|
|
231
|
+
)
|
|
232
|
+
tmp_settings.update(beamline.settings)
|
|
233
|
+
beamline.settings = tmp_settings
|
|
234
|
+
print(f" Settings from Redis: {beamline.settings}")
|
|
235
|
+
|
|
236
|
+
beamline.plan_status = GLOBAL_USER_STATUS.request_status_dict(
|
|
237
|
+
"PLAN_STATUS", use_redis=True
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return context
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class InitializeMetadataStep(InitializationStep):
|
|
244
|
+
"""Initialize metadata Redis connection"""
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def name(self) -> str:
|
|
248
|
+
return "Initialize Metadata"
|
|
249
|
+
|
|
250
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
251
|
+
redis_md_settings = (
|
|
252
|
+
beamline.config.get("settings", {})
|
|
253
|
+
.get("redis", {})
|
|
254
|
+
.get("md", {})
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if redis_md_settings:
|
|
258
|
+
import redis
|
|
259
|
+
from nbs_bl.status import RedisStatusDict
|
|
260
|
+
from nbs_bl.redisDevice import _RedisSignal
|
|
261
|
+
|
|
262
|
+
mdredis = redis.Redis(
|
|
263
|
+
redis_md_settings["host"],
|
|
264
|
+
port=redis_md_settings.get("port", 6379),
|
|
265
|
+
db=redis_md_settings.get("db", 0),
|
|
266
|
+
)
|
|
267
|
+
beamline.md = RedisStatusDict(
|
|
268
|
+
mdredis, prefix=redis_md_settings.get("prefix", "")
|
|
269
|
+
)
|
|
270
|
+
GLOBAL_USER_STATUS.add_status("USER_MD", beamline.md)
|
|
271
|
+
_RedisSignal.set_default_status_provider(GLOBAL_USER_STATUS)
|
|
272
|
+
else:
|
|
273
|
+
beamline.md = GLOBAL_USER_STATUS.request_status_dict("USER_MD")
|
|
274
|
+
_RedisSignal.set_default_status_provider(GLOBAL_USER_STATUS)
|
|
275
|
+
|
|
276
|
+
context["namespace"].update({"md": beamline.md})
|
|
277
|
+
|
|
278
|
+
return context
|
|
279
|
+
|
|
280
|
+
class InitializeRunEngineStep(InitializationStep):
|
|
281
|
+
"""Initialize the run engine"""
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def name(self) -> str:
|
|
285
|
+
return "Initialize Run Engine"
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def depends_on(self) -> list[type]:
|
|
289
|
+
return [InitializeMetadataStep]
|
|
290
|
+
|
|
291
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
292
|
+
beamline.run_engine = create_run_engine(setup=True)
|
|
293
|
+
|
|
294
|
+
beamline.run_engine.md = beamline.md
|
|
295
|
+
context["namespace"].update({"RE": beamline.run_engine})
|
|
296
|
+
return context
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class LoadDevicesStep(InitializationStep):
|
|
300
|
+
"""Load devices from configuration (multi-pass)"""
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def name(self) -> str:
|
|
304
|
+
return "Load Devices"
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def depends_on(self) -> list[type]:
|
|
308
|
+
return [LoadConfigurationFilesStep]
|
|
309
|
+
|
|
310
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
311
|
+
device_config = beamline.config["devices"]
|
|
312
|
+
beamline._initialize_groups()
|
|
313
|
+
ns = context.get("namespace")
|
|
314
|
+
# Move all of this to a helper function in hw.py
|
|
315
|
+
devices = loadDevices(device_config, ns, mode="default")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
for device_name, device_info in devices.items():
|
|
320
|
+
beamline.add_device(device_name, device_info)
|
|
321
|
+
|
|
322
|
+
context["devices"] = devices
|
|
323
|
+
return context
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class SetupSpecialDevicesStep(InitializationStep):
|
|
327
|
+
"""Setup special devices like sampleholders"""
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def name(self) -> str:
|
|
331
|
+
return "Setup Special Devices"
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def depends_on(self) -> list[type]:
|
|
335
|
+
return [LoadDevicesStep]
|
|
336
|
+
|
|
337
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
338
|
+
beamline.handle_special_devices()
|
|
339
|
+
|
|
340
|
+
return context
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ConfigureBaselineStep(InitializationStep):
|
|
344
|
+
"""Configure baseline devices for bluesky"""
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def name(self) -> str:
|
|
348
|
+
return "Configure Baseline"
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def depends_on(self) -> list[type]:
|
|
352
|
+
return [LoadDevicesStep]
|
|
353
|
+
|
|
354
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
355
|
+
configuration = beamline.config.get("configuration", {})
|
|
356
|
+
baseline_groups = configuration.get("baseline", [])
|
|
357
|
+
all_device_config = beamline.config.get("devices", {})
|
|
358
|
+
|
|
359
|
+
for groupname in baseline_groups:
|
|
360
|
+
group = getattr(beamline, groupname, None)
|
|
361
|
+
if group:
|
|
362
|
+
for key in group.devices:
|
|
363
|
+
device_config = all_device_config.get(key, {})
|
|
364
|
+
should_add = device_config.get("_baseline", True)
|
|
365
|
+
if should_add:
|
|
366
|
+
beamline.add_to_baseline(key, False)
|
|
367
|
+
|
|
368
|
+
for key, device_config in all_device_config.items():
|
|
369
|
+
if device_config.get("_baseline", False):
|
|
370
|
+
if key in beamline.devices:
|
|
371
|
+
beamline.add_to_baseline(key, False)
|
|
372
|
+
|
|
373
|
+
return context
|
|
374
|
+
|
|
375
|
+
class UserStartupHook(InitializationStep):
|
|
376
|
+
"""Configure modules"""
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def name(self) -> str:
|
|
380
|
+
return "Configure Startup Modules"
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def depends_on(self) -> list[type]:
|
|
384
|
+
return [LoadSettingsStep]
|
|
385
|
+
|
|
386
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
387
|
+
modules = beamline.settings.get("startup_modules", [])
|
|
388
|
+
ip = get_ipython()
|
|
389
|
+
|
|
390
|
+
for module_name in modules:
|
|
391
|
+
module_path = find_spec(module_name).origin
|
|
392
|
+
print(f"Trying to import {module_name} from {module_path}")
|
|
393
|
+
ip.run_line_magic("run", module_path)
|
|
394
|
+
|
|
395
|
+
return context
|
|
396
|
+
|
|
397
|
+
class LoadPlansStep(InitializationStep):
|
|
398
|
+
"""Load plans from configuration files"""
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def name(self) -> str:
|
|
402
|
+
return "Load Plans"
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def depends_on(self) -> list[type]:
|
|
406
|
+
return [UserStartupHook]
|
|
407
|
+
|
|
408
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
409
|
+
"""
|
|
410
|
+
Load all plans using registered entry points.
|
|
411
|
+
|
|
412
|
+
Parameters
|
|
413
|
+
----------
|
|
414
|
+
startup_dir : str
|
|
415
|
+
Directory containing plan configuration files
|
|
416
|
+
"""
|
|
417
|
+
startup_dir = context["startup_dir"]
|
|
418
|
+
plan_settings = beamline.settings.get("plans", {})
|
|
419
|
+
print(f"Loading plans from {startup_dir}")
|
|
420
|
+
# Iterate through all registered plan loaders
|
|
421
|
+
for entry_point in entry_points(group="nbs_bl.plan_loaders"):
|
|
422
|
+
plan_type = entry_point.name
|
|
423
|
+
print(f"Loading {plan_type} plans")
|
|
424
|
+
plan_files = plan_settings.get(plan_type, [])
|
|
425
|
+
|
|
426
|
+
if not plan_files:
|
|
427
|
+
print(f"No {plan_type} plans found")
|
|
428
|
+
continue
|
|
429
|
+
print(f"Loading {plan_type} plans from {plan_files}")
|
|
430
|
+
# Load the plan loader function
|
|
431
|
+
plan_loader = entry_point.load()
|
|
432
|
+
|
|
433
|
+
# Load each plan file for this plan type
|
|
434
|
+
for plan_file in plan_files:
|
|
435
|
+
full_path = join(startup_dir, plan_file)
|
|
436
|
+
try:
|
|
437
|
+
plan_loader(full_path)
|
|
438
|
+
print(f"Loaded {plan_type} plans from {plan_file}")
|
|
439
|
+
except Exception as e:
|
|
440
|
+
print(f"Error loading {plan_type} plans from {plan_file}: {str(e)}")
|
|
441
|
+
|
|
442
|
+
return context
|
|
443
|
+
|
|
444
|
+
class InitializeGlobalNamespaceStep(InitializationStep):
|
|
445
|
+
"""Initialize the global namespace"""
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def name(self) -> str:
|
|
449
|
+
return "Initialize Global Namespace"
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def depends_on(self) -> list[type]:
|
|
453
|
+
return [InitializeRunEngineStep]
|
|
454
|
+
|
|
455
|
+
def execute(self, beamline: BeamlineModel, context: dict) -> dict:
|
|
456
|
+
from nbs_bl.help import GLOBAL_IMPORT_DICTIONARY
|
|
457
|
+
|
|
458
|
+
for key in GLOBAL_IMPORT_DICTIONARY:
|
|
459
|
+
if key not in context["namespace"]:
|
|
460
|
+
context["namespace"][key] = GLOBAL_IMPORT_DICTIONARY[key]
|
|
461
|
+
return context
|
|
462
|
+
|
|
463
|
+
class BeamlineInitializer:
|
|
464
|
+
"""
|
|
465
|
+
Orchestrates the initialization pipeline.
|
|
466
|
+
|
|
467
|
+
Manages the order and execution of initialization steps.
|
|
468
|
+
Tracks which steps have been completed to support partial initialization.
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
def __init__(self, beamline: BeamlineModel):
|
|
472
|
+
"""
|
|
473
|
+
Initialize the initializer with a beamline model.
|
|
474
|
+
|
|
475
|
+
Parameters
|
|
476
|
+
----------
|
|
477
|
+
beamline : BeamlineModel
|
|
478
|
+
The beamline model to initialize
|
|
479
|
+
"""
|
|
480
|
+
self.beamline = beamline
|
|
481
|
+
self.steps: list[InitializationStep] = []
|
|
482
|
+
self._completed_steps: set[int] = set()
|
|
483
|
+
self._completed_step_classes: set[type] = set()
|
|
484
|
+
self._context: dict = {}
|
|
485
|
+
self._register_default_steps()
|
|
486
|
+
|
|
487
|
+
def _register_default_steps(self):
|
|
488
|
+
"""Register the default initialization steps in order."""
|
|
489
|
+
self.steps = [
|
|
490
|
+
LoadSettingsStep(),
|
|
491
|
+
LoadConfigurationFilesStep(),
|
|
492
|
+
UserInitializationHook(),
|
|
493
|
+
InitializeRedisStep(),
|
|
494
|
+
InitializeMetadataStep(),
|
|
495
|
+
InitializeRunEngineStep(),
|
|
496
|
+
LoadDevicesStep(),
|
|
497
|
+
SetupSpecialDevicesStep(),
|
|
498
|
+
ConfigureBaselineStep(),
|
|
499
|
+
UserStartupHook(),
|
|
500
|
+
LoadPlansStep(),
|
|
501
|
+
InitializeGlobalNamespaceStep(),
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
def list_steps(self) -> list[str]:
|
|
505
|
+
"""
|
|
506
|
+
List the names of all steps in the initialization pipeline.
|
|
507
|
+
|
|
508
|
+
Returns
|
|
509
|
+
-------
|
|
510
|
+
list[str]
|
|
511
|
+
Names of all steps in the pipeline
|
|
512
|
+
"""
|
|
513
|
+
return [step.name for step in self.steps]
|
|
514
|
+
|
|
515
|
+
def add_step(
|
|
516
|
+
self, step: InitializationStep, after: str = None, before: str = None
|
|
517
|
+
):
|
|
518
|
+
"""
|
|
519
|
+
Add a custom initialization step.
|
|
520
|
+
|
|
521
|
+
Parameters
|
|
522
|
+
----------
|
|
523
|
+
step : InitializationStep
|
|
524
|
+
The step to add
|
|
525
|
+
after : str, optional
|
|
526
|
+
Insert after step with this name
|
|
527
|
+
before : str, optional
|
|
528
|
+
Insert before step with this name
|
|
529
|
+
|
|
530
|
+
Raises
|
|
531
|
+
------
|
|
532
|
+
ValueError
|
|
533
|
+
If both after and before are specified, or if the reference step is not found
|
|
534
|
+
"""
|
|
535
|
+
if after and before:
|
|
536
|
+
raise ValueError("Cannot specify both 'after' and 'before'")
|
|
537
|
+
|
|
538
|
+
if after:
|
|
539
|
+
try:
|
|
540
|
+
idx = next(i for i, s in enumerate(self.steps) if s.name == after)
|
|
541
|
+
self.steps.insert(idx + 1, step)
|
|
542
|
+
except StopIteration:
|
|
543
|
+
raise ValueError(f"Step '{after}' not found")
|
|
544
|
+
|
|
545
|
+
elif before:
|
|
546
|
+
try:
|
|
547
|
+
idx = next(i for i, s in enumerate(self.steps) if s.name == before)
|
|
548
|
+
self.steps.insert(idx, step)
|
|
549
|
+
except StopIteration:
|
|
550
|
+
raise ValueError(f"Step '{before}' not found")
|
|
551
|
+
else:
|
|
552
|
+
self.steps.append(step)
|
|
553
|
+
|
|
554
|
+
def remove_step(self, step_name: str) -> None:
|
|
555
|
+
"""
|
|
556
|
+
Remove a step from the initialization pipeline.
|
|
557
|
+
|
|
558
|
+
Parameters
|
|
559
|
+
----------
|
|
560
|
+
step_name : str
|
|
561
|
+
The name of the step to remove
|
|
562
|
+
"""
|
|
563
|
+
try:
|
|
564
|
+
idx = next(i for i, s in enumerate(self.steps) if s.name == step_name)
|
|
565
|
+
self.steps.pop(idx)
|
|
566
|
+
except StopIteration:
|
|
567
|
+
raise ValueError(f"Step '{step_name}' not found")
|
|
568
|
+
|
|
569
|
+
def replace_step(self, step: InitializationStep, old_step: str) -> None:
|
|
570
|
+
"""
|
|
571
|
+
Replace a step with a new step.
|
|
572
|
+
|
|
573
|
+
Parameters
|
|
574
|
+
----------
|
|
575
|
+
step : InitializationStep
|
|
576
|
+
The new step to replace the old step
|
|
577
|
+
old_step : str
|
|
578
|
+
The name of the step to replace
|
|
579
|
+
|
|
580
|
+
Raises
|
|
581
|
+
------
|
|
582
|
+
ValueError
|
|
583
|
+
If the step is not found or has already been completed
|
|
584
|
+
"""
|
|
585
|
+
try:
|
|
586
|
+
idx = next(i for i, s in enumerate(self.steps) if s.name == old_step)
|
|
587
|
+
except StopIteration:
|
|
588
|
+
raise ValueError(f"Step '{old_step}' not found")
|
|
589
|
+
|
|
590
|
+
if idx in self._completed_steps:
|
|
591
|
+
raise ValueError(
|
|
592
|
+
f"Cannot replace step '{old_step}' - it has already been completed. "
|
|
593
|
+
f"Use reset() to clear completed steps first."
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
self.steps[idx] = step
|
|
597
|
+
|
|
598
|
+
def get_completed_steps(self) -> list[str]:
|
|
599
|
+
"""
|
|
600
|
+
Get a list of completed step names.
|
|
601
|
+
|
|
602
|
+
Returns
|
|
603
|
+
-------
|
|
604
|
+
list[str]
|
|
605
|
+
Names of steps that have been completed
|
|
606
|
+
"""
|
|
607
|
+
return [self.steps[i].name for i in sorted(self._completed_steps)]
|
|
608
|
+
|
|
609
|
+
def _check_dependencies(self, step: InitializationStep) -> tuple[bool, list[type]]:
|
|
610
|
+
"""
|
|
611
|
+
Check if all dependencies for a step are satisfied.
|
|
612
|
+
|
|
613
|
+
A dependency is satisfied if any completed step class is a subclass of
|
|
614
|
+
(or is) the required dependency class. This allows custom steps that
|
|
615
|
+
inherit from dependency classes to satisfy dependencies.
|
|
616
|
+
|
|
617
|
+
Parameters
|
|
618
|
+
----------
|
|
619
|
+
step : InitializationStep
|
|
620
|
+
The step to check dependencies for
|
|
621
|
+
|
|
622
|
+
Returns
|
|
623
|
+
-------
|
|
624
|
+
tuple[bool, list[type]]
|
|
625
|
+
Tuple of (all_satisfied, missing_dependencies)
|
|
626
|
+
"""
|
|
627
|
+
dependencies = step.depends_on
|
|
628
|
+
missing = []
|
|
629
|
+
|
|
630
|
+
for dep_class in dependencies:
|
|
631
|
+
satisfied = False
|
|
632
|
+
for completed_class in self._completed_step_classes:
|
|
633
|
+
if issubclass(completed_class, dep_class):
|
|
634
|
+
satisfied = True
|
|
635
|
+
break
|
|
636
|
+
if not satisfied:
|
|
637
|
+
missing.append(dep_class)
|
|
638
|
+
|
|
639
|
+
return len(missing) == 0, missing
|
|
640
|
+
|
|
641
|
+
def get_next_step_index(self) -> int:
|
|
642
|
+
"""
|
|
643
|
+
Get the index of the next step to be initialized.
|
|
644
|
+
|
|
645
|
+
Returns
|
|
646
|
+
-------
|
|
647
|
+
int
|
|
648
|
+
Index of the next uninitialized step with satisfied dependencies,
|
|
649
|
+
or -1 if all steps are complete or no step has satisfied dependencies
|
|
650
|
+
"""
|
|
651
|
+
for i in range(len(self.steps)):
|
|
652
|
+
if i not in self._completed_steps:
|
|
653
|
+
step = self.steps[i]
|
|
654
|
+
deps_satisfied, _ = self._check_dependencies(step)
|
|
655
|
+
if deps_satisfied:
|
|
656
|
+
return i
|
|
657
|
+
return -1
|
|
658
|
+
|
|
659
|
+
def get_next_step_name(self) -> str:
|
|
660
|
+
"""
|
|
661
|
+
Get the name of the next step to be initialized.
|
|
662
|
+
|
|
663
|
+
Returns
|
|
664
|
+
-------
|
|
665
|
+
str
|
|
666
|
+
Name of the next uninitialized step, or None if all steps are complete
|
|
667
|
+
"""
|
|
668
|
+
idx = self.get_next_step_index()
|
|
669
|
+
if idx == -1:
|
|
670
|
+
return None
|
|
671
|
+
return self.steps[idx].name
|
|
672
|
+
|
|
673
|
+
def initialize_next_step(self, startup_dir: str = None, ns: dict = None) -> tuple[str, dict]:
|
|
674
|
+
"""
|
|
675
|
+
Initialize the next uninitialized step.
|
|
676
|
+
|
|
677
|
+
Parameters
|
|
678
|
+
----------
|
|
679
|
+
startup_dir : str, optional
|
|
680
|
+
Directory containing configuration files. Only needed for first step.
|
|
681
|
+
ns : dict, optional
|
|
682
|
+
Namespace for loading devices. Only needed for first step.
|
|
683
|
+
|
|
684
|
+
Returns
|
|
685
|
+
-------
|
|
686
|
+
tuple[str, dict]
|
|
687
|
+
Tuple of (step_name, context) after executing the step
|
|
688
|
+
|
|
689
|
+
Raises
|
|
690
|
+
------
|
|
691
|
+
RuntimeError
|
|
692
|
+
If the step fails to execute
|
|
693
|
+
ValueError
|
|
694
|
+
If startup_dir is not provided and this is the first step
|
|
695
|
+
"""
|
|
696
|
+
idx = self.get_next_step_index()
|
|
697
|
+
if idx == -1:
|
|
698
|
+
raise ValueError("All steps have been completed")
|
|
699
|
+
|
|
700
|
+
if not self._context:
|
|
701
|
+
if startup_dir is None:
|
|
702
|
+
raise ValueError("startup_dir must be provided for the first step")
|
|
703
|
+
self._context = {
|
|
704
|
+
"startup_dir": startup_dir,
|
|
705
|
+
"namespace": ns,
|
|
706
|
+
}
|
|
707
|
+
elif startup_dir is not None or ns is not None:
|
|
708
|
+
if startup_dir is not None:
|
|
709
|
+
self._context["startup_dir"] = startup_dir
|
|
710
|
+
if ns is not None:
|
|
711
|
+
self._context["namespace"] = ns
|
|
712
|
+
|
|
713
|
+
step = self.steps[idx]
|
|
714
|
+
|
|
715
|
+
deps_satisfied, missing_deps = self._check_dependencies(step)
|
|
716
|
+
if not deps_satisfied:
|
|
717
|
+
missing_names = [dep.__name__ for dep in missing_deps]
|
|
718
|
+
raise RuntimeError(
|
|
719
|
+
f"Step '{step.name}' cannot run: missing dependencies: {missing_names}"
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
print(f"Beamline Initialization Step {idx + 1}/{len(self.steps)}: {step.name}")
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
self._context = step.execute(self.beamline, self._context)
|
|
726
|
+
self._completed_steps.add(idx)
|
|
727
|
+
self._completed_step_classes.add(type(step))
|
|
728
|
+
return step.name, self._context
|
|
729
|
+
except Exception as e:
|
|
730
|
+
raise RuntimeError(
|
|
731
|
+
f"Initialization failed at step '{step.name}': {e}"
|
|
732
|
+
) from e
|
|
733
|
+
|
|
734
|
+
def reset(self):
|
|
735
|
+
"""
|
|
736
|
+
Reset the initialization state.
|
|
737
|
+
|
|
738
|
+
Clears completed steps and context, allowing re-initialization.
|
|
739
|
+
"""
|
|
740
|
+
self._completed_steps.clear()
|
|
741
|
+
self._completed_step_classes.clear()
|
|
742
|
+
self._context = {}
|
|
743
|
+
self.beamline.reset()
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def initialize(
|
|
747
|
+
self,
|
|
748
|
+
startup_dir: str,
|
|
749
|
+
ns: dict = None,
|
|
750
|
+
num_steps: int = None,
|
|
751
|
+
from_step: int = 0,
|
|
752
|
+
) -> BeamlineModel:
|
|
753
|
+
"""
|
|
754
|
+
Execute the initialization pipeline.
|
|
755
|
+
|
|
756
|
+
Parameters
|
|
757
|
+
----------
|
|
758
|
+
startup_dir : str
|
|
759
|
+
Directory containing configuration files
|
|
760
|
+
ns : dict, optional
|
|
761
|
+
Namespace for loading devices
|
|
762
|
+
num_steps : int, optional
|
|
763
|
+
Number of steps to execute. If None, executes all remaining steps.
|
|
764
|
+
from_step : int, optional
|
|
765
|
+
Index of the first step to execute (0-based). Default is 0.
|
|
766
|
+
|
|
767
|
+
Returns
|
|
768
|
+
-------
|
|
769
|
+
BeamlineModel
|
|
770
|
+
The beamline model (partially or fully initialized)
|
|
771
|
+
|
|
772
|
+
Raises
|
|
773
|
+
------
|
|
774
|
+
RuntimeError
|
|
775
|
+
If any initialization step fails
|
|
776
|
+
ValueError
|
|
777
|
+
If from_step is out of range or num_steps is invalid
|
|
778
|
+
"""
|
|
779
|
+
if from_step < 0 or from_step >= len(self.steps):
|
|
780
|
+
raise ValueError(f"from_step must be between 0 and {len(self.steps) - 1}")
|
|
781
|
+
|
|
782
|
+
if num_steps is not None and num_steps <= 0:
|
|
783
|
+
raise ValueError("num_steps must be positive")
|
|
784
|
+
|
|
785
|
+
if not self._context:
|
|
786
|
+
self._context = {
|
|
787
|
+
"startup_dir": startup_dir,
|
|
788
|
+
"namespace": ns,
|
|
789
|
+
}
|
|
790
|
+
else:
|
|
791
|
+
if startup_dir is not None:
|
|
792
|
+
self._context["startup_dir"] = startup_dir
|
|
793
|
+
if ns is not None:
|
|
794
|
+
self._context["namespace"] = ns
|
|
795
|
+
|
|
796
|
+
if from_step == 0 and not self._completed_steps:
|
|
797
|
+
print(f"Initializing beamline from {startup_dir}")
|
|
798
|
+
else:
|
|
799
|
+
print(f"Continuing initialization from step {from_step + 1}")
|
|
800
|
+
|
|
801
|
+
end_step = len(self.steps)
|
|
802
|
+
if num_steps is not None:
|
|
803
|
+
end_step = min(from_step + num_steps, len(self.steps))
|
|
804
|
+
|
|
805
|
+
for idx in range(from_step, end_step):
|
|
806
|
+
if idx in self._completed_steps:
|
|
807
|
+
print(f"Beamline Initialization Step {idx + 1}/{len(self.steps)}: {self.steps[idx].name} (already completed)")
|
|
808
|
+
continue
|
|
809
|
+
|
|
810
|
+
step = self.steps[idx]
|
|
811
|
+
deps_satisfied, missing_deps = self._check_dependencies(step)
|
|
812
|
+
|
|
813
|
+
if not deps_satisfied:
|
|
814
|
+
missing_names = [dep.__name__ for dep in missing_deps]
|
|
815
|
+
print(
|
|
816
|
+
f"Beamline Initialization Step {idx + 1}/{len(self.steps)}: {step.name} "
|
|
817
|
+
f"(skipped - missing dependencies: {missing_names})"
|
|
818
|
+
)
|
|
819
|
+
continue
|
|
820
|
+
|
|
821
|
+
print(f"Beamline Initialization Step {idx + 1}/{len(self.steps)}: {step.name}")
|
|
822
|
+
|
|
823
|
+
try:
|
|
824
|
+
self._context = step.execute(self.beamline, self._context)
|
|
825
|
+
self._completed_steps.add(idx)
|
|
826
|
+
self._completed_step_classes.add(type(step))
|
|
827
|
+
except Exception as e:
|
|
828
|
+
raise RuntimeError(
|
|
829
|
+
f"Initialization failed at step '{step.name}': {e}"
|
|
830
|
+
) from e
|
|
831
|
+
|
|
832
|
+
if end_step == len(self.steps) and len(self._completed_steps) == len(self.steps):
|
|
833
|
+
print("Beamline initialization complete")
|
|
834
|
+
else:
|
|
835
|
+
remaining = len(self.steps) - len(self._completed_steps)
|
|
836
|
+
print(f"Beamline initialization paused: {remaining} step(s) remaining")
|
|
837
|
+
|
|
838
|
+
return self.beamline
|