dls-dodal 1.66.0__py3-none-any.whl → 1.68.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 (83) hide show
  1. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/RECORD +75 -65
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/b07.py +1 -1
  5. dodal/beamlines/b07_1.py +1 -1
  6. dodal/beamlines/i03.py +92 -208
  7. dodal/beamlines/i04.py +22 -1
  8. dodal/beamlines/i05.py +1 -1
  9. dodal/beamlines/i06.py +1 -1
  10. dodal/beamlines/i09.py +1 -1
  11. dodal/beamlines/i09_1.py +27 -3
  12. dodal/beamlines/i09_2.py +58 -2
  13. dodal/beamlines/i10_optics.py +44 -25
  14. dodal/beamlines/i16.py +23 -0
  15. dodal/beamlines/i17.py +7 -3
  16. dodal/beamlines/i19_1.py +26 -14
  17. dodal/beamlines/i19_2.py +49 -38
  18. dodal/beamlines/i21.py +61 -2
  19. dodal/beamlines/i22.py +16 -1
  20. dodal/beamlines/p60.py +1 -1
  21. dodal/beamlines/training_rig.py +0 -16
  22. dodal/cli.py +26 -12
  23. dodal/common/coordination.py +3 -2
  24. dodal/device_manager.py +604 -0
  25. dodal/devices/cryostream.py +28 -57
  26. dodal/devices/eiger.py +41 -27
  27. dodal/devices/electron_analyser/__init__.py +0 -33
  28. dodal/devices/electron_analyser/base/__init__.py +58 -0
  29. dodal/devices/electron_analyser/base/base_controller.py +73 -0
  30. dodal/devices/electron_analyser/base/base_detector.py +214 -0
  31. dodal/devices/electron_analyser/{abstract → base}/base_driver_io.py +23 -42
  32. dodal/devices/electron_analyser/{abstract → base}/base_region.py +47 -11
  33. dodal/devices/electron_analyser/{util.py → base/base_util.py} +1 -1
  34. dodal/devices/electron_analyser/{energy_sources.py → base/energy_sources.py} +1 -1
  35. dodal/devices/electron_analyser/specs/__init__.py +4 -4
  36. dodal/devices/electron_analyser/specs/specs_detector.py +46 -0
  37. dodal/devices/electron_analyser/specs/{driver_io.py → specs_driver_io.py} +23 -26
  38. dodal/devices/electron_analyser/specs/{region.py → specs_region.py} +4 -3
  39. dodal/devices/electron_analyser/vgscienta/__init__.py +4 -4
  40. dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +52 -0
  41. dodal/devices/electron_analyser/vgscienta/{driver_io.py → vgscienta_driver_io.py} +25 -31
  42. dodal/devices/electron_analyser/vgscienta/{region.py → vgscienta_region.py} +6 -6
  43. dodal/devices/i04/max_pixel.py +38 -0
  44. dodal/devices/i09_1_shared/__init__.py +8 -1
  45. dodal/devices/i09_1_shared/hard_energy.py +112 -0
  46. dodal/devices/i09_2_shared/__init__.py +0 -0
  47. dodal/devices/i09_2_shared/i09_apple2.py +14 -0
  48. dodal/devices/i10/i10_apple2.py +24 -22
  49. dodal/devices/i17/i17_apple2.py +32 -20
  50. dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
  51. dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
  52. dodal/devices/i19/access_controlled/shutter.py +2 -4
  53. dodal/devices/i21/__init__.py +3 -1
  54. dodal/devices/insertion_device/__init__.py +58 -0
  55. dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +102 -44
  56. dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
  57. dodal/devices/insertion_device/id_enum.py +17 -0
  58. dodal/devices/insertion_device/lookup_table_models.py +317 -0
  59. dodal/devices/motors.py +14 -0
  60. dodal/devices/robot.py +16 -11
  61. dodal/plans/__init__.py +1 -1
  62. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
  63. dodal/testing/electron_analyser/device_factory.py +4 -4
  64. dodal/testing/fixtures/devices/__init__.py +0 -0
  65. dodal/testing/fixtures/devices/apple2.py +78 -0
  66. dodal/testing/fixtures/run_engine.py +4 -0
  67. dodal/utils.py +6 -3
  68. dodal/devices/electron_analyser/abstract/__init__.py +0 -25
  69. dodal/devices/electron_analyser/abstract/base_detector.py +0 -63
  70. dodal/devices/electron_analyser/abstract/types.py +0 -12
  71. dodal/devices/electron_analyser/detector.py +0 -143
  72. dodal/devices/electron_analyser/specs/detector.py +0 -34
  73. dodal/devices/electron_analyser/types.py +0 -57
  74. dodal/devices/electron_analyser/vgscienta/detector.py +0 -48
  75. dodal/devices/util/lookup_tables_apple2.py +0 -390
  76. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/WHEEL +0 -0
  77. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/entry_points.txt +0 -0
  78. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/licenses/LICENSE +0 -0
  79. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/top_level.txt +0 -0
  80. /dodal/devices/electron_analyser/{enums.py → base/base_enums.py} +0 -0
  81. /dodal/devices/electron_analyser/specs/{enums.py → specs_enums.py} +0 -0
  82. /dodal/devices/electron_analyser/vgscienta/{enums.py → vgscienta_enums.py} +0 -0
  83. /dodal/plans/{scanspec.py → spec_path.py} +0 -0
@@ -1,143 +0,0 @@
1
- from typing import Generic, TypeVar
2
-
3
- from bluesky.protocols import Stageable
4
- from ophyd_async.core import AsyncStatus
5
- from ophyd_async.epics.adcore import ADBaseController
6
-
7
- from dodal.common.data_util import load_json_file_to_class
8
- from dodal.devices.controllers import ConstantDeadTimeController
9
- from dodal.devices.electron_analyser.abstract.base_detector import (
10
- BaseElectronAnalyserDetector,
11
- )
12
- from dodal.devices.electron_analyser.abstract.base_driver_io import (
13
- TAbstractAnalyserDriverIO,
14
- )
15
- from dodal.devices.electron_analyser.abstract.base_region import (
16
- TAbstractBaseRegion,
17
- TAbstractBaseSequence,
18
- )
19
-
20
-
21
- class ElectronAnalyserRegionDetector(
22
- BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
23
- Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
24
- ):
25
- """
26
- Extends electron analyser detector to configure specific region settings before data
27
- acquisition. It is designed to only exist inside a plan.
28
- """
29
-
30
- def __init__(
31
- self,
32
- controller: ADBaseController[TAbstractAnalyserDriverIO],
33
- region: TAbstractBaseRegion,
34
- name: str = "",
35
- ):
36
- self.region = region
37
- super().__init__(controller, name)
38
-
39
- @AsyncStatus.wrap
40
- async def trigger(self) -> None:
41
- # Configure region parameters on the driver first before data collection.
42
- await self._controller.driver.set(self.region)
43
- await super().trigger()
44
-
45
-
46
- TElectronAnalyserRegionDetector = TypeVar(
47
- "TElectronAnalyserRegionDetector",
48
- bound=ElectronAnalyserRegionDetector,
49
- )
50
-
51
-
52
- class ElectronAnalyserDetector(
53
- BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
54
- Stageable,
55
- Generic[
56
- TAbstractAnalyserDriverIO,
57
- TAbstractBaseSequence,
58
- TAbstractBaseRegion,
59
- ],
60
- ):
61
- """
62
- Electron analyser detector with the additional functionality to load a sequence file
63
- and create a list of temporary ElectronAnalyserRegionDetector objects. These will
64
- setup configured region settings before data acquisition.
65
- """
66
-
67
- def __init__(
68
- self,
69
- sequence_class: type[TAbstractBaseSequence],
70
- driver: TAbstractAnalyserDriverIO,
71
- name: str = "",
72
- ):
73
- # Save driver as direct child so participates with connect()
74
- self.driver = driver
75
- self._sequence_class = sequence_class
76
- controller = ConstantDeadTimeController[TAbstractAnalyserDriverIO](driver, 0)
77
- super().__init__(controller, name)
78
-
79
- @AsyncStatus.wrap
80
- async def stage(self) -> None:
81
- """
82
- Prepare the detector for use by ensuring it is idle and ready.
83
-
84
- This method asynchronously stages the detector by first disarming the controller
85
- to ensure the detector is not actively acquiring data, then invokes the driver's
86
- stage procedure. This ensures the detector is in a known, ready state
87
- before use.
88
-
89
- Raises:
90
- Any exceptions raised by the driver's stage or controller's disarm methods.
91
- """
92
- await self._controller.disarm()
93
- await self.driver.stage()
94
-
95
- @AsyncStatus.wrap
96
- async def unstage(self) -> None:
97
- """Disarm the detector."""
98
- await self._controller.disarm()
99
- await self.driver.unstage()
100
-
101
- def load_sequence(self, filename: str) -> TAbstractBaseSequence:
102
- """
103
- Load the sequence data from a provided json file into a sequence class.
104
-
105
- Args:
106
- filename: Path to the sequence file containing the region data.
107
-
108
- Returns:
109
- Pydantic model representing the sequence file.
110
- """
111
- return load_json_file_to_class(self._sequence_class, filename)
112
-
113
- def create_region_detector_list(
114
- self, filename: str, enabled_only=True
115
- ) -> list[
116
- ElectronAnalyserRegionDetector[TAbstractAnalyserDriverIO, TAbstractBaseRegion]
117
- ]:
118
- """
119
- Create a list of detectors equal to the number of regions in a sequence file.
120
- Each detector is responsible for setting up a specific region.
121
-
122
- Args:
123
- filename: Path to the sequence file containing the region data.
124
- enabled_only: If true, only include the region if enabled is True.
125
-
126
- Returns:
127
- List of ElectronAnalyserRegionDetector, equal to the number of regions in
128
- the sequence file.
129
- """
130
- seq = self.load_sequence(filename)
131
- regions = seq.get_enabled_regions() if enabled_only else seq.regions
132
- return [
133
- ElectronAnalyserRegionDetector(
134
- self._controller, r, self.name + "_" + r.name
135
- )
136
- for r in regions
137
- ]
138
-
139
-
140
- TElectronAnalyserDetector = TypeVar(
141
- "TElectronAnalyserDetector",
142
- bound=ElectronAnalyserDetector,
143
- )
@@ -1,34 +0,0 @@
1
- from typing import Generic
2
-
3
- from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
4
- from dodal.devices.electron_analyser.detector import (
5
- ElectronAnalyserDetector,
6
- )
7
- from dodal.devices.electron_analyser.energy_sources import (
8
- DualEnergySource,
9
- EnergySource,
10
- )
11
- from dodal.devices.electron_analyser.specs.driver_io import SpecsAnalyserDriverIO
12
- from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSequence
13
-
14
-
15
- class SpecsDetector(
16
- ElectronAnalyserDetector[
17
- SpecsAnalyserDriverIO[TLensMode, TPsuMode],
18
- SpecsSequence[TLensMode, TPsuMode],
19
- SpecsRegion[TLensMode, TPsuMode],
20
- ],
21
- Generic[TLensMode, TPsuMode],
22
- ):
23
- def __init__(
24
- self,
25
- prefix: str,
26
- lens_mode_type: type[TLensMode],
27
- psu_mode_type: type[TPsuMode],
28
- energy_source: DualEnergySource | EnergySource,
29
- name: str = "",
30
- ):
31
- driver = SpecsAnalyserDriverIO[TLensMode, TPsuMode](
32
- prefix, lens_mode_type, psu_mode_type, energy_source
33
- )
34
- super().__init__(SpecsSequence[lens_mode_type, psu_mode_type], driver, name)
@@ -1,57 +0,0 @@
1
- from ophyd_async.core import StrictEnum, SupersetEnum
2
-
3
- from dodal.devices.electron_analyser.abstract.base_driver_io import (
4
- AbstractAnalyserDriverIO,
5
- )
6
- from dodal.devices.electron_analyser.abstract.base_region import (
7
- AbstractBaseRegion,
8
- AbstractBaseSequence,
9
- )
10
- from dodal.devices.electron_analyser.detector import (
11
- ElectronAnalyserDetector,
12
- ElectronAnalyserRegionDetector,
13
- )
14
- from dodal.devices.electron_analyser.specs.detector import (
15
- SpecsAnalyserDriverIO,
16
- SpecsDetector,
17
- )
18
- from dodal.devices.electron_analyser.vgscienta.detector import (
19
- VGScientaAnalyserDriverIO,
20
- VGScientaDetector,
21
- )
22
-
23
- AnyAcqMode = StrictEnum
24
- AnyLensMode = SupersetEnum | StrictEnum
25
- AnyPsuMode = SupersetEnum | StrictEnum
26
- AnyPassEnergy = StrictEnum | float
27
- AnyPassEnergyEnum = StrictEnum
28
-
29
- # Electron analyser types that encompasses all implementations, useful for tests and
30
- # plans
31
- ElectronAnalyserDetectorImpl = (
32
- VGScientaDetector[AnyLensMode, AnyPsuMode, AnyPassEnergyEnum]
33
- | SpecsDetector[AnyLensMode, AnyPsuMode]
34
- )
35
- ElectronAnalyserDriverImpl = (
36
- VGScientaAnalyserDriverIO[AnyLensMode, AnyPsuMode, AnyPassEnergyEnum]
37
- | SpecsAnalyserDriverIO[AnyLensMode, AnyPsuMode]
38
- )
39
-
40
- # Short hand the type so less verbose
41
- AbstractBaseRegion = AbstractBaseRegion[AnyAcqMode, AnyLensMode, AnyPassEnergy]
42
-
43
- # Generic electron analyser types that supports full typing with the abstract classes.
44
- GenericElectronAnalyserDetector = ElectronAnalyserDetector[
45
- AbstractAnalyserDriverIO[
46
- AbstractBaseRegion, AnyAcqMode, AnyLensMode, AnyPsuMode, AnyPassEnergy
47
- ],
48
- AbstractBaseSequence[AbstractBaseRegion],
49
- AbstractBaseRegion,
50
- ]
51
-
52
- GenericElectronAnalyserRegionDetector = ElectronAnalyserRegionDetector[
53
- AbstractAnalyserDriverIO[
54
- AbstractBaseRegion, AnyAcqMode, AnyLensMode, AnyPsuMode, AnyPassEnergy
55
- ],
56
- AbstractBaseRegion,
57
- ]
@@ -1,48 +0,0 @@
1
- from typing import Generic
2
-
3
- from dodal.devices.electron_analyser.abstract.types import (
4
- TLensMode,
5
- TPassEnergyEnum,
6
- TPsuMode,
7
- )
8
- from dodal.devices.electron_analyser.detector import (
9
- ElectronAnalyserDetector,
10
- )
11
- from dodal.devices.electron_analyser.energy_sources import (
12
- DualEnergySource,
13
- EnergySource,
14
- )
15
- from dodal.devices.electron_analyser.vgscienta.driver_io import (
16
- VGScientaAnalyserDriverIO,
17
- )
18
- from dodal.devices.electron_analyser.vgscienta.region import (
19
- VGScientaRegion,
20
- VGScientaSequence,
21
- )
22
-
23
-
24
- class VGScientaDetector(
25
- ElectronAnalyserDetector[
26
- VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum],
27
- VGScientaSequence[TLensMode, TPsuMode, TPassEnergyEnum],
28
- VGScientaRegion[TLensMode, TPassEnergyEnum],
29
- ],
30
- Generic[TLensMode, TPsuMode, TPassEnergyEnum],
31
- ):
32
- def __init__(
33
- self,
34
- prefix: str,
35
- lens_mode_type: type[TLensMode],
36
- psu_mode_type: type[TPsuMode],
37
- pass_energy_type: type[TPassEnergyEnum],
38
- energy_source: DualEnergySource | EnergySource,
39
- name: str = "",
40
- ):
41
- driver = VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum](
42
- prefix, lens_mode_type, psu_mode_type, pass_energy_type, energy_source
43
- )
44
- super().__init__(
45
- VGScientaSequence[lens_mode_type, psu_mode_type, pass_energy_type],
46
- driver,
47
- name,
48
- )
@@ -1,390 +0,0 @@
1
- """Apple2 lookup table utilities and CSV converter.
2
-
3
- This module provides helpers to read, validate and convert Apple2 insertion-device
4
- lookup tables (energy -> gap/phase polynomials) from CSV sources into an
5
- in-memory dictionary format used by the Apple2 controllers.
6
-
7
- Data format produced
8
- The lookup-table dictionary created by convert_csv_to_lookup() follows this
9
- structure:
10
-
11
- {
12
- "POL_MODE": {
13
- "energies": {
14
- "<min_energy>": {
15
- "low": <float>,
16
- "high": <float>,
17
- "poly": <numpy.poly1d>
18
- },
19
- ...
20
- },
21
- "limit": {
22
- "minimum": <float>,
23
- "maximum": <float>
24
- }
25
- },
26
- }
27
-
28
- """
29
-
30
- import csv
31
- import io
32
- from collections.abc import Generator
33
- from pathlib import Path
34
-
35
- import numpy as np
36
- from daq_config_server.client import ConfigServer
37
- from pydantic import (
38
- BaseModel,
39
- ConfigDict,
40
- Field,
41
- RootModel,
42
- field_serializer,
43
- field_validator,
44
- )
45
-
46
- from dodal.devices.apple2_undulator import Pol
47
- from dodal.log import LOGGER
48
-
49
- DEFAULT_GAP_FILE = "IDEnergy2GapCalibrations.csv"
50
- DEFAULT_PHASE_FILE = "IDEnergy2PhaseCalibrations.csv"
51
-
52
-
53
- DEFAULT_POLY_DEG = [
54
- "7th-order",
55
- "6th-order",
56
- "5th-order",
57
- "4th-order",
58
- "3rd-order",
59
- "2nd-order",
60
- "1st-order",
61
- "b",
62
- ]
63
-
64
- MODE_NAME_CONVERT = {"CR": "pc", "CL": "nc"}
65
-
66
-
67
- class LookupTableConfig(BaseModel):
68
- source: tuple[str, str] | None = None
69
- mode: str = "Mode"
70
- min_energy: str = "MinEnergy"
71
- max_energy: str = "MaxEnergy"
72
- poly_deg: list[str] = Field(default_factory=lambda: DEFAULT_POLY_DEG)
73
- mode_name_convert: dict[str, str] = Field(default_factory=lambda: MODE_NAME_CONVERT)
74
-
75
-
76
- class EnergyMinMax(BaseModel):
77
- minimum: float
78
- maximum: float
79
-
80
-
81
- class EnergyCoverageEntry(BaseModel):
82
- model_config = ConfigDict(arbitrary_types_allowed=True) # So np.poly1d can be used.
83
- low: float
84
- high: float
85
- poly: np.poly1d
86
-
87
- @field_validator("poly", mode="before")
88
- @classmethod
89
- def validate_and_convert_poly(cls, value):
90
- """If reading from serialized data, it will be using a list. Convert to np.poly1d"""
91
- if isinstance(value, list):
92
- return np.poly1d(value)
93
- return value
94
-
95
- @field_serializer("poly", mode="plain")
96
- def serialize_poly(self, value: np.poly1d) -> list:
97
- """Allow np.poly1d to work when serializing."""
98
- return value.coefficients.tolist()
99
-
100
-
101
- class EnergyCoverage(RootModel[dict[float, EnergyCoverageEntry]]):
102
- pass
103
-
104
-
105
- class LookupTableEntries(BaseModel):
106
- energies: EnergyCoverage
107
- limit: EnergyMinMax
108
-
109
-
110
- class LookupTable(RootModel[dict[Pol, LookupTableEntries]]):
111
- # Allow to auto specify a dict if one not provided
112
- def __init__(self, root: dict[Pol, LookupTableEntries] | None = None):
113
- super().__init__(root=root or {})
114
-
115
-
116
- class GapPhaseLookupTables(BaseModel):
117
- gap: LookupTable = Field(default_factory=lambda: LookupTable())
118
- phase: LookupTable = Field(default_factory=lambda: LookupTable())
119
-
120
-
121
- def convert_csv_to_lookup(
122
- file_contents: str,
123
- lut_config: LookupTableConfig,
124
- skip_line_start_with: str = "#",
125
- ) -> LookupTable:
126
- """
127
- Convert CSV content into the Apple2 lookup-table dictionary.
128
-
129
- Parameters:
130
- -----------
131
- file_contents:
132
- The CSV file contents as string.
133
- lut_config:
134
- The configuration that how to process the file_contents into a LookupTable.
135
- skip_line_start_with
136
- Lines beginning with this prefix are skipped (default "#").
137
-
138
- Returns:
139
- -----------
140
- LookupTable
141
- """
142
-
143
- def process_row(row: dict, lut: LookupTable):
144
- """Process a single row from the CSV file and update the lookup table."""
145
- mode_value = str(row[lut_config.mode]).lower()
146
- if mode_value in lut_config.mode_name_convert:
147
- mode_value = lut_config.mode_name_convert[f"{mode_value}"]
148
- mode_value = Pol(mode_value)
149
-
150
- # Create polynomial object for energy-to-gap/phase conversion
151
- coefficients = [float(row[coef]) for coef in lut_config.poly_deg]
152
- if mode_value not in lut.root:
153
- lut.root[mode_value] = generate_lookup_table_entry(
154
- min_energy=float(row[lut_config.min_energy]),
155
- max_energy=float(row[lut_config.max_energy]),
156
- poly1d_param=coefficients,
157
- )
158
-
159
- else:
160
- lut.root[mode_value].energies.root[float(row[lut_config.min_energy])] = (
161
- EnergyCoverageEntry(
162
- low=float(row[lut_config.min_energy]),
163
- high=float(row[lut_config.max_energy]),
164
- poly=np.poly1d(coefficients),
165
- )
166
- )
167
-
168
- # Update energy limits
169
- lut.root[mode_value].limit.minimum = min(
170
- lut.root[mode_value].limit.minimum,
171
- float(row[lut_config.min_energy]),
172
- )
173
- lut.root[mode_value].limit.maximum = max(
174
- lut.root[mode_value].limit.maximum,
175
- float(row[lut_config.max_energy]),
176
- )
177
- return lut
178
-
179
- reader = csv.DictReader(read_file_and_skip(file_contents, skip_line_start_with))
180
- lut = LookupTable()
181
-
182
- for row in reader:
183
- # If there are multiple source only convert requested.
184
- if lut_config.source is not None:
185
- if row[lut_config.source[0]] == lut_config.source[1]:
186
- process_row(row=row, lut=lut)
187
- else:
188
- process_row(row=row, lut=lut)
189
-
190
- # Check if our LookupTable is empty after processing, raise error if it is.
191
- if not lut.root:
192
- raise RuntimeError(
193
- "LookupTable content is empty, failed to convert the file contents to "
194
- "a LookupTable!"
195
- )
196
- return lut
197
-
198
-
199
- def read_file_and_skip(file: str, skip_line_start_with: str = "#") -> Generator[str]:
200
- """Yield non-comment lines from the CSV content string."""
201
- for line in io.StringIO(file):
202
- if line.startswith(skip_line_start_with):
203
- continue
204
- else:
205
- yield line
206
-
207
-
208
- def get_poly(
209
- energy: float,
210
- pol: Pol,
211
- lookup_table: LookupTable,
212
- ) -> np.poly1d:
213
- """
214
- Return the numpy.poly1d polynomial applicable for the given energy and polarisation.
215
-
216
- Parameters:
217
- -----------
218
- energy:
219
- Energy value in the same units used to create the lookup table (eV).
220
- pol:
221
- Polarisation mode (Pol enum).
222
- lookup_table:
223
- The converted lookup table dictionary for either 'gap' or 'phase'.
224
- """
225
- if (
226
- energy < lookup_table.root[pol].limit.minimum
227
- or energy > lookup_table.root[pol].limit.maximum
228
- ):
229
- raise ValueError(
230
- "Demanding energy must lie between"
231
- + f" {lookup_table.root[pol].limit.minimum}"
232
- + f" and {lookup_table.root[pol].limit.maximum} eV!"
233
- )
234
- else:
235
- for energy_range in lookup_table.root[pol].energies.root.values():
236
- if energy >= energy_range.low and energy < energy_range.high:
237
- return energy_range.poly
238
-
239
- raise ValueError(
240
- "Cannot find polynomial coefficients for your requested energy."
241
- + " There might be gap in the calibration lookup table."
242
- )
243
-
244
-
245
- def generate_lookup_table_entry(
246
- min_energy: float, max_energy: float, poly1d_param: list[float]
247
- ) -> LookupTableEntries:
248
- return LookupTableEntries(
249
- energies=EnergyCoverage(
250
- {
251
- min_energy: EnergyCoverageEntry(
252
- low=min_energy,
253
- high=max_energy,
254
- poly=np.poly1d(poly1d_param),
255
- )
256
- }
257
- ),
258
- limit=EnergyMinMax(
259
- minimum=float(min_energy),
260
- maximum=float(max_energy),
261
- ),
262
- )
263
-
264
-
265
- def generate_lookup_table(
266
- pol: Pol, min_energy: float, max_energy: float, poly1d_param: list[float]
267
- ) -> LookupTable:
268
- return LookupTable(
269
- {pol: generate_lookup_table_entry(min_energy, max_energy, poly1d_param)}
270
- )
271
-
272
-
273
- def make_phase_tables(
274
- pols: list[Pol],
275
- min_energies: list[float],
276
- max_energies: list[float],
277
- poly1d_params: list[list[float]],
278
- ) -> LookupTable:
279
- """Generate a dictionary containing multiple lookuptable entries
280
- for provided polarisations."""
281
- lookuptable_phase = LookupTable()
282
- for i in range(len(pols)):
283
- lookuptable_phase.root[pols[i]] = generate_lookup_table_entry(
284
- min_energy=min_energies[i],
285
- max_energy=max_energies[i],
286
- poly1d_param=poly1d_params[i],
287
- )
288
-
289
- return lookuptable_phase
290
-
291
-
292
- class EnergyMotorLookup:
293
- """
294
- Handles lookup tables for Apple2 ID, converting energy and polarisation to gap
295
- and phase. Fetches and parses lookup tables from a config server, supports dynamic
296
- updates, and validates input. If custom logic is required for lookup tables, sub
297
- classes should override the _update_gap_lut and _update_phase_lut methods.
298
-
299
- After update_lookuptable() has populated the 'gap' and 'phase' tables,
300
- `get_motor_from_energy()` can be used to compute (gap, phase) for a requested
301
- (energy, pol) pair.
302
- """
303
-
304
- def __init__(
305
- self,
306
- config_client: ConfigServer,
307
- lut_config: LookupTableConfig,
308
- gap_path: Path,
309
- phase_path: Path,
310
- ):
311
- """Initialise the EnergyMotorLookup class with lookup table headers provided.
312
-
313
- Parameters:
314
- -----------
315
- config_client:
316
- The config server client to fetch the look up table data.
317
- lut_config:
318
- Configuration that defines how to process file contents into a LookupTable
319
- gap_path:
320
- File path to the gap lookup table.
321
- phase_path:
322
- File path to the phase lookup table.
323
- """
324
- self.lookup_tables = GapPhaseLookupTables()
325
- self.config_client = config_client
326
- self.lut_config = lut_config
327
- self.gap_path = gap_path
328
- self.phase_path = phase_path
329
- self._available_pol = []
330
-
331
- @property
332
- def available_pol(self) -> list[Pol]:
333
- return self._available_pol
334
-
335
- @available_pol.setter
336
- def available_pol(self, value: list[Pol]) -> None:
337
- self._available_pol = value
338
-
339
- def _update_gap_lut(self) -> None:
340
- file_contents = self.config_client.get_file_contents(
341
- self.gap_path, reset_cached_result=True
342
- )
343
- self.lookup_tables.gap = convert_csv_to_lookup(
344
- file_contents, lut_config=self.lut_config
345
- )
346
- self.available_pol = list(self.lookup_tables.gap.root.keys())
347
-
348
- def _update_phase_lut(self) -> None:
349
- file_contents = self.config_client.get_file_contents(
350
- self.phase_path, reset_cached_result=True
351
- )
352
- self.lookup_tables.phase = convert_csv_to_lookup(
353
- file_contents, lut_config=self.lut_config
354
- )
355
-
356
- def update_lookuptables(self):
357
- """
358
- Update lookup tables from files and validate their format.
359
- """
360
- LOGGER.info("Updating lookup table from file for gap.")
361
- self._update_gap_lut()
362
- LOGGER.info("Updating lookup table from file for phase.")
363
- self._update_phase_lut()
364
-
365
- def get_motor_from_energy(self, energy: float, pol: Pol) -> tuple[float, float]:
366
- """
367
- Convert energy and polarisation to gap and phase motor positions.
368
-
369
- Parameters:
370
- -----------
371
- energy : float
372
- Desired energy in eV.
373
- pol : Pol
374
- Polarisation mode.
375
-
376
- Returns:
377
- ----------
378
- tuple[float, float]
379
- (gap, phase) motor positions.
380
- """
381
- if self.available_pol == []:
382
- self.update_lookuptables()
383
-
384
- gap_poly = get_poly(lookup_table=self.lookup_tables.gap, energy=energy, pol=pol)
385
- phase_poly = get_poly(
386
- lookup_table=self.lookup_tables.phase,
387
- energy=energy,
388
- pol=pol,
389
- )
390
- return gap_poly(energy), phase_poly(energy)
File without changes