dls-dodal 1.48.0__py3-none-any.whl → 1.50.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 (41) hide show
  1. {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/METADATA +2 -1
  2. {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/RECORD +41 -30
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/aithre.py +15 -0
  5. dodal/beamlines/b16.py +65 -0
  6. dodal/beamlines/b18.py +38 -0
  7. dodal/beamlines/i10.py +41 -233
  8. dodal/beamlines/k11.py +35 -0
  9. dodal/common/beamlines/device_helpers.py +1 -0
  10. dodal/devices/apple2_undulator.py +257 -136
  11. dodal/devices/b16/__init__.py +0 -0
  12. dodal/devices/b16/detector.py +34 -0
  13. dodal/devices/bimorph_mirror.py +29 -36
  14. dodal/devices/electron_analyser/__init__.py +12 -2
  15. dodal/devices/electron_analyser/abstract/base_detector.py +3 -128
  16. dodal/devices/electron_analyser/abstract/base_driver_io.py +8 -3
  17. dodal/devices/electron_analyser/abstract/base_region.py +6 -3
  18. dodal/devices/electron_analyser/detector.py +141 -0
  19. dodal/devices/electron_analyser/enums.py +6 -0
  20. dodal/devices/electron_analyser/specs/__init__.py +2 -0
  21. dodal/devices/electron_analyser/specs/detector.py +1 -1
  22. dodal/devices/electron_analyser/specs/driver_io.py +4 -5
  23. dodal/devices/electron_analyser/specs/enums.py +8 -0
  24. dodal/devices/electron_analyser/specs/region.py +3 -2
  25. dodal/devices/electron_analyser/types.py +30 -4
  26. dodal/devices/electron_analyser/util.py +1 -1
  27. dodal/devices/electron_analyser/vgscienta/__init__.py +2 -0
  28. dodal/devices/electron_analyser/vgscienta/detector.py +1 -1
  29. dodal/devices/electron_analyser/vgscienta/driver_io.py +2 -1
  30. dodal/devices/electron_analyser/vgscienta/enums.py +19 -0
  31. dodal/devices/electron_analyser/vgscienta/region.py +7 -22
  32. dodal/devices/hutch_shutter.py +6 -6
  33. dodal/devices/i10/__init__.py +0 -0
  34. dodal/devices/i10/i10_apple2.py +181 -126
  35. dodal/devices/i22/nxsas.py +1 -1
  36. dodal/devices/oav/oav_detector.py +45 -7
  37. dodal/plans/bimorph.py +333 -0
  38. {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/WHEEL +0 -0
  39. {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/entry_points.txt +0 -0
  40. {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/licenses/LICENSE +0 -0
  41. {dls_dodal-1.48.0.dist-info → dls_dodal-1.50.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/k11.py ADDED
@@ -0,0 +1,35 @@
1
+ from pathlib import Path
2
+
3
+ from ophyd_async.epics.motor import Motor
4
+
5
+ from dodal.common.beamlines.beamline_utils import (
6
+ device_factory,
7
+ set_path_provider,
8
+ )
9
+ from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
10
+ from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider
11
+ from dodal.log import set_beamline as set_log_beamline
12
+ from dodal.utils import BeamlinePrefix, get_beamline_name
13
+
14
+ beamline = get_beamline_name("k11")
15
+ PREFIX = BeamlinePrefix(beamline)
16
+ set_log_beamline(beamline)
17
+ set_utils_beamline(beamline)
18
+
19
+ set_path_provider(
20
+ StaticVisitPathProvider(
21
+ beamline,
22
+ Path("/dls/k11/data/2025/cm40627-3"),
23
+ client=RemoteDirectoryServiceClient("https://k11-control:8088/api"),
24
+ )
25
+ )
26
+
27
+
28
+ @device_factory()
29
+ def kb_x() -> Motor:
30
+ return Motor(f"{PREFIX.beamline_prefix}-OP-KBM-01:CS:X")
31
+
32
+
33
+ @device_factory()
34
+ def kb_y() -> Motor:
35
+ return Motor(f"{PREFIX.beamline_prefix}-OP-KBM-01:CS:Y")
@@ -3,6 +3,7 @@ from dodal.devices.slits import Slits
3
3
  from dodal.utils import skip_device
4
4
 
5
5
  HDF5_SUFFIX = "HDF5:"
6
+ TIFF_SUFFIX = "TIFF:"
6
7
  CAM_SUFFIX = "CAM:"
7
8
  DET_SUFFIX = "DET:"
8
9
 
@@ -1,18 +1,19 @@
1
1
  import abc
2
2
  import asyncio
3
3
  from dataclasses import dataclass
4
+ from math import isclose
4
5
  from typing import Any, Generic, TypeVar
5
6
 
6
7
  import numpy as np
7
8
  from bluesky.protocols import Movable
8
9
  from ophyd_async.core import (
9
10
  AsyncStatus,
10
- Reference,
11
11
  SignalR,
12
12
  SignalW,
13
13
  StandardReadable,
14
14
  StandardReadableFormat,
15
15
  StrictEnum,
16
+ derived_signal_rw,
16
17
  soft_signal_r_and_setter,
17
18
  wait_for_value,
18
19
  )
@@ -23,6 +24,8 @@ from dodal.log import LOGGER
23
24
 
24
25
  T = TypeVar("T")
25
26
 
27
+ DEFAULT_MOTOR_MIN_TIMEOUT = 10
28
+
26
29
 
27
30
  class UndulatorGateStatus(StrictEnum):
28
31
  OPEN = "Open"
@@ -68,8 +71,7 @@ class LookupTableEntries(BaseModel):
68
71
 
69
72
 
70
73
  class Lookuptable(RootModel):
71
- """
72
- BaseModel class for the lookup table.
74
+ """BaseModel class for the lookup table.
73
75
  Apple2 lookup table should be in this format.
74
76
 
75
77
  {mode: {'Energies': {Any: {'Low': float,
@@ -87,6 +89,16 @@ class Lookuptable(RootModel):
87
89
  root: dict[str, LookupTableEntries]
88
90
 
89
91
 
92
+ class Pol(StrictEnum):
93
+ NONE = "None"
94
+ LH = "lh"
95
+ LV = "lv"
96
+ PC = "pc"
97
+ NC = "nc"
98
+ LA = "la"
99
+ LH3 = "lh3"
100
+
101
+
90
102
  ROW_PHASE_MOTOR_TOLERANCE = 0.004
91
103
  MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
92
104
  MAXIMUM_GAP_MOTOR_POSITION = 100
@@ -98,7 +110,7 @@ async def estimate_motor_timeout(
98
110
  vel = await velocity.get_value()
99
111
  cur_pos = await curr_pos.get_value()
100
112
  target_pos = float(await setpoint.get_value())
101
- return abs((target_pos - cur_pos) * 2.0 / vel) + 1
113
+ return abs((target_pos - cur_pos) * 2.0 / vel) + DEFAULT_MOTOR_MIN_TIMEOUT
102
114
 
103
115
 
104
116
  class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
@@ -216,11 +228,10 @@ class UndulatorPhaseMotor(StandardReadable):
216
228
  """
217
229
  fullPV = f"{prefix}BL{infix}"
218
230
  self.user_setpoint = epics_signal_w(str, fullPV + "SET")
219
- self.user_setpoint_demand_readback = epics_signal_r(float, fullPV + "DMD")
220
-
231
+ self.user_setpoint_readback = epics_signal_r(float, fullPV + "DMD")
221
232
  fullPV = fullPV + "MTR"
222
233
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
223
- self.user_setpoint_readback = epics_signal_r(float, fullPV + ".RBV")
234
+ self.user_readback = epics_signal_r(float, fullPV + ".RBV")
224
235
 
225
236
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
226
237
  self.motor_egu = epics_signal_r(str, fullPV + ".EGU")
@@ -282,20 +293,24 @@ class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
282
293
  timeouts = await asyncio.gather(
283
294
  *[
284
295
  estimate_motor_timeout(
285
- axis.user_setpoint_demand_readback,
286
296
  axis.user_setpoint_readback,
297
+ axis.user_readback,
287
298
  axis.velocity,
288
299
  )
289
300
  for axis in axes
290
301
  ]
291
302
  )
292
- return np.max(timeouts)
303
+ """A 2.0 multiplier is required to prevent premature motor timeouts in phase
304
+ axes as it is a master-slave system, where the slave's movement,
305
+ being dependent on the master, can take up to twice as long to complete.
306
+ """
307
+ return np.max(timeouts) * 2.0
293
308
 
294
309
 
295
310
  class UndulatorJawPhase(SafeUndulatorMover[float]):
296
311
  """
297
312
  A JawPhase movable, this is use for moving the jaw phase which is use to control the
298
- linear arbitrary polarisation but only one some of the beamline.
313
+ linear arbitrary polarisation but only on some of the beamline.
299
314
  """
300
315
 
301
316
  def __init__(
@@ -321,23 +336,70 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
321
336
  Get motor speed, current position and target position to calculate required timeout.
322
337
  """
323
338
  return await estimate_motor_timeout(
324
- self.jaw_phase.user_setpoint_demand_readback,
325
339
  self.jaw_phase.user_setpoint_readback,
340
+ self.jaw_phase.user_readback,
326
341
  self.jaw_phase.velocity,
327
342
  )
328
343
 
329
344
 
330
- class Apple2(StandardReadable, Movable):
345
+ class Apple2(abc.ABC, StandardReadable, Movable):
331
346
  """
332
- Apple 2 ID/undulator has 4 extra degrees of freedom compare to the standard Undulator,
333
- each bank of magnet can move independently to each other,
334
- which allow the production of different x-ray polarisation as well as energy.
335
- This type of ID is use on I10, I21, I09, I17 and I06 for soft x-ray.
347
+ Apple2 Undulator Device
348
+
349
+ The `Apple2` class represents an Apple 2 insertion device (undulator) used in synchrotron beamlines.
350
+ This device provides additional degrees of freedom compared to standard undulators, allowing independent
351
+ movement of magnet banks to produce X-rays with various polarisations and energies.
352
+
353
+ The class is designed to manage the undulator's gap, phase motors, and polarisation settings, while
354
+ abstracting hardware interactions and providing a high-level interface for beamline operations.
355
+
336
356
 
337
357
  A pair of look up tables are needed to provide the conversion between motor position
338
- and energy.
339
- This conversion (update_lookuptable) and the way the id move (set) are two abstract
340
- methods that are beamline specific and need to be implemented.
358
+ and energy.
359
+
360
+ Attributes
361
+ ----------
362
+ gap : UndulatorGap
363
+ The gap control device for the undulator.
364
+ phase : UndulatorPhaseAxes
365
+ The phase control device, consisting of four phase motors.
366
+ energy : SignalR
367
+ A soft signal for the current energy readback.
368
+ polarisation_setpoint : SignalR
369
+ A soft signal for the polarisation setpoint.
370
+ polarisation : SignalRW
371
+ A hardware-backed signal for polarisation readback and control.
372
+ lookup_tables : dict
373
+ A dictionary storing lookup tables for gap and phase motor positions, used for energy and polarisation conversion.
374
+ _available_pol : list
375
+ A list of available polarisations supported by the device.
376
+
377
+ Abstract Methods
378
+ ----------------
379
+ set(value: float) -> None
380
+ Abstract method to set motor positions for a given energy and polarisation.
381
+ update_lookuptable() -> None
382
+ Abstract method to load and validate lookup tables from external sources.
383
+
384
+ Methods
385
+ -------
386
+ _set_pol_setpoint(pol: Pol) -> None
387
+ Sets the polarisation setpoint without moving hardware.
388
+ determine_phase_from_hardware(...) -> tuple[Pol, float]
389
+ Determines the polarisation and phase value based on motor positions.
390
+
391
+ Notes
392
+ -----
393
+ - This class requires beamline-specific implementations of the abstract methods.
394
+ - The lookup tables must follow the `Lookuptable` format and be validated before use.
395
+ - The device supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
396
+ positive circular (PC), negative circular (NC), and linear arbitrary (LA).
397
+
398
+ For more detail see
399
+ `UML </_images/apple2_design.png>`__ for detail.
400
+
401
+ .. figure:: /explanations/umls/apple2_design.png
402
+
341
403
  """
342
404
 
343
405
  def __init__(
@@ -348,129 +410,198 @@ class Apple2(StandardReadable, Movable):
348
410
  name: str = "",
349
411
  ) -> None:
350
412
  """
413
+
351
414
  Parameters
352
415
  ----------
353
- id_gap:
354
- An UndulatorGap device.
355
- id_phase:
356
- An UndulatorPhaseAxes device.
357
- prefix:
358
- Not in use but needed for device_instantiation.
359
- name:
360
- Name of the device.
416
+ id_gap: An UndulatorGap device.
417
+ id_phase: An UndulatorPhaseAxes device.
418
+ prefix: Not in use but needed for device_instantiation.
419
+ name: Name of the device.
361
420
  """
362
421
  super().__init__(name)
363
422
 
364
423
  # Attributes are set after super call so they are not renamed to
365
424
  # <name>-undulator, etc.
366
- self.gap = Reference(id_gap)
367
- self.phase = Reference(id_phase)
425
+ self.gap = id_gap
426
+ self.phase = id_phase
368
427
 
369
428
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
370
- # Store the polarisation for readback.
371
- self.polarisation, self._polarisation_set = soft_signal_r_and_setter(
372
- str, initial_value=None
373
- )
374
429
  # Store the set energy for readback.
375
- self.energy, self._energy_set = soft_signal_r_and_setter(
430
+ self.energy, self._set_energy_rbv = soft_signal_r_and_setter(
376
431
  float, initial_value=None
377
432
  )
433
+
434
+ # Store the polarisation for setpoint. And provide readback for LH3.
435
+ # LH3 is a special case as it is indistinguishable from LH in the hardware.
436
+ self.polarisation_setpoint, self._polarisation_setpoint_set = (
437
+ soft_signal_r_and_setter(Pol)
438
+ )
378
439
  # This store two lookup tables, Gap and Phase in the Lookuptable format
379
440
  self.lookup_tables: dict[str, dict[str | None, dict[str, dict[str, Any]]]] = {
380
441
  "Gap": {},
381
442
  "Phase": {},
382
443
  }
383
- # List of available polarisation according to the lookup table.
444
+ # Hardware backed read/write for polarisation.
445
+ self.polarisation = derived_signal_rw(
446
+ raw_to_derived=self._read_pol,
447
+ set_derived=self._set_pol,
448
+ pol=self.polarisation_setpoint,
449
+ top_outer=self.phase.top_outer.user_readback,
450
+ top_inner=self.phase.top_inner.user_readback,
451
+ btm_inner=self.phase.btm_inner.user_readback,
452
+ btm_outer=self.phase.btm_outer.user_readback,
453
+ gap=id_gap.user_readback,
454
+ )
455
+
384
456
  self._available_pol = []
385
- # The polarisation state of the id that are use for internal checking before setting.
386
- self._pol = None
387
457
  """
388
458
  Abstract method that run at start up to load lookup tables into self.lookup_tables
389
- and set available_pol.
459
+ and set available_pol.
390
460
  """
391
461
  self.update_lookuptable()
392
462
 
393
- @property
394
- def pol(self):
395
- return self._pol
463
+ def _set_pol_setpoint(self, pol: Pol) -> None:
464
+ """Set the polarisation setpoint without moving hardware. The polarisation
465
+ setpoint is used to determine the gap and phase motor positions when
466
+ setting the energy/polarisation of the undulator."""
467
+ self._polarisation_setpoint_set(pol)
396
468
 
397
- @pol.setter
398
- def pol(self, pol: str):
399
- # This set the polarisation but does not actually move hardware.
400
- if pol in self._available_pol:
401
- self._pol = pol
402
- else:
403
- raise ValueError(
404
- f"Polarisation {pol} is not available:"
405
- + f"/n Polarisations available: {self._available_pol}"
469
+ async def _set_pol(
470
+ self,
471
+ value: Pol,
472
+ ) -> None:
473
+ # This changes the pol setpoint and then changes polarisation via set energy.
474
+ self._set_pol_setpoint(value)
475
+ await self.set(await self.energy.get_value())
476
+
477
+ @abc.abstractmethod
478
+ @AsyncStatus.wrap
479
+ async def set(self, value: float) -> None:
480
+ """
481
+ Set should be in energy units, this will set the energy of the ID by setting the
482
+ gap and phase motors to the correct position for the given energy
483
+ and polarisation.
484
+ This method should be implemented by the beamline specific ID class as the
485
+ motor positions will be different for each beamline depending on the
486
+ undulator design and the lookup table used.
487
+ _set can be used to set the motor positions for the given energy and
488
+ polarisation provided that all motors can be moved at the same time.
489
+
490
+ Examples
491
+ --------
492
+ >>> RE( id.set(888.0)) # This will set the ID to 888 eV
493
+ >>> RE(scan([detector], id,600,700,100)) # This will scan the ID from 600 to 700 eV in 100 steps.
494
+ """
495
+
496
+ def _read_pol(
497
+ self,
498
+ pol: Pol,
499
+ top_outer: float,
500
+ top_inner: float,
501
+ btm_inner: float,
502
+ btm_outer: float,
503
+ gap: float,
504
+ ) -> Pol:
505
+ LOGGER.info(
506
+ f"Reading polarisation setpoint from hardware: "
507
+ f"top_outer={top_outer}, top_inner={top_inner}, "
508
+ f"btm_inner={btm_inner}, btm_outer={btm_outer}, gap={gap}."
509
+ )
510
+
511
+ read_pol, _ = self.determine_phase_from_hardware(
512
+ top_outer, top_inner, btm_inner, btm_outer, gap
513
+ )
514
+ # LH3 is indistinguishable from LH see determine_phase_from_hardware's docString
515
+ # so we return LH3 if the setpoint is LH3 and the readback is LH.
516
+ if pol == Pol.LH3 and read_pol == Pol.LH:
517
+ LOGGER.info(
518
+ "The hardware cannot distinguish between LH and LH3."
519
+ " Returning the last commanded polarisation value"
406
520
  )
521
+ return Pol.LH3
522
+
523
+ return read_pol
407
524
 
408
525
  async def _set(self, value: Apple2Val, energy: float) -> None:
409
526
  """
410
- Check ID is in a movable state and set all the demand value before moving.
411
-
527
+ Check ID is in a movable state and set all the demand value before moving them
528
+ all at the same time. This should be modified by the beamline specific ID class
529
+ , if the ID motors has to move in a specific order.
412
530
  """
413
531
 
414
532
  # Only need to check gap as the phase motors share both fault and gate with gap.
415
- await self.gap().raise_if_cannot_move()
533
+ await self.gap.raise_if_cannot_move()
416
534
  await asyncio.gather(
417
- self.phase().top_outer.user_setpoint.set(value=value.top_outer),
418
- self.phase().top_inner.user_setpoint.set(value=value.top_inner),
419
- self.phase().btm_inner.user_setpoint.set(value=value.btm_inner),
420
- self.phase().btm_outer.user_setpoint.set(value=value.btm_outer),
421
- self.gap().user_setpoint.set(value=value.gap),
535
+ self.phase.top_outer.user_setpoint.set(value=value.top_outer),
536
+ self.phase.top_inner.user_setpoint.set(value=value.top_inner),
537
+ self.phase.btm_inner.user_setpoint.set(value=value.btm_inner),
538
+ self.phase.btm_outer.user_setpoint.set(value=value.btm_outer),
539
+ self.gap.user_setpoint.set(value=value.gap),
422
540
  )
423
541
  timeout = np.max(
424
- await asyncio.gather(self.gap().get_timeout(), self.phase().get_timeout())
542
+ await asyncio.gather(self.gap.get_timeout(), self.phase.get_timeout())
425
543
  )
426
544
  LOGGER.info(
427
- f"Moving f{self.name} energy and polorisation to {energy}, {self.pol}"
545
+ f"Moving f{self.name} energy and polorisation to {energy}, {await self.polarisation.get_value()}"
428
546
  + f"with motor position {value}, timeout = {timeout}"
429
547
  )
430
-
431
548
  await asyncio.gather(
432
- self.gap().set_move.set(value=1, timeout=timeout),
433
- self.phase().set_move.set(value=1, timeout=timeout),
434
- )
435
- await wait_for_value(
436
- self.gap().gate, UndulatorGateStatus.CLOSE, timeout=timeout
549
+ self.gap.set_move.set(value=1, wait=False, timeout=timeout),
550
+ self.phase.set_move.set(value=1, wait=False, timeout=timeout),
437
551
  )
438
- self._energy_set(energy) # Update energy for after move for readback.
552
+ await wait_for_value(self.gap.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
553
+ self._set_energy_rbv(energy) # Update energy after move for readback.
439
554
 
440
- def _get_id_gap_phase(self, energy: float) -> tuple[float, float]:
555
+ async def _get_id_gap_phase(self, energy: float) -> tuple[float, float]:
441
556
  """
442
557
  Converts energy and polarisation to gap and phase.
443
558
  """
444
- gap_poly = self._get_poly(
559
+ gap_poly = await self._get_poly(
445
560
  lookup_table=self.lookup_tables["Gap"], new_energy=energy
446
561
  )
447
- phase_poly = self._get_poly(
562
+ phase_poly = await self._get_poly(
448
563
  lookup_table=self.lookup_tables["Phase"], new_energy=energy
449
564
  )
450
565
  return gap_poly(energy), phase_poly(energy)
451
566
 
452
- def _get_poly(
567
+ async def _get_poly(
453
568
  self,
454
569
  new_energy: float,
455
570
  lookup_table: dict[str | None, dict[str, dict[str, Any]]],
456
571
  ) -> np.poly1d:
457
572
  """
458
573
  Get the correct polynomial for a given energy form lookuptable
459
- for any given polarisation.
574
+ for the current polarisation setpoint.
575
+ Parameters
576
+ ----------
577
+ new_energy : float
578
+ The energy in eV for which the polynomial is requested.
579
+ lookup_table : dict[str | None, dict[str, dict[str, Any]]]
580
+ The lookup table containing polynomial coefficients for different energies
581
+ and polarisations.
582
+ Returns
583
+ -------
584
+ np.poly1d
585
+ The polynomial coefficients for the requested energy and polarisation.
586
+ Raises
587
+ ------
588
+ ValueError
589
+ If the requested energy is outside the limits defined in the lookup table
590
+ or if no polynomial coefficients are found for the requested energy.
460
591
  """
461
-
592
+ pol = await self.polarisation_setpoint.get_value()
462
593
  if (
463
- new_energy < lookup_table[self.pol]["Limit"]["Minimum"]
464
- or new_energy > lookup_table[self.pol]["Limit"]["Maximum"]
594
+ new_energy < lookup_table[pol]["Limit"]["Minimum"]
595
+ or new_energy > lookup_table[pol]["Limit"]["Maximum"]
465
596
  ):
466
597
  raise ValueError(
467
598
  "Demanding energy must lie between {} and {} eV!".format(
468
- lookup_table[self.pol]["Limit"]["Minimum"],
469
- lookup_table[self.pol]["Limit"]["Maximum"],
599
+ lookup_table[pol]["Limit"]["Minimum"],
600
+ lookup_table[pol]["Limit"]["Maximum"],
470
601
  )
471
602
  )
472
603
  else:
473
- for energy_range in lookup_table[self.pol]["Energies"].values():
604
+ for energy_range in lookup_table[pol]["Energies"].values():
474
605
  if (
475
606
  new_energy >= energy_range["Low"]
476
607
  and new_energy < energy_range["High"]
@@ -492,87 +623,77 @@ class Apple2(StandardReadable, Movable):
492
623
 
493
624
  """
494
625
 
495
- async def determinePhaseFromHardware(self) -> tuple[str | None, float]:
626
+ def determine_phase_from_hardware(
627
+ self,
628
+ top_outer: float,
629
+ top_inner: float,
630
+ btm_inner: float,
631
+ btm_outer: float,
632
+ gap: float,
633
+ ) -> tuple[Pol, float]:
496
634
  """
497
- Try to determine polarisation and phase value using row phase motor position pattern.
635
+ Determine polarisation and phase value using motor position patterns.
498
636
  However there is no way to return lh3 polarisation or higher harmonic setting.
499
637
  (May be for future one can use the inverse poly to work out the energy and try to match it with the current energy
500
638
  to workout the polarisation but during my test the inverse poly is too unstable for general use.)
501
639
  """
502
- top_outer = await self.phase().top_outer.user_setpoint_readback.get_value()
503
- top_inner = await self.phase().top_inner.user_setpoint_readback.get_value()
504
- btm_inner = await self.phase().btm_inner.user_setpoint_readback.get_value()
505
- btm_outer = await self.phase().btm_outer.user_setpoint_readback.get_value()
506
- gap = await self.gap().user_readback.get_value()
507
640
  if gap > MAXIMUM_GAP_MOTOR_POSITION:
508
641
  raise RuntimeError(
509
642
  f"{self.name} is not in use, close gap or set polarisation to use this ID"
510
643
  )
511
644
 
512
645
  if all(
513
- motor_position_equal(x, 0.0)
646
+ isclose(x, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
514
647
  for x in [top_outer, top_inner, btm_inner, btm_outer]
515
648
  ):
516
- # Linear Horizontal
517
- polarisation = "lh"
518
- phase = 0.0
519
- return polarisation, phase
649
+ LOGGER.info("Determined polarisation: LH (Linear Horizontal).")
650
+ return Pol.LH, 0.0
520
651
  if (
521
- motor_position_equal(top_outer, MAXIMUM_ROW_PHASE_MOTOR_POSITION)
522
- and motor_position_equal(top_inner, 0.0)
523
- and motor_position_equal(btm_inner, MAXIMUM_ROW_PHASE_MOTOR_POSITION)
524
- and motor_position_equal(btm_outer, 0.0)
652
+ isclose(
653
+ top_outer,
654
+ MAXIMUM_ROW_PHASE_MOTOR_POSITION,
655
+ abs_tol=ROW_PHASE_MOTOR_TOLERANCE,
656
+ )
657
+ and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
658
+ and isclose(
659
+ btm_inner,
660
+ MAXIMUM_ROW_PHASE_MOTOR_POSITION,
661
+ abs_tol=ROW_PHASE_MOTOR_TOLERANCE,
662
+ )
663
+ and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
525
664
  ):
526
- # Linear Vertical
527
- polarisation = "lv"
528
- phase = MAXIMUM_ROW_PHASE_MOTOR_POSITION
529
- return polarisation, phase
665
+ LOGGER.info("Determined polarisation: LV (Linear Vertical).")
666
+ return Pol.LV, MAXIMUM_ROW_PHASE_MOTOR_POSITION
530
667
  if (
531
- motor_position_equal(top_outer, btm_inner)
668
+ isclose(top_outer, btm_inner, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
532
669
  and top_outer > 0.0
533
- and motor_position_equal(top_inner, 0.0)
534
- and motor_position_equal(btm_outer, 0.0)
670
+ and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
671
+ and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
535
672
  ):
536
- # Positive Circular
537
- polarisation = "pc"
538
- phase = top_outer
539
- return polarisation, phase
673
+ LOGGER.info("Determined polarisation: PC (Positive Circular).")
674
+ return Pol.PC, top_outer
540
675
  if (
541
- motor_position_equal(top_outer, btm_inner)
676
+ isclose(top_outer, btm_inner, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
542
677
  and top_outer < 0.0
543
- and motor_position_equal(top_inner, 0.0)
544
- and motor_position_equal(btm_outer, 0.0)
678
+ and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
679
+ and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
545
680
  ):
546
- # Negative Circular
547
- polarisation = "nc"
548
- phase = top_outer
549
- return polarisation, phase
681
+ LOGGER.info("Determined polarisation: NC (Negative Circular).")
682
+ return Pol.NC, top_outer
550
683
  if (
551
- motor_position_equal(top_outer, -btm_inner)
552
- and motor_position_equal(top_inner, 0.0)
553
- and motor_position_equal(btm_outer, 0.0)
684
+ isclose(top_outer, -btm_inner, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
685
+ and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
686
+ and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
554
687
  ):
555
- # Positive Linear Arbitrary
556
- polarisation = "la"
557
- phase = top_outer
558
- return polarisation, phase
688
+ LOGGER.info("Determined polarisation: LA (Positive Linear Arbitrary).")
689
+ return Pol.LA, top_outer
559
690
  if (
560
- motor_position_equal(top_inner, -btm_outer)
561
- and motor_position_equal(top_outer, 0.0)
562
- and motor_position_equal(btm_inner, 0.0)
691
+ isclose(top_inner, -btm_outer, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
692
+ and isclose(top_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
693
+ and isclose(btm_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
563
694
  ):
564
- # Negative Linear Arbitrary
565
- polarisation = "la"
566
- phase = top_inner
567
- return polarisation, phase
568
- # UNKNOWN default
569
- polarisation = None
570
- phase = 0.0
571
- return (polarisation, phase)
572
-
695
+ LOGGER.info("Determined polarisation: LA (Negative Linear Arbitrary).")
696
+ return Pol.LA, top_inner
573
697
 
574
- def motor_position_equal(a, b) -> bool:
575
- """
576
- Check motor is within tolerance.
577
- """
578
- return abs(a - b) < ROW_PHASE_MOTOR_TOLERANCE
698
+ LOGGER.warning("Unable to determine polarisation. Defaulting to NONE.")
699
+ return Pol.NONE, 0.0
File without changes
@@ -0,0 +1,34 @@
1
+ from ophyd_async.epics.adcore import (
2
+ ADBaseController,
3
+ ADBaseIO,
4
+ ADTIFFWriter,
5
+ AreaDetector,
6
+ )
7
+
8
+ from dodal.common.beamlines.beamline_utils import get_path_provider
9
+ from dodal.common.beamlines.device_helpers import CAM_SUFFIX, TIFF_SUFFIX
10
+
11
+
12
+ class ConstantDeadTimeController(ADBaseController):
13
+ def __init__(self, driver: ADBaseIO, deadtime: float):
14
+ super().__init__(driver)
15
+ self.deadtime = deadtime
16
+
17
+ def get_deadtime(self, exposure: float | None) -> float:
18
+ return self.deadtime
19
+
20
+
21
+ def software_triggered_tiff_area_detector(prefix: str, deadtime: float = 0.0):
22
+ """
23
+ Wrapper for AreaDetector with fixed dead time (defaulted to 0)
24
+ and a TIFF file writer.
25
+ Most detectors in B16 could be configured like this
26
+ """
27
+ return AreaDetector(
28
+ writer=ADTIFFWriter.with_io(
29
+ prefix=prefix, path_provider=get_path_provider(), fileio_suffix=TIFF_SUFFIX
30
+ ),
31
+ controller=ConstantDeadTimeController(
32
+ driver=ADBaseIO(prefix + CAM_SUFFIX), deadtime=deadtime
33
+ ),
34
+ )