ophyd-async 0.13.3__py3-none-any.whl → 0.13.5__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 (42) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +26 -3
  3. ophyd_async/core/_derived_signal_backend.py +2 -1
  4. ophyd_async/core/_detector.py +2 -2
  5. ophyd_async/core/_device.py +9 -9
  6. ophyd_async/core/_enums.py +5 -0
  7. ophyd_async/core/_signal.py +34 -38
  8. ophyd_async/core/_signal_backend.py +3 -1
  9. ophyd_async/core/_status.py +2 -2
  10. ophyd_async/core/_table.py +8 -0
  11. ophyd_async/core/_utils.py +11 -11
  12. ophyd_async/epics/adcore/_core_logic.py +3 -1
  13. ophyd_async/epics/adcore/_utils.py +4 -4
  14. ophyd_async/epics/core/_aioca.py +2 -2
  15. ophyd_async/epics/core/_p4p.py +2 -2
  16. ophyd_async/epics/motor.py +28 -7
  17. ophyd_async/epics/pmac/_pmac_io.py +8 -4
  18. ophyd_async/epics/pmac/_pmac_trajectory.py +144 -41
  19. ophyd_async/epics/pmac/_pmac_trajectory_generation.py +692 -0
  20. ophyd_async/epics/pmac/_utils.py +1 -681
  21. ophyd_async/fastcs/jungfrau/__init__.py +2 -1
  22. ophyd_async/fastcs/jungfrau/_controller.py +29 -11
  23. ophyd_async/fastcs/jungfrau/_utils.py +10 -2
  24. ophyd_async/fastcs/panda/__init__.py +10 -0
  25. ophyd_async/fastcs/panda/_block.py +14 -0
  26. ophyd_async/fastcs/panda/_trigger.py +123 -3
  27. ophyd_async/sim/_motor.py +4 -2
  28. ophyd_async/sim/_stage.py +14 -4
  29. ophyd_async/tango/core/__init__.py +17 -3
  30. ophyd_async/tango/core/_signal.py +18 -22
  31. ophyd_async/tango/core/_tango_transport.py +407 -239
  32. ophyd_async/tango/core/_utils.py +9 -0
  33. ophyd_async/tango/demo/_mover.py +1 -2
  34. ophyd_async/tango/testing/__init__.py +2 -1
  35. ophyd_async/tango/testing/_one_of_everything.py +13 -5
  36. ophyd_async/tango/testing/_test_config.py +11 -0
  37. ophyd_async/testing/_assert.py +2 -2
  38. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/METADATA +2 -36
  39. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/RECORD +42 -40
  40. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/WHEEL +0 -0
  41. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/licenses/LICENSE +0 -0
  42. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/top_level.txt +0 -0
@@ -1,98 +1,201 @@
1
1
  import asyncio
2
+ from dataclasses import dataclass
2
3
 
3
4
  import numpy as np
4
- from scanspec.core import Path
5
+ from scanspec.core import Path, Slice
5
6
  from scanspec.specs import Spec
6
7
 
7
8
  from ophyd_async.core import (
8
9
  DEFAULT_TIMEOUT,
10
+ AsyncStatus,
9
11
  FlyerController,
12
+ error_if_none,
13
+ observe_value,
10
14
  set_and_wait_for_value,
11
15
  wait_for_value,
12
16
  )
13
17
  from ophyd_async.epics.motor import Motor
14
18
  from ophyd_async.epics.pmac import PmacIO
15
19
  from ophyd_async.epics.pmac._pmac_io import CS_LETTERS
20
+ from ophyd_async.epics.pmac._pmac_trajectory_generation import PVT, Trajectory
16
21
  from ophyd_async.epics.pmac._utils import (
17
22
  _PmacMotorInfo,
18
- _Trajectory,
19
23
  calculate_ramp_position_and_duration,
20
24
  )
21
25
 
26
+ # PMAC durations are in milliseconds
27
+ # We must convert from scanspec durations (seconds) to milliseconds
28
+ # PMAC motion program multiples durations by 0.001
29
+ # (see https://github.com/DiamondLightSource/pmac/blob/afe81f8bb9179c3a20eff351f30bc6cfd1539ad9/pmacApp/pmc/trajectory_scan_code_ppmac.pmc#L241)
30
+ # Therefore, we must divide scanspec durations by 10e-6
31
+ TICK_S = 0.000001
32
+ SLICE_SIZE = 4000
33
+
34
+
35
+ @dataclass
36
+ class PmacPrepareContext:
37
+ path: Path
38
+ motor_info: _PmacMotorInfo
39
+ ramp_up_time: float
40
+
22
41
 
23
42
  class PmacTrajectoryTriggerLogic(FlyerController):
24
43
  def __init__(self, pmac: PmacIO) -> None:
25
44
  self.pmac = pmac
26
- self.scantime: float | None = None
45
+ self._next_pvt: PVT | None
46
+ self._loaded: int = 0
47
+ self._trajectory_status: AsyncStatus | None = None
48
+ self._prepare_context: PmacPrepareContext | None = None
27
49
 
28
50
  async def prepare(self, value: Spec[Motor]):
29
- slice = Path(value.calculate()).consume()
30
- motor_info = await _PmacMotorInfo.from_motors(self.pmac, slice.axes())
51
+ path = Path(value.calculate())
52
+ slice = path.consume(SLICE_SIZE)
53
+ path_length = len(path)
54
+ motors = slice.axes()
55
+ motor_info = await _PmacMotorInfo.from_motors(self.pmac, motors)
31
56
  ramp_up_pos, ramp_up_time = calculate_ramp_position_and_duration(
32
57
  slice, motor_info, True
33
58
  )
34
- ramp_down_pos, ramp_down_time = calculate_ramp_position_and_duration(
35
- slice, motor_info, False
59
+ self._prepare_context = PmacPrepareContext(
60
+ path=path, motor_info=motor_info, ramp_up_time=ramp_up_time
36
61
  )
37
- trajectory = _Trajectory.from_slice(slice, ramp_up_time, motor_info)
38
62
  await asyncio.gather(
39
- self._build_trajectory(
40
- trajectory, motor_info, ramp_down_pos, ramp_down_time
41
- ),
63
+ self._build_trajectory(motor_info, slice, path_length, ramp_up_time),
42
64
  self._move_to_start(motor_info, ramp_up_pos),
43
65
  )
44
66
 
45
67
  async def kickoff(self):
46
- if not self.scantime:
47
- raise RuntimeError("Cannot kickoff. Must call prepare first.")
48
- self.status = await self.pmac.trajectory.execute_profile.set(
49
- True, timeout=self.scantime + 1
68
+ prepare_context = error_if_none(
69
+ self._prepare_context, "Cannot kickoff. Must call prepare first."
70
+ )
71
+ self._prepare_context = None
72
+ self._trajectory_status = self._execute_trajectory(
73
+ prepare_context.path, prepare_context.motor_info
74
+ )
75
+ # Wait for the ramp up to happen
76
+ await wait_for_value(
77
+ self.pmac.trajectory.total_points,
78
+ lambda v: v >= 1,
79
+ prepare_context.ramp_up_time + DEFAULT_TIMEOUT,
50
80
  )
51
81
 
52
82
  async def complete(self):
53
- if not self.scantime:
54
- raise RuntimeError("Cannot complete. Must call prepare first.")
55
- await wait_for_value(
56
- self.pmac.trajectory.execute_profile,
57
- False,
58
- timeout=self.scantime + DEFAULT_TIMEOUT,
83
+ trajectory_status = error_if_none(
84
+ self._trajectory_status, "Cannot complete. Must call kickoff first."
59
85
  )
86
+ await trajectory_status
87
+ # Reset trajectory status and number of loaded points
88
+ self._trajectory_status = None
89
+ self._loaded = 0
60
90
 
61
91
  async def stop(self):
62
- await self.pmac.trajectory.abort_profile.set(True)
92
+ await self.pmac.trajectory.abort_profile.trigger()
93
+
94
+ @AsyncStatus.wrap
95
+ async def _execute_trajectory(self, path: Path, motor_info: _PmacMotorInfo):
96
+ execute_status = self.pmac.trajectory.execute_profile.set(
97
+ True, wait=True, timeout=None
98
+ )
99
+ # We consume SLICE_SIZE from self.path and parse a trajectory
100
+ # containing at least 2 * SLICE_SIZE, as a gapless trajectory
101
+ # will contain 2 points per slice frame. If gaps are present,
102
+ # additional points are inserted, overfilling the buffer.
103
+ min_buffer_size = SLICE_SIZE * 2
104
+ async for current_point in observe_value(
105
+ self.pmac.trajectory.total_points,
106
+ done_status=execute_status,
107
+ timeout=DEFAULT_TIMEOUT,
108
+ ):
109
+ # Ensure we maintain a minimum buffer size, if we have more points to append
110
+ if len(path) != 0 and self._loaded - current_point < min_buffer_size:
111
+ # We have less than SLICE_SIZE * 2 points in the buffer, so refill
112
+ next_slice = path.consume(SLICE_SIZE)
113
+ path_length = len(path)
114
+ await self._append_trajectory(next_slice, path_length, motor_info)
115
+
116
+ async def _append_trajectory(
117
+ self, slice: Slice, path_length: int, motor_info: _PmacMotorInfo
118
+ ):
119
+ trajectory = await self._parse_trajectory(slice, path_length, motor_info)
120
+ await self._set_trajectory_arrays(trajectory, motor_info)
121
+ await self.pmac.trajectory.append_profile.trigger()
63
122
 
64
123
  async def _build_trajectory(
65
124
  self,
66
- trajectory: _Trajectory,
67
125
  motor_info: _PmacMotorInfo,
68
- ramp_down_pos: dict[Motor, np.float64],
69
- ramp_down_time: float,
126
+ slice: Slice,
127
+ path_length: int,
128
+ ramp_up_time: float,
70
129
  ):
71
- trajectory = trajectory.append_ramp_down(ramp_down_pos, ramp_down_time, 0)
72
- self.scantime = np.sum(trajectory.durations)
73
- use_axis = {axis + 1: False for axis in range(len(CS_LETTERS))}
74
-
75
- for motor, number in motor_info.motor_cs_index.items():
76
- use_axis[number + 1] = True
77
- await self.pmac.trajectory.positions[number + 1].set(
78
- trajectory.positions[motor]
79
- )
80
- await self.pmac.trajectory.velocities[number + 1].set(
81
- trajectory.velocities[motor]
82
- )
130
+ trajectory = await self._parse_trajectory(
131
+ slice, path_length, motor_info, ramp_up_time
132
+ )
133
+ use_axis = {
134
+ axis + 1: (axis in motor_info.motor_cs_index.values())
135
+ for axis in range(len(CS_LETTERS))
136
+ }
83
137
 
84
138
  coros = [
85
139
  self.pmac.trajectory.profile_cs_name.set(motor_info.cs_port),
86
- self.pmac.trajectory.time_array.set(trajectory.durations),
87
- self.pmac.trajectory.user_array.set(trajectory.user_programs),
88
- self.pmac.trajectory.points_to_build.set(len(trajectory.durations)),
89
140
  self.pmac.trajectory.calculate_velocities.set(False),
141
+ self._set_trajectory_arrays(trajectory, motor_info),
90
142
  ] + [
91
143
  self.pmac.trajectory.use_axis[number].set(use)
92
144
  for number, use in use_axis.items()
93
145
  ]
146
+
147
+ await asyncio.gather(*coros)
148
+ await self.pmac.trajectory.build_profile.trigger()
149
+
150
+ async def _parse_trajectory(
151
+ self,
152
+ slice: Slice,
153
+ path_length: int,
154
+ motor_info: _PmacMotorInfo,
155
+ ramp_up_time: float | None = None,
156
+ ) -> Trajectory:
157
+ trajectory, exit_pvt = Trajectory.from_slice(
158
+ slice,
159
+ motor_info,
160
+ None if ramp_up_time else self._next_pvt,
161
+ ramp_up_time=ramp_up_time,
162
+ )
163
+
164
+ if path_length == 0:
165
+ ramp_down_pos, ramp_down_time = calculate_ramp_position_and_duration(
166
+ slice, motor_info, False
167
+ )
168
+ trajectory = trajectory.with_ramp_down(
169
+ exit_pvt, ramp_down_pos, ramp_down_time, 0
170
+ )
171
+ self._next_pvt = exit_pvt
172
+ self._loaded += len(trajectory)
173
+
174
+ return trajectory
175
+
176
+ async def _set_trajectory_arrays(
177
+ self, trajectory: Trajectory, motor_info: _PmacMotorInfo
178
+ ):
179
+ coros = []
180
+ for motor, number in motor_info.motor_cs_index.items():
181
+ coros.append(
182
+ self.pmac.trajectory.positions[number + 1].set(
183
+ trajectory.positions[motor]
184
+ )
185
+ )
186
+ coros.append(
187
+ self.pmac.trajectory.velocities[number + 1].set(
188
+ trajectory.velocities[motor]
189
+ )
190
+ )
191
+ coros.extend(
192
+ [
193
+ self.pmac.trajectory.time_array.set(trajectory.durations / TICK_S),
194
+ self.pmac.trajectory.user_array.set(trajectory.user_programs),
195
+ self.pmac.trajectory.points_to_build.set(len(trajectory.durations)),
196
+ ]
197
+ )
94
198
  await asyncio.gather(*coros)
95
- await self.pmac.trajectory.build_profile.set(True)
96
199
 
97
200
  async def _move_to_start(
98
201
  self, motor_info: _PmacMotorInfo, ramp_up_position: dict[Motor, np.float64]