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