dls-dodal 1.62.0__py3-none-any.whl → 1.63.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 (79) hide show
  1. {dls_dodal-1.62.0.dist-info → dls_dodal-1.63.0.dist-info}/METADATA +1 -1
  2. {dls_dodal-1.62.0.dist-info → dls_dodal-1.63.0.dist-info}/RECORD +76 -71
  3. dls_dodal-1.63.0.dist-info/entry_points.txt +3 -0
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +1 -0
  6. dodal/beamlines/adsim.py +5 -3
  7. dodal/beamlines/b21.py +3 -1
  8. dodal/beamlines/i02_2.py +32 -0
  9. dodal/beamlines/i03.py +9 -0
  10. dodal/beamlines/i09.py +10 -3
  11. dodal/beamlines/i09_1.py +9 -3
  12. dodal/beamlines/i10.py +7 -69
  13. dodal/beamlines/i10_1.py +35 -0
  14. dodal/beamlines/i10_optics.py +205 -0
  15. dodal/beamlines/i15_1.py +5 -5
  16. dodal/beamlines/i17.py +50 -1
  17. dodal/beamlines/i18.py +15 -9
  18. dodal/beamlines/i19_1.py +3 -3
  19. dodal/beamlines/i19_2.py +2 -2
  20. dodal/beamlines/i19_optics.py +4 -1
  21. dodal/beamlines/i24.py +3 -3
  22. dodal/cli.py +4 -4
  23. dodal/common/visit.py +4 -4
  24. dodal/devices/aperturescatterguard.py +6 -4
  25. dodal/devices/apple2_undulator.py +211 -114
  26. dodal/devices/attenuator/filter_selections.py +6 -6
  27. dodal/devices/common_dcm.py +62 -15
  28. dodal/devices/current_amplifiers/femto.py +4 -4
  29. dodal/devices/current_amplifiers/sr570.py +3 -3
  30. dodal/devices/fast_grid_scan.py +4 -4
  31. dodal/devices/fast_shutter.py +19 -7
  32. dodal/devices/i02_2/__init__.py +0 -0
  33. dodal/devices/i03/dcm.py +4 -2
  34. dodal/devices/i04/murko_results.py +35 -14
  35. dodal/devices/i09/__init__.py +1 -2
  36. dodal/devices/i10/__init__.py +29 -0
  37. dodal/devices/i10/diagnostics.py +37 -5
  38. dodal/devices/i10/i10_apple2.py +125 -229
  39. dodal/devices/i10/slits.py +38 -6
  40. dodal/devices/i15/dcm.py +6 -45
  41. dodal/devices/i17/__init__.py +0 -0
  42. dodal/devices/i17/i17_apple2.py +51 -0
  43. dodal/devices/i19/access_controlled/__init__.py +0 -0
  44. dodal/devices/i19/{shutter.py → access_controlled/shutter.py} +7 -4
  45. dodal/devices/i22/dcm.py +2 -2
  46. dodal/devices/i24/dcm.py +2 -2
  47. dodal/devices/oav/oav_detector.py +1 -1
  48. dodal/devices/oav/oav_parameters.py +4 -4
  49. dodal/devices/oav/oav_to_redis_forwarder.py +4 -4
  50. dodal/devices/oav/pin_image_recognition/__init__.py +3 -3
  51. dodal/devices/oav/pin_image_recognition/utils.py +1 -1
  52. dodal/devices/oav/snapshots/snapshot.py +1 -1
  53. dodal/devices/oav/snapshots/snapshot_image_processing.py +12 -12
  54. dodal/devices/oav/snapshots/snapshot_with_grid.py +1 -1
  55. dodal/devices/oav/utils.py +2 -2
  56. dodal/devices/pgm.py +3 -3
  57. dodal/devices/robot.py +5 -5
  58. dodal/devices/tetramm.py +9 -5
  59. dodal/devices/thawer.py +0 -4
  60. dodal/devices/v2f.py +2 -2
  61. dodal/devices/zebra/zebra_constants_mapping.py +2 -2
  62. dodal/devices/zocalo/__init__.py +4 -4
  63. dodal/devices/zocalo/zocalo_results.py +4 -4
  64. dodal/log.py +9 -9
  65. dodal/plan_stubs/motor_utils.py +4 -4
  66. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -2
  67. dodal/plans/save_panda.py +7 -7
  68. dodal/plans/verify_undulator_gap.py +2 -2
  69. dls_dodal-1.62.0.dist-info/entry_points.txt +0 -3
  70. dodal/beamlines/i10-1.py +0 -25
  71. dodal/devices/i09/dcm.py +0 -26
  72. {dls_dodal-1.62.0.dist-info → dls_dodal-1.63.0.dist-info}/WHEEL +0 -0
  73. {dls_dodal-1.62.0.dist-info → dls_dodal-1.63.0.dist-info}/licenses/LICENSE +0 -0
  74. {dls_dodal-1.62.0.dist-info → dls_dodal-1.63.0.dist-info}/top_level.txt +0 -0
  75. /dodal/devices/areadetector/plugins/{CAM.py → cam.py} +0 -0
  76. /dodal/devices/areadetector/plugins/{MJPG.py → mjpg.py} +0 -0
  77. /dodal/devices/i18/{KBMirror.py → kb_mirror.py} +0 -0
  78. /dodal/devices/i19/{blueapi_device.py → access_controlled/blueapi_device.py} +0 -0
  79. /dodal/devices/i19/{hutch_access.py → access_controlled/hutch_access.py} +0 -0
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  import csv
3
2
  import io
4
3
  from dataclasses import dataclass
@@ -10,20 +9,18 @@ from bluesky.protocols import Movable
10
9
  from daq_config_server.client import ConfigServer
11
10
  from ophyd_async.core import (
12
11
  AsyncStatus,
13
- Device,
14
12
  Reference,
15
13
  StandardReadable,
16
14
  StandardReadableFormat,
17
- soft_signal_r_and_setter,
15
+ derived_signal_rw,
18
16
  soft_signal_rw,
19
17
  )
20
18
  from pydantic import BaseModel, ConfigDict, RootModel
21
19
 
22
20
  from dodal.devices.apple2_undulator import (
23
21
  Apple2,
24
- Apple2Motors,
22
+ Apple2Controller,
25
23
  Apple2Val,
26
- EnergyMotorConvertor,
27
24
  Pol,
28
25
  UndulatorGap,
29
26
  UndulatorJawPhase,
@@ -31,8 +28,6 @@ from dodal.devices.apple2_undulator import (
31
28
  )
32
29
  from dodal.log import LOGGER
33
30
 
34
- from ..pgm import PGM
35
-
36
31
  ROW_PHASE_MOTOR_TOLERANCE = 0.004
37
32
  MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
38
33
  MAXIMUM_GAP_MOTOR_POSITION = 100
@@ -107,7 +102,7 @@ class I10EnergyMotorLookup:
107
102
 
108
103
  def __init__(
109
104
  self,
110
- look_up_table_dir: str,
105
+ lookuptable_dir: str,
111
106
  source: tuple[str, str],
112
107
  config_client: ConfigServer,
113
108
  mode: str = "Mode",
@@ -141,8 +136,8 @@ class I10EnergyMotorLookup:
141
136
  "Gap": {},
142
137
  "Phase": {},
143
138
  }
144
- energy_gap_table_path = Path(look_up_table_dir, gap_file_name)
145
- energy_phase_table_path = Path(look_up_table_dir, phase_file_name)
139
+ energy_gap_table_path = Path(lookuptable_dir, gap_file_name)
140
+ energy_phase_table_path = Path(lookuptable_dir, phase_file_name)
146
141
  self.lookup_table_config = LookupTableConfig(
147
142
  path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
148
143
  source=source,
@@ -322,82 +317,123 @@ class I10EnergyMotorLookup:
322
317
 
323
318
 
324
319
  class I10Apple2(Apple2):
325
- """I10Apple2 is the i10 version of Apple2 ID, set and energy_motor_convertor
326
- should be the only part that is I10 specific.
320
+ def __init__(
321
+ self,
322
+ id_gap: UndulatorGap,
323
+ id_phase: UndulatorPhaseAxes,
324
+ id_jaw_phase: UndulatorJawPhase,
325
+ name: str = "",
326
+ ) -> None:
327
+ """
328
+ I10Apple2 device is an apple2 with extra jaw phase motor.
327
329
 
328
- A EnergyMotorConvertor function is needed to provide the conversion between
329
- x-ray motor position and energy.
330
+ Parameters
331
+ ----------
330
332
 
331
- Set is in energy(eV).
333
+ id_gap : UndulatorJawPhase
334
+ The gap motor of the undulator.
335
+ id_phase : UndulatorJawPhase
336
+ The phase motors of the undulator.
337
+ id_jaw_phase : UndulatorJawPhase
338
+ The jaw phase motor of the undulator.
339
+ name : str, optional
340
+ The name of the device, by default "".
341
+ """
342
+ with self.add_children_as_readables():
343
+ self.jaw_phase = id_jaw_phase
344
+ super().__init__(id_gap=id_gap, id_phase=id_phase, name=name)
345
+
346
+
347
+ class I10Apple2Controller(Apple2Controller[I10Apple2]):
348
+ """
349
+ I10Apple2Controller is a extension of Apple2Controller which provide linear
350
+ arbitrary angle control.
332
351
  """
333
352
 
334
353
  def __init__(
335
354
  self,
336
- prefix: str,
337
- energy_motor_convertor: EnergyMotorConvertor,
355
+ apple2: I10Apple2,
356
+ lookuptable_dir: str,
357
+ source: tuple[str, str],
358
+ config_client: ConfigServer,
359
+ jaw_phase_limit: float = 12.0,
360
+ jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
361
+ angle_threshold_deg=30.0,
338
362
  name: str = "",
339
363
  ) -> None:
340
364
  """
341
- Parameters
365
+
366
+ parameters
342
367
  ----------
343
- look_up_table_dir:
368
+ apple2 : I10Apple2
369
+ An I10Apple2 device.
370
+ lookuptable_dir : str
344
371
  The path to look up table.
345
- source:
346
- The column name and the name of the source in look up table. e.g. ("source", "idu")
347
- mode:
348
- The column name of the mode in look up table.
349
- min_energy:
350
- The column name that contain the maximum energy in look up table.
351
- max_energy:
352
- The column name that contain the maximum energy in look up table.
353
- poly_deg:
354
- The column names for the parameters for the energy conversion polynomial, starting with the least significant.
355
- prefix:
356
- epic pv for id
357
- Name:
358
- Name of the device
372
+ source : tuple[str, str]
373
+ The column name and the name of the source in look up table. e.g. ( "source", "idu")
374
+ config_client : ConfigServer
375
+ The config server client to fetch the look up table.
376
+ jaw_phase_limit : float, optional
377
+ The maximum allowed jaw_phase movement., by default 12.0
378
+ jaw_phase_poly_param : list[float], optional
379
+ polynomial parameters highest power first., by default DEFAULT_JAW_PHASE_POLY_PARAMS
380
+ angle_threshold_deg : float, optional
381
+ The angle threshold to switch between 0-180 and 180-360 range., by default 30.0
382
+ name : str, optional
383
+ New device name.
359
384
  """
360
385
 
361
- with self.add_children_as_readables():
362
- super().__init__(
363
- apple2_motors=Apple2Motors(
364
- id_gap=UndulatorGap(prefix=prefix),
365
- id_phase=UndulatorPhaseAxes(
366
- prefix=prefix,
367
- top_outer="RPQ1",
368
- top_inner="RPQ2",
369
- btm_inner="RPQ3",
370
- btm_outer="RPQ4",
371
- ),
372
- ),
373
- energy_motor_convertor=energy_motor_convertor,
374
- name=name,
375
- )
376
- self.id_jaw_phase = UndulatorJawPhase(
377
- prefix=prefix,
378
- move_pv="RPQ1",
379
- )
386
+ self.lookup_table_client = I10EnergyMotorLookup(
387
+ lookuptable_dir=lookuptable_dir,
388
+ source=source,
389
+ config_client=config_client,
390
+ )
391
+ super().__init__(
392
+ apple2=apple2,
393
+ energy_to_motor_converter=self.lookup_table_client.get_motor_from_energy,
394
+ name=name,
395
+ )
380
396
 
381
- async def _set(self, value: float) -> None:
382
- """
383
- Check polarisation state and use it together with the energy(value)
384
- to calculate the required gap and phases before setting it.
385
- """
397
+ self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
398
+ self.angle_threshold_deg = angle_threshold_deg
399
+ self.jaw_phase_limit = jaw_phase_limit
400
+ self._linear_arbitrary_angle = soft_signal_rw(float, initial_value=None)
386
401
 
387
- pol = await self.polarisation_setpoint.get_value()
402
+ self.linear_arbitrary_angle = derived_signal_rw(
403
+ raw_to_derived=self._read_linear_arbitrary_angle,
404
+ set_derived=self._set_linear_arbitrary_angle,
405
+ pol_angle=self._linear_arbitrary_angle,
406
+ pol=self.polarisation,
407
+ )
408
+
409
+ def _read_linear_arbitrary_angle(self, pol_angle: float, pol: Pol) -> float:
410
+ self._raise_if_not_la(pol)
411
+ return pol_angle
388
412
 
389
- if pol == Pol.NONE:
390
- LOGGER.warning(
391
- "Found no setpoint for polarisation. Attempting to"
392
- " determine polarisation from hardware..."
413
+ async def _set_linear_arbitrary_angle(self, pol_angle: float) -> None:
414
+ pol = await self.polarisation.get_value()
415
+ self._raise_if_not_la(pol)
416
+ # Moving to real angle which is 210 to 30.
417
+ alpha_real = (
418
+ pol_angle
419
+ if pol_angle > self.angle_threshold_deg
420
+ else pol_angle + ALPHA_OFFSET
421
+ )
422
+ jaw_phase = self.jaw_phase_from_angle(alpha_real)
423
+ if abs(jaw_phase) > self.jaw_phase_limit:
424
+ raise RuntimeError(
425
+ f"jaw_phase position for angle ({pol_angle}) is outside permitted range"
426
+ f" [-{self.jaw_phase_limit}, {self.jaw_phase_limit}]"
393
427
  )
394
- pol = await self.polarisation.get_value()
395
- if pol == Pol.NONE:
396
- raise ValueError(
397
- f"Polarisation cannot be determined from hardware for {self.name}"
398
- )
428
+ await self.apple2().jaw_phase.set(jaw_phase)
429
+ await self._linear_arbitrary_angle.set(pol_angle)
430
+
431
+ async def _set_motors_from_energy(self, value: float) -> None:
432
+ """
433
+ Set the undulator motors for a given energy and polarisation.
434
+ """
399
435
 
400
- self._set_pol_setpoint(pol)
436
+ pol = await self._check_and_get_pol_setpoint()
401
437
  gap, phase = self.energy_to_motor(energy=value, pol=pol)
402
438
  phase3 = phase * (-1 if pol == Pol.LA else 1)
403
439
  id_set_val = Apple2Val(
@@ -409,185 +445,45 @@ class I10Apple2(Apple2):
409
445
  )
410
446
 
411
447
  LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
412
- await self.motors.set(id_motor_values=id_set_val)
448
+ await self.apple2().set(id_motor_values=id_set_val)
413
449
  if pol != Pol.LA:
414
- await self.id_jaw_phase.set(0)
415
- await self.id_jaw_phase.set_move.set(1)
416
-
417
-
418
- class EnergySetter(StandardReadable, Movable[float]):
419
- """
420
- Compound device to set both ID and PGM energy at the same time.
421
-
422
- """
423
-
424
- def __init__(self, id: I10Apple2, pgm: PGM, name: str = "") -> None:
425
- """
426
- Parameters
427
- ----------
428
- id:
429
- An Apple2 device.
430
- pgm:
431
- A PGM/mono device.
432
- name:
433
- New device name.
434
- """
435
- super().__init__(name=name)
436
- self.id = id
437
- self.pgm_ref = Reference(pgm)
438
-
439
- self.add_readables(
440
- [self.id.energy, self.pgm_ref().energy.user_readback],
441
- StandardReadableFormat.HINTED_SIGNAL,
442
- )
443
-
444
- with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
445
- self.energy_offset = soft_signal_rw(float, initial_value=0)
446
-
447
- @AsyncStatus.wrap
448
- async def set(self, value: float) -> None:
449
- LOGGER.info(f"Moving f{self.name} energy to {value}.")
450
- await asyncio.gather(
451
- self.id.set(value=value + await self.energy_offset.get_value()),
452
- self.pgm_ref().energy.set(value),
453
- )
454
-
455
-
456
- class I10Apple2Pol(StandardReadable, Movable[Pol]):
457
- """
458
- Compound device to set polorisation of ID.
459
- """
460
-
461
- def __init__(self, id: I10Apple2, name: str = "") -> None:
462
- """
463
- Parameters
464
- ----------
465
- id:
466
- An I10Apple2 device.
467
- name:
468
- New device name.
469
- """
470
- super().__init__(name=name)
471
- self.id_ref = Reference(id)
472
- self.add_readables([self.id_ref().polarisation])
450
+ await self.apple2().jaw_phase.set(0)
451
+ await self.apple2().jaw_phase.set_move.set(1)
473
452
 
474
- @AsyncStatus.wrap
475
- async def set(self, value: Pol) -> None:
476
- LOGGER.info(f"Changing f{self.name} polarisation to {value}.")
477
- # Timeout is determined internally by the set method later, so we set it to max here.
478
- await self.id_ref().polarisation.set(value, timeout=MAXIMUM_MOVE_TIME)
453
+ def _raise_if_not_la(self, pol: Pol) -> None:
454
+ if pol != Pol.LA:
455
+ raise RuntimeError(
456
+ "Angle control is not available in polarisation"
457
+ + f" {pol} with {self.name}"
458
+ )
479
459
 
480
460
 
481
461
  class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
482
462
  """
483
- Device to set polorisation angle of the ID. Linear Arbitrary Angle (laa)
484
- is the direction of the magnetic field which can be change by varying the jaw_phase
485
- in (linear arbitrary (la) mode,
486
- The angle of 0 is equivalent to linear horizontal "lh" (sigma) and
487
- 90 is linear vertical "lv" (pi).
488
- This device require a jaw_phase to angle conversion which is done via a polynomial.
463
+ Device to set the polarisation angle of the Apple2 undulator in Linear Arbitrary (LA) mode.
489
464
  """
490
465
 
491
466
  def __init__(
492
467
  self,
493
- id: I10Apple2,
468
+ id_controller: I10Apple2Controller,
494
469
  name: str = "",
495
- jaw_phase_limit: float = 12.0,
496
- jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
497
- angle_threshold_deg=30.0,
498
470
  ) -> None:
499
471
  """
500
472
  Parameters
501
473
  ----------
502
- id: I10Apple2
503
- An I10Apple2 device.
504
- name: str
474
+ id_controller : I10Apple2Controller
475
+ The I10Apple2Controller which control the ID.
476
+ name : str, optional
505
477
  New device name.
506
- jaw_phase_limit: float
507
- The maximum allowed jaw_phase movement.
508
- jaw_phase_poly_param: list
509
- polynomial parameters highest power first.
510
478
  """
511
479
  super().__init__(name=name)
512
- self.id_ref = Reference(id)
513
- self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
514
- self.angle_threshold_deg = angle_threshold_deg
515
- self.jaw_phase_limit = jaw_phase_limit
516
- with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
517
- self.angle, self._angle_set = soft_signal_r_and_setter(
518
- float, initial_value=None
519
- )
480
+ self.linear_arbitrary_angle = Reference(id_controller.linear_arbitrary_angle)
520
481
 
521
- @AsyncStatus.wrap
522
- async def set(self, value: SupportsFloat) -> None:
523
- value = float(value)
524
- pol = await self.id_ref().polarisation.get_value()
525
- if pol != Pol.LA:
526
- raise RuntimeError(
527
- f"Angle control is not available in polarisation {pol} with {self.id_ref().name}"
528
- )
529
- # Moving to real angle which is 210 to 30.
530
- alpha_real = value if value > self.angle_threshold_deg else value + ALPHA_OFFSET
531
- jaw_phase = self.jaw_phase_from_angle(alpha_real)
532
- if abs(jaw_phase) > self.jaw_phase_limit:
533
- raise RuntimeError(
534
- f"jaw_phase position for angle ({value}) is outside permitted range"
535
- f" [-{self.jaw_phase_limit}, {self.jaw_phase_limit}]"
536
- )
537
- await self.id_ref().id_jaw_phase.set(jaw_phase)
538
- self._angle_set(value)
539
-
540
-
541
- class I10Id(Device):
542
- def __init__(
543
- self,
544
- pgm: PGM,
545
- prefix: str,
546
- look_up_table_dir: str,
547
- source: tuple[str, str],
548
- config_client: ConfigServer,
549
- jaw_phase_limit=12.0,
550
- jaw_phase_poly_param=DEFAULT_JAW_PHASE_POLY_PARAMS,
551
- angle_threshold_deg=30.0,
552
- name: str = "",
553
- ) -> None:
554
- """I10Id is a compound device that combines the I10-specific Apple2 undulator,
555
- energy setter, and polarization control.
556
- This class provides a high-level interface for controlling the undulator's
557
- energy, polarization, and linear arbitrary angle.
558
-
559
- Attributes
560
- ----------
561
- id : I10Apple2
562
- The I10-specific Apple2 undulator device.
563
- energy_setter : EnergySetter
564
- A device for synchronizing the undulator and monochromator energy.
565
- pol : I10Apple2Pol
566
- A device for controlling the polarization of the undulator.
567
- linear_arbitrary_angle : LinearArbitraryAngle
568
- A device for controlling the linear arbitrary polarization angle.
569
- """
570
- self.lookup_table_client = I10EnergyMotorLookup(
571
- look_up_table_dir=look_up_table_dir,
572
- source=source,
573
- config_client=config_client,
574
- )
575
- self.energy = EnergySetter(
576
- id=I10Apple2(
577
- prefix=prefix,
578
- energy_motor_convertor=self.lookup_table_client.get_motor_from_energy,
579
- name="id_energy",
580
- ),
581
- pgm=pgm,
582
- name="energy",
583
- )
584
- self.pol = I10Apple2Pol(id=self.energy.id, name="pol")
585
- self.laa = LinearArbitraryAngle(
586
- id=self.energy.id,
587
- name="laa",
588
- jaw_phase_limit=jaw_phase_limit,
589
- jaw_phase_poly_param=jaw_phase_poly_param,
590
- angle_threshold_deg=angle_threshold_deg,
482
+ self.add_readables(
483
+ [self.linear_arbitrary_angle()],
484
+ StandardReadableFormat.HINTED_SIGNAL,
591
485
  )
592
486
 
593
- super().__init__(name=name)
487
+ @AsyncStatus.wrap
488
+ async def set(self, angle: float) -> None:
489
+ await self.linear_arbitrary_angle().set(angle)
@@ -77,9 +77,7 @@ class I10PrimarySlits(Slits):
77
77
  )
78
78
 
79
79
 
80
- class I10Slits(Device):
81
- """Collection of all the i10 slits before end station."""
82
-
80
+ class I10SharedSlits(Device):
83
81
  def __init__(self, prefix: str, name: str = "") -> None:
84
82
  self.s1 = I10PrimarySlits(
85
83
  prefix=prefix + "01:",
@@ -90,6 +88,13 @@ class I10Slits(Device):
90
88
  self.s3 = I10SlitsBlades(
91
89
  prefix=prefix + "03:",
92
90
  )
91
+ super().__init__(name=name)
92
+
93
+
94
+ class I10Slits(Device):
95
+ """Collection of all the i10 slits before end station."""
96
+
97
+ def __init__(self, prefix: str, name: str = "") -> None:
93
98
  self.s4 = MinimalSlits(
94
99
  prefix=prefix + "04:",
95
100
  x_gap="XSIZE",
@@ -104,9 +109,39 @@ class I10Slits(Device):
104
109
  super().__init__(name=name)
105
110
 
106
111
 
112
+ class I10JSlits(Device):
113
+ """Collection of all the i10-1 slits before end station."""
114
+
115
+ def __init__(self, prefix: str, name: str = "") -> None:
116
+ self.s7 = MinimalSlits(
117
+ prefix=prefix + "01:",
118
+ x_gap="XSIZE",
119
+ y_gap="YSIZE",
120
+ )
121
+ self.s8 = I10SlitsBlades(
122
+ prefix=prefix + "02:",
123
+ )
124
+ self.s9 = I10SlitsBlades(
125
+ prefix=prefix + "03:",
126
+ )
127
+ super().__init__(name=name)
128
+
129
+
107
130
  class I10SlitsDrainCurrent(Device):
108
131
  """Collection of all the drain current from i10 slits."""
109
132
 
133
+ def __init__(
134
+ self, prefix: str, name: str = "", connector: DeviceConnector | None = None
135
+ ) -> None:
136
+ self.s4 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-02:")
137
+ self.s5 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-03:")
138
+ self.s6 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-04:")
139
+ super().__init__(name, connector)
140
+
141
+
142
+ class I10SharedSlitsDrainCurrent(Device):
143
+ """Collection of all the drain current from i10 and i10-1 slits."""
144
+
110
145
  def __init__(
111
146
  self, prefix: str, name: str = "", connector: DeviceConnector | None = None
112
147
  ) -> None:
@@ -118,7 +153,4 @@ class I10SlitsDrainCurrent(Device):
118
153
  suffix_bot_blade="YMINUS:I",
119
154
  )
120
155
  self.s3 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-01:")
121
- self.s4 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-02:")
122
- self.s5 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-03:")
123
- self.s6 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-04:")
124
156
  super().__init__(name, connector)
dodal/devices/i15/dcm.py CHANGED
@@ -1,9 +1,7 @@
1
- from typing import Generic, TypeVar
2
-
3
- from ophyd_async.core import StandardReadable
4
1
  from ophyd_async.epics.motor import Motor
5
2
 
6
3
  from dodal.devices.common_dcm import (
4
+ DoubleCrystalMonochromatorBase,
7
5
  StationaryCrystal,
8
6
  )
9
7
 
@@ -24,50 +22,13 @@ class ThetaRollYZCrystal(ThetaYCrystal):
24
22
  super().__init__(prefix)
25
23
 
26
24
 
27
- Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
28
- Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
29
-
30
-
31
- class BaseDCMforI15(StandardReadable, Generic[Xtal_1, Xtal_2]):
32
- """
33
- Device for double crystal monochromators (DCM), which only allow energy of the beam to be selected.
34
-
35
- Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
36
- each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
37
- This device only accounts for combinations of energy plus two crystals.
38
-
39
- This device is designed to be a drop in replacement for BaseDCM for i15, which doesn't require WAVELENGTH, BRAGG and OFFSET to
40
- be available. Once the i15 DCM supports all of the PVs required by BaseDCM, the i15 DCM device can switch to inheriting from
41
- BaseDCM and this class can be removed.
42
- """
43
-
44
- def __init__(
45
- self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
46
- ) -> None:
47
- with self.add_children_as_readables():
48
- # Virtual motor PV's which set the physical motors so that the DCM produces requested
49
- # wavelength/energy
50
- self.energy_in_kev = Motor(prefix + "ENERGY")
51
- self._make_crystals(prefix, xtal_1, xtal_2)
52
-
53
- super().__init__(name)
54
-
55
- # Prefix convention is different depending on whether there are one or two controllable crystals
56
- def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]):
57
- if StationaryCrystal not in [xtal_1, xtal_2]:
58
- self.xtal_1 = xtal_1(f"{prefix}XTAL1:")
59
- self.xtal_2 = xtal_2(f"{prefix}XTAL2:")
60
- else:
61
- self.xtal_1 = xtal_1(prefix)
62
- self.xtal_2 = xtal_2(prefix)
63
-
64
-
65
- class DCM(BaseDCMforI15[ThetaRollYZCrystal, ThetaYCrystal]):
25
+ class DCM(DoubleCrystalMonochromatorBase[ThetaRollYZCrystal, ThetaYCrystal]):
66
26
  """
67
- A double crystal monocromator device, used to select the beam energy.
27
+ A double crystal monochromator device, used to select the beam energy.
68
28
 
69
- Once the i15 DCM supports all of the PVs required by BaseDCM, this class can be
70
- changed to inherit from BaseDCM and BaseDCMforI15 can be removed.
29
+ Once the i15 DCM supports all of the PVs required by DoubleCrystalMonochromator or
30
+ DoubleCrystalMonochromatorWithDSpacing this class can be changed to inherit from it,
31
+ see https://jira.diamond.ac.uk/browse/I15-1053 for more info.
71
32
  """
72
33
 
73
34
  def __init__(self, prefix: str, name: str = "") -> None:
File without changes
@@ -0,0 +1,51 @@
1
+ from dodal.devices.apple2_undulator import (
2
+ Apple2,
3
+ Apple2Controller,
4
+ Apple2Val,
5
+ EnergyMotorConvertor,
6
+ )
7
+ from dodal.log import LOGGER
8
+
9
+ ROW_PHASE_MOTOR_TOLERANCE = 0.004
10
+ MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
11
+ MAXIMUM_GAP_MOTOR_POSITION = 100
12
+ DEFAULT_JAW_PHASE_POLY_PARAMS = [1.0 / 7.5, -120.0 / 7.5]
13
+ ALPHA_OFFSET = 180
14
+ MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
15
+
16
+
17
+ class I17Apple2Controller(Apple2Controller[Apple2]):
18
+ """
19
+ I10Apple2Controller is a extension of Apple2Controller which provide linear
20
+ arbitrary angle control.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ apple2: Apple2,
26
+ energy_to_motor_converter: EnergyMotorConvertor,
27
+ name: str = "",
28
+ ) -> None:
29
+ super().__init__(
30
+ apple2=apple2,
31
+ energy_to_motor_converter=energy_to_motor_converter,
32
+ name=name,
33
+ )
34
+
35
+ async def _set_motors_from_energy(self, value: float) -> None:
36
+ """
37
+ Set the undulator motors for a given energy and polarisation.
38
+ """
39
+
40
+ pol = await self._check_and_get_pol_setpoint()
41
+ gap, phase = self.energy_to_motor(energy=value, pol=pol)
42
+ id_set_val = Apple2Val(
43
+ top_outer=f"{phase:.6f}",
44
+ top_inner="0.0",
45
+ btm_inner=f"{phase:.6f}",
46
+ btm_outer="0.0",
47
+ gap=f"{gap:.6f}",
48
+ )
49
+
50
+ LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
51
+ await self.apple2().set(id_motor_values=id_set_val)
File without changes
@@ -2,8 +2,11 @@ from ophyd_async.core import AsyncStatus, StandardReadableFormat
2
2
  from ophyd_async.epics.core import epics_signal_r
3
3
 
4
4
  from dodal.devices.hutch_shutter import ShutterDemand, ShutterState
5
- from dodal.devices.i19.blueapi_device import HutchState, OpticsBlueAPIDevice
6
- from dodal.devices.i19.hutch_access import ACCESS_DEVICE_NAME
5
+ from dodal.devices.i19.access_controlled.blueapi_device import (
6
+ HutchState,
7
+ OpticsBlueAPIDevice,
8
+ )
9
+ from dodal.devices.i19.access_controlled.hutch_access import ACCESS_DEVICE_NAME
7
10
 
8
11
 
9
12
  class AccessControlledShutter(OpticsBlueAPIDevice):
@@ -39,7 +42,7 @@ class AccessControlledShutter(OpticsBlueAPIDevice):
39
42
 
40
43
  @AsyncStatus.wrap
41
44
  async def set(self, value: ShutterDemand):
42
- REQUEST_PARAMS = {
45
+ request_params = {
43
46
  "name": "operate_shutter_plan",
44
47
  "params": {
45
48
  "experiment_hutch": self.hutch_request.value,
@@ -48,4 +51,4 @@ class AccessControlledShutter(OpticsBlueAPIDevice):
48
51
  },
49
52
  "instrument_session": self.instrument_session,
50
53
  }
51
- await super().set(REQUEST_PARAMS)
54
+ await super().set(request_params)