ophyd-async 0.10.0a4__py3-none-any.whl → 0.11__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 (37) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +12 -1
  3. ophyd_async/core/_derived_signal.py +69 -23
  4. ophyd_async/core/_derived_signal_backend.py +53 -29
  5. ophyd_async/core/_detector.py +3 -3
  6. ophyd_async/core/_device.py +24 -16
  7. ophyd_async/core/_flyer.py +35 -1
  8. ophyd_async/core/_hdf_dataset.py +12 -11
  9. ophyd_async/core/_providers.py +1 -1
  10. ophyd_async/core/_signal.py +49 -29
  11. ophyd_async/core/_signal_backend.py +1 -1
  12. ophyd_async/core/_table.py +3 -3
  13. ophyd_async/core/_utils.py +25 -0
  14. ophyd_async/core/_yaml_settings.py +3 -3
  15. ophyd_async/epics/adandor/__init__.py +7 -1
  16. ophyd_async/epics/adandor/_andor_controller.py +5 -8
  17. ophyd_async/epics/adandor/_andor_io.py +12 -19
  18. ophyd_async/epics/adcore/_core_logic.py +34 -10
  19. ophyd_async/epics/adcore/_hdf_writer.py +27 -19
  20. ophyd_async/epics/eiger/_odin_io.py +4 -2
  21. ophyd_async/epics/motor.py +46 -96
  22. ophyd_async/epics/pmac/__init__.py +3 -0
  23. ophyd_async/epics/pmac/_pmac_io.py +100 -0
  24. ophyd_async/fastcs/eiger/__init__.py +1 -2
  25. ophyd_async/fastcs/eiger/_eiger.py +3 -9
  26. ophyd_async/fastcs/panda/_trigger.py +4 -4
  27. ophyd_async/fastcs/panda/_writer.py +15 -13
  28. ophyd_async/sim/__init__.py +1 -2
  29. ophyd_async/sim/_blob_detector_writer.py +6 -12
  30. ophyd_async/sim/_mirror_horizontal.py +3 -2
  31. ophyd_async/sim/_mirror_vertical.py +1 -0
  32. ophyd_async/sim/_motor.py +13 -43
  33. {ophyd_async-0.10.0a4.dist-info → ophyd_async-0.11.dist-info}/METADATA +3 -3
  34. {ophyd_async-0.10.0a4.dist-info → ophyd_async-0.11.dist-info}/RECORD +37 -35
  35. {ophyd_async-0.10.0a4.dist-info → ophyd_async-0.11.dist-info}/WHEEL +1 -1
  36. {ophyd_async-0.10.0a4.dist-info → ophyd_async-0.11.dist-info}/licenses/LICENSE +0 -0
  37. {ophyd_async-0.10.0a4.dist-info → ophyd_async-0.11.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,6 @@ from bluesky.protocols import (
14
14
  Stoppable,
15
15
  Subscribable,
16
16
  )
17
- from pydantic import BaseModel, Field
18
17
 
19
18
  from ophyd_async.core import (
20
19
  CALCULATE_TIMEOUT,
@@ -22,16 +21,18 @@ from ophyd_async.core import (
22
21
  AsyncStatus,
23
22
  CalculatableTimeout,
24
23
  Callback,
24
+ FlyMotorInfo,
25
25
  StandardReadable,
26
26
  StrictEnum,
27
27
  WatchableAsyncStatus,
28
28
  WatcherUpdate,
29
+ error_if_none,
29
30
  observe_value,
30
31
  )
31
32
  from ophyd_async.core import StandardReadableFormat as Format
32
33
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
33
34
 
34
- __all__ = ["MotorLimitsException", "FlyMotorInfo", "Motor"]
35
+ __all__ = ["MotorLimitsException", "Motor"]
35
36
 
36
37
 
37
38
  class MotorLimitsException(Exception):
@@ -40,26 +41,6 @@ class MotorLimitsException(Exception):
40
41
  pass
41
42
 
42
43
 
43
- class FlyMotorInfo(BaseModel):
44
- """Minimal set of information required to fly a motor."""
45
-
46
- start_position: float = Field(frozen=True)
47
- """Absolute position of the motor once it finishes accelerating to desired
48
- velocity, in motor EGUs"""
49
-
50
- end_position: float = Field(frozen=True)
51
- """Absolute position of the motor once it begins decelerating from desired
52
- velocity, in EGUs"""
53
-
54
- time_for_move: float = Field(frozen=True, gt=0)
55
- """Time taken for the motor to get from start_position to end_position, excluding
56
- run-up and run-down, in seconds."""
57
-
58
- timeout: CalculatableTimeout = Field(frozen=True, default=CALCULATE_TIMEOUT)
59
- """Maximum time for the complete motor move, including run up and run down.
60
- Defaults to `time_for_move` + run up and run down times + 10s."""
61
-
62
-
63
44
  class OffsetMode(StrictEnum):
64
45
  VARIABLE = "Variable"
65
46
  FROZEN = "Frozen"
@@ -111,15 +92,12 @@ class Motor(
111
92
  # Whether set() should complete successfully or not
112
93
  self._set_success = True
113
94
 
114
- # end_position of a fly move, with run_up_distance added on.
115
- self._fly_completed_position: float | None = None
95
+ # Currently requested fly info, stored in prepare
96
+ self._fly_info: FlyMotorInfo | None = None
116
97
 
117
98
  # Set on kickoff(), complete when motor reaches self._fly_completed_position
118
99
  self._fly_status: WatchableAsyncStatus | None = None
119
100
 
120
- # Set during prepare
121
- self._fly_timeout: CalculatableTimeout | None = CALCULATE_TIMEOUT
122
-
123
101
  super().__init__(name=name)
124
102
 
125
103
  def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
@@ -131,41 +109,64 @@ class Motor(
131
109
  @AsyncStatus.wrap
132
110
  async def prepare(self, value: FlyMotorInfo):
133
111
  """Move to the beginning of a suitable run-up distance ready for a flyscan."""
134
- self._fly_timeout = value.timeout
112
+ self._fly_info = value
135
113
 
136
114
  # Velocity, at which motor travels from start_position to end_position, in motor
137
115
  # egu/s.
138
- fly_velocity = await self._prepare_velocity(
139
- value.start_position,
140
- value.end_position,
141
- value.time_for_move,
116
+ max_speed, egu = await asyncio.gather(
117
+ self.max_velocity.get_value(), self.motor_egu.get_value()
142
118
  )
119
+ if abs(value.velocity) > max_speed:
120
+ raise MotorLimitsException(
121
+ f"Velocity {abs(value.velocity)} {egu}/s was requested for a motor "
122
+ f" with max speed of {max_speed} {egu}/s"
123
+ )
124
+
125
+ acceleration_time = await self.acceleration_time.get_value()
126
+ ramp_up_start_pos = value.ramp_up_start_pos(acceleration_time)
127
+ ramp_down_end_pos = value.ramp_down_end_pos(acceleration_time)
143
128
 
144
- # start_position with run_up_distance added on.
145
- fly_prepared_position = await self._prepare_motor_path(
146
- abs(fly_velocity), value.start_position, value.end_position
129
+ motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
130
+ self.low_limit_travel.get_value(),
131
+ self.high_limit_travel.get_value(),
132
+ self.motor_egu.get_value(),
147
133
  )
148
134
 
149
- await self.set(fly_prepared_position)
150
- await self.velocity.set(abs(fly_velocity))
135
+ if (
136
+ not motor_upper_limit >= ramp_up_start_pos >= motor_lower_limit
137
+ or not motor_upper_limit >= ramp_down_end_pos >= motor_lower_limit
138
+ ):
139
+ raise MotorLimitsException(
140
+ f"Motor trajectory for requested fly is from "
141
+ f"{ramp_up_start_pos}{egu} to "
142
+ f"{ramp_down_end_pos}{egu} but motor limits are "
143
+ f"{motor_lower_limit}{egu} <= x <= {motor_upper_limit}{egu} "
144
+ )
145
+
146
+ # move to prepare position at maximum velocity
147
+ await self.velocity.set(abs(max_speed))
148
+ await self.set(ramp_up_start_pos)
149
+
150
+ # Set velocity we will be using for the flyscan
151
+ await self.velocity.set(abs(value.velocity))
151
152
 
152
153
  @AsyncStatus.wrap
153
154
  async def kickoff(self):
154
155
  """Begin moving motor from prepared position to final position."""
155
- if not self._fly_completed_position:
156
- msg = "Motor must be prepared before attempting to kickoff"
157
- raise RuntimeError(msg)
156
+ fly_info = error_if_none(
157
+ self._fly_info, "Motor must be prepared before attempting to kickoff"
158
+ )
158
159
 
160
+ acceleration_time = await self.acceleration_time.get_value()
159
161
  self._fly_status = self.set(
160
- self._fly_completed_position, timeout=self._fly_timeout
162
+ fly_info.ramp_down_end_pos(acceleration_time),
163
+ timeout=fly_info.timeout,
161
164
  )
162
165
 
163
166
  def complete(self) -> WatchableAsyncStatus:
164
167
  """Mark as complete once motor reaches completed position."""
165
- if not self._fly_status:
166
- msg = "kickoff not called"
167
- raise RuntimeError(msg)
168
- return self._fly_status
168
+ fly_status = error_if_none(self._fly_status, "kickoff not called")
169
+ return fly_status
169
170
 
170
171
  @WatchableAsyncStatus.wrap
171
172
  async def set( # type: ignore
@@ -220,22 +221,6 @@ class Motor(
220
221
  # the move above, so need to pass wait=False
221
222
  await self.motor_stop.set(1, wait=False)
222
223
 
223
- async def _prepare_velocity(
224
- self, start_position: float, end_position: float, time_for_move: float
225
- ) -> float:
226
- fly_velocity = (end_position - start_position) / time_for_move
227
- max_speed, egu = await asyncio.gather(
228
- self.max_velocity.get_value(), self.motor_egu.get_value()
229
- )
230
- if abs(fly_velocity) > max_speed:
231
- raise MotorLimitsException(
232
- f"Motor speed of {abs(fly_velocity)} {egu}/s was requested for a motor "
233
- f" with max speed of {max_speed} {egu}/s"
234
- )
235
- # move to prepare position at maximum velocity
236
- await self.velocity.set(abs(max_speed))
237
- return fly_velocity
238
-
239
224
  async def locate(self) -> Location[float]:
240
225
  """Return the current setpoint and readback of the motor."""
241
226
  setpoint, readback = await asyncio.gather(
@@ -250,38 +235,3 @@ class Motor(
250
235
  def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
251
236
  """Unsubscribe."""
252
237
  self.user_readback.clear_sub(function)
253
-
254
- async def _prepare_motor_path(
255
- self, fly_velocity: float, start_position: float, end_position: float
256
- ) -> float:
257
- # Distance required for motor to accelerate from stationary to fly_velocity, and
258
- # distance required for motor to decelerate from fly_velocity to stationary
259
- run_up_distance = (
260
- (await self.acceleration_time.get_value()) * fly_velocity * 0.5
261
- )
262
-
263
- direction = 1 if end_position > start_position else -1
264
- self._fly_completed_position = end_position + (run_up_distance * direction)
265
-
266
- # Prepared position not used after prepare, so no need to store in self
267
- fly_prepared_position = start_position - (run_up_distance * direction)
268
-
269
- motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
270
- self.low_limit_travel.get_value(),
271
- self.high_limit_travel.get_value(),
272
- self.motor_egu.get_value(),
273
- )
274
-
275
- if (
276
- not motor_upper_limit >= fly_prepared_position >= motor_lower_limit
277
- or not motor_upper_limit
278
- >= self._fly_completed_position
279
- >= motor_lower_limit
280
- ):
281
- raise MotorLimitsException(
282
- f"Motor trajectory for requested fly is from "
283
- f"{fly_prepared_position}{egu} to "
284
- f"{self._fly_completed_position}{egu} but motor limits are "
285
- f"{motor_lower_limit}{egu} <= x <= {motor_upper_limit}{egu} "
286
- )
287
- return fly_prepared_position
@@ -0,0 +1,3 @@
1
+ from ._pmac_io import PmacAxisIO, PmacCoordIO, PmacIO, PmacTrajectoryIO
2
+
3
+ __all__ = ["PmacAxisIO", "PmacCoordIO", "PmacIO", "PmacTrajectoryIO"]
@@ -0,0 +1,100 @@
1
+ from collections.abc import Sequence
2
+
3
+ import numpy as np
4
+
5
+ from ophyd_async.core import Array1D, Device, DeviceVector, StandardReadable
6
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
7
+
8
+ CS_LETTERS = "ABCUVWXYZ"
9
+
10
+
11
+ class PmacTrajectoryIO(StandardReadable):
12
+ """Device that moves a PMAC Motor record."""
13
+
14
+ def __init__(self, prefix: str, name: str = "") -> None:
15
+ self.time_array = epics_signal_rw(
16
+ Array1D[np.float64], prefix + ":ProfileTimeArray"
17
+ )
18
+ self.user_array = epics_signal_rw(Array1D[np.int32], prefix + ":UserArray")
19
+ # 1 indexed CS axes so we can index into them from the compound motor input link
20
+ self.positions = DeviceVector(
21
+ {
22
+ i + 1: epics_signal_rw(
23
+ Array1D[np.float64], f"{prefix}:{letter}:Positions"
24
+ )
25
+ for i, letter in enumerate(CS_LETTERS)
26
+ }
27
+ )
28
+ self.use_axis = DeviceVector(
29
+ {
30
+ i + 1: epics_signal_rw(bool, f"{prefix}:{letter}:UseAxis")
31
+ for i, letter in enumerate(CS_LETTERS)
32
+ }
33
+ )
34
+ self.velocities = DeviceVector(
35
+ {
36
+ i + 1: epics_signal_rw(
37
+ Array1D[np.float64], f"{prefix}:{letter}:Velocities"
38
+ )
39
+ for i, letter in enumerate(CS_LETTERS)
40
+ }
41
+ )
42
+ self.points_to_build = epics_signal_rw(int, prefix + ":ProfilePointsToBuild")
43
+ self.build_profile = epics_signal_rw(bool, prefix + ":ProfileBuild")
44
+ self.execute_profile = epics_signal_rw(bool, prefix + ":ProfileExecute")
45
+ self.scan_percent = epics_signal_r(float, prefix + ":TscanPercent_RBV")
46
+ self.abort_profile = epics_signal_rw(bool, prefix + ":ProfileAbort")
47
+ self.profile_cs_name = epics_signal_rw(str, prefix + ":ProfileCsName")
48
+ self.calculate_velocities = epics_signal_rw(bool, prefix + ":ProfileCalcVel")
49
+
50
+ super().__init__(name=name)
51
+
52
+
53
+ class PmacAxisIO(Device):
54
+ """A Device that (direct) moves a PMAC Coordinate System Motor.
55
+
56
+ Note that this does not go through a motor record.
57
+ """
58
+
59
+ def __init__(self, prefix: str, name: str = "") -> None:
60
+ self.cs_axis_letter = epics_signal_r(str, f"{prefix}:CsAxis_RBV")
61
+ self.cs_port = epics_signal_r(str, f"{prefix}:CsPort_RBV")
62
+ super().__init__(name=name)
63
+
64
+
65
+ class PmacCoordIO(Device):
66
+ """A Device that represents a Pmac Coordinate System."""
67
+
68
+ def __init__(self, prefix: str, name: str = "") -> None:
69
+ self.defer_moves = epics_signal_r(bool, f"{prefix}:DeferMoves")
70
+ self.cs_axis_setpoint = DeviceVector(
71
+ {
72
+ i + 1: epics_signal_rw(
73
+ Array1D[np.float64], f"{prefix}:M{i + 1}:DirectDemand"
74
+ )
75
+ for i in range(len(CS_LETTERS))
76
+ }
77
+ )
78
+ super().__init__(name=name)
79
+
80
+
81
+ class PmacIO(Device):
82
+ """Device that represents a pmac controller."""
83
+
84
+ def __init__(
85
+ self,
86
+ prefix: str,
87
+ axis_nums: Sequence[int],
88
+ coord_nums: Sequence[int],
89
+ name: str = "",
90
+ ) -> None:
91
+ self.axis = DeviceVector(
92
+ {axis: PmacAxisIO(f"{prefix}:M{axis}") for axis in axis_nums}
93
+ )
94
+ self.coord = DeviceVector(
95
+ {coord: PmacCoordIO(f"{prefix}:CS{coord}") for coord in coord_nums}
96
+ )
97
+ # Trajectory PVs have the same prefix as the pmac device
98
+ self.trajectory = PmacTrajectoryIO(prefix)
99
+
100
+ super().__init__(name=name)
@@ -1,4 +1,4 @@
1
- from ._eiger import EigerDetector, EigerTriggerInfo
1
+ from ._eiger import EigerDetector
2
2
  from ._eiger_controller import EigerController
3
3
  from ._eiger_io import EigerDetectorIO, EigerDriverIO, EigerMonitorIO, EigerStreamIO
4
4
 
@@ -6,7 +6,6 @@ __all__ = [
6
6
  "EigerDetector",
7
7
  "EigerController",
8
8
  "EigerDriverIO",
9
- "EigerTriggerInfo",
10
9
  "EigerDetectorIO",
11
10
  "EigerMonitorIO",
12
11
  "EigerStreamIO",
@@ -1,5 +1,3 @@
1
- from pydantic import Field
2
-
3
1
  from ophyd_async.core import (
4
2
  AsyncStatus,
5
3
  PathProvider,
@@ -12,10 +10,6 @@ from ._eiger_controller import EigerController
12
10
  from ._eiger_io import EigerDriverIO
13
11
 
14
12
 
15
- class EigerTriggerInfo(TriggerInfo):
16
- energy_ev: float = Field(gt=0)
17
-
18
-
19
13
  class EigerDetector(StandardDetector):
20
14
  """Ophyd-async implementation of an Eiger Detector."""
21
15
 
@@ -28,10 +22,11 @@ class EigerDetector(StandardDetector):
28
22
  path_provider: PathProvider,
29
23
  drv_suffix="-EA-EIGER-01:",
30
24
  hdf_suffix="-EA-EIGER-01:OD:",
25
+ odin_nodes: int = 4,
31
26
  name="",
32
27
  ):
33
28
  self.drv = EigerDriverIO(prefix + drv_suffix)
34
- self.odin = Odin(prefix + hdf_suffix)
29
+ self.odin = Odin(prefix + hdf_suffix, nodes=odin_nodes)
35
30
 
36
31
  super().__init__(
37
32
  EigerController(self.drv),
@@ -44,6 +39,5 @@ class EigerDetector(StandardDetector):
44
39
  )
45
40
 
46
41
  @AsyncStatus.wrap
47
- async def prepare(self, value: EigerTriggerInfo) -> None: # type: ignore
48
- await self._controller.set_energy(value.energy_ev)
42
+ async def prepare(self, value: TriggerInfo) -> None:
49
43
  await super().prepare(value)
@@ -1,8 +1,8 @@
1
1
  import asyncio
2
2
 
3
- from pydantic import BaseModel, Field
3
+ from pydantic import Field
4
4
 
5
- from ophyd_async.core import FlyerController, wait_for_value
5
+ from ophyd_async.core import ConfinedModel, FlyerController, wait_for_value
6
6
 
7
7
  from ._block import (
8
8
  PandaBitMux,
@@ -14,7 +14,7 @@ from ._block import (
14
14
  from ._table import SeqTable
15
15
 
16
16
 
17
- class SeqTableInfo(BaseModel):
17
+ class SeqTableInfo(ConfinedModel):
18
18
  """Info for the PandA `SeqTable` for flyscanning."""
19
19
 
20
20
  sequence_table: SeqTable = Field(strict=True)
@@ -51,7 +51,7 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
51
51
  await wait_for_value(self.seq.active, False, timeout=1)
52
52
 
53
53
 
54
- class PcompInfo(BaseModel):
54
+ class PcompInfo(ConfinedModel):
55
55
  """Info for the PandA `PcompBlock` for flyscanning."""
56
56
 
57
57
  start_postion: int = Field(description="start position in counts")
@@ -33,12 +33,12 @@ class PandaHDFWriter(DetectorWriter):
33
33
 
34
34
  # Triggered on PCAP arm
35
35
  async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]:
36
+ self._composer = None
36
37
  """Retrieve and get descriptor of all PandA signals marked for capture."""
37
38
  self._exposures_per_event = exposures_per_event
38
39
  # Ensure flushes are immediate
39
40
  await self.panda_data_block.flush_period.set(0)
40
41
 
41
- self._composer = None
42
42
  info = self._path_provider(device_name=name)
43
43
 
44
44
  # Set create dir depth first to guarantee that callback when setting
@@ -64,7 +64,15 @@ class PandaHDFWriter(DetectorWriter):
64
64
  # Wait for it to start, stashing the status that tells us when it finishes
65
65
  await self.panda_data_block.capture.set(True)
66
66
 
67
- return await self._describe(name)
67
+ describe = await self._describe(name)
68
+
69
+ self._composer = HDFDocumentComposer(
70
+ Path(await self.panda_data_block.hdf_directory.get_value())
71
+ / Path(await self.panda_data_block.hdf_file_name.get_value()),
72
+ self._datasets,
73
+ )
74
+
75
+ return describe
68
76
 
69
77
  async def _describe(self, name: str) -> dict[str, DataKey]:
70
78
  """Return a describe based on the datasets PV."""
@@ -145,17 +153,11 @@ class PandaHDFWriter(DetectorWriter):
145
153
  self, name: str, indices_written: int
146
154
  ) -> AsyncIterator[StreamAsset]:
147
155
  # TODO: fail if we get dropped frames
148
- if indices_written:
149
- if not self._composer:
150
- self._composer = HDFDocumentComposer(
151
- Path(await self.panda_data_block.hdf_directory.get_value())
152
- / Path(await self.panda_data_block.hdf_file_name.get_value()),
153
- self._datasets,
154
- )
155
- for doc in self._composer.stream_resources():
156
- yield "stream_resource", doc
157
- for doc in self._composer.stream_data(indices_written):
158
- yield "stream_datum", doc
156
+ if self._composer is None:
157
+ msg = f"open() not called on {self}"
158
+ raise RuntimeError(msg)
159
+ for doc in self._composer.make_stream_docs(indices_written):
160
+ yield doc
159
161
 
160
162
  # Could put this function as default for StandardDetector
161
163
  async def close(self):
@@ -8,14 +8,13 @@ from ._mirror_vertical import (
8
8
  TwoJackTransform,
9
9
  VerticalMirror,
10
10
  )
11
- from ._motor import FlySimMotorInfo, SimMotor
11
+ from ._motor import SimMotor
12
12
  from ._pattern_generator import PatternGenerator
13
13
  from ._point_detector import SimPointDetector
14
14
  from ._stage import SimStage
15
15
 
16
16
  __all__ = [
17
17
  "SimMotor",
18
- "FlySimMotorInfo",
19
18
  "SimStage",
20
19
  "PatternGenerator",
21
20
  "SimPointDetector",
@@ -52,7 +52,7 @@ class BlobDetectorWriter(DetectorWriter):
52
52
  chunk_shape=(1024,),
53
53
  ),
54
54
  ]
55
- self.composer = None
55
+ self.composer = HDFDocumentComposer(self.path, self.datasets)
56
56
  describe = {
57
57
  ds.data_key: DataKey(
58
58
  source="sim://pattern-generator-hdf-file",
@@ -85,17 +85,11 @@ class BlobDetectorWriter(DetectorWriter):
85
85
  self, name: str, indices_written: int
86
86
  ) -> AsyncIterator[StreamAsset]:
87
87
  # When we have written something to the file
88
- if indices_written:
89
- # Only emit stream resource the first time we see frames in
90
- # the file
91
- if not self.composer:
92
- if not self.path:
93
- raise RuntimeError(f"open() not called on {self}")
94
- self.composer = HDFDocumentComposer(self.path, self.datasets)
95
- for doc in self.composer.stream_resources():
96
- yield "stream_resource", doc
97
- for doc in self.composer.stream_data(indices_written):
98
- yield "stream_datum", doc
88
+ if self.composer is None:
89
+ msg = f"open() not called on {self}"
90
+ raise RuntimeError(msg)
91
+ for doc in self.composer.make_stream_docs(indices_written):
92
+ yield doc
99
93
 
100
94
  async def close(self) -> None:
101
95
  self.pattern_generator.close_file()
@@ -3,7 +3,7 @@ from typing import TypedDict
3
3
 
4
4
  from bluesky.protocols import Movable
5
5
 
6
- from ophyd_async.core import AsyncStatus, DerivedSignalFactory, Device, soft_signal_rw
6
+ from ophyd_async.core import AsyncStatus, DerivedSignalFactory, Device
7
7
 
8
8
  from ._mirror_vertical import TwoJackDerived, TwoJackTransform
9
9
  from ._motor import SimMotor
@@ -20,7 +20,8 @@ class HorizontalMirror(Device, Movable):
20
20
  self.x1 = SimMotor()
21
21
  self.x2 = SimMotor()
22
22
  # Parameter
23
- self.x1_x2_distance = soft_signal_rw(float, initial_value=1)
23
+ # This could also be set as 'soft_signal_rw(float, initial_value=1)'
24
+ self.x1_x2_distance = 1.0
24
25
  # Derived signals
25
26
  self._factory = DerivedSignalFactory(
26
27
  TwoJackTransform,
@@ -51,6 +51,7 @@ class VerticalMirror(Device, Movable[TwoJackDerived]):
51
51
  self.y1 = SimMotor()
52
52
  self.y2 = SimMotor()
53
53
  # Parameter
54
+ # This could also be set as '1.0', if constant.
54
55
  self.y1_y2_distance = soft_signal_rw(float, initial_value=1)
55
56
  # Derived signals
56
57
  self._factory = DerivedSignalFactory(
ophyd_async/sim/_motor.py CHANGED
@@ -4,14 +4,15 @@ import time
4
4
 
5
5
  import numpy as np
6
6
  from bluesky.protocols import Locatable, Location, Reading, Stoppable, Subscribable
7
- from pydantic import BaseModel, ConfigDict, Field
8
7
 
9
8
  from ophyd_async.core import (
10
9
  AsyncStatus,
11
10
  Callback,
11
+ FlyMotorInfo,
12
12
  StandardReadable,
13
13
  WatchableAsyncStatus,
14
14
  WatcherUpdate,
15
+ error_if_none,
15
16
  observe_value,
16
17
  soft_signal_r_and_setter,
17
18
  soft_signal_rw,
@@ -19,37 +20,6 @@ from ophyd_async.core import (
19
20
  from ophyd_async.core import StandardReadableFormat as Format
20
21
 
21
22
 
22
- class FlySimMotorInfo(BaseModel):
23
- """Minimal set of information required to fly a [](#SimMotor)."""
24
-
25
- model_config = ConfigDict(frozen=True)
26
-
27
- cv_start: float
28
- """Absolute position of the motor once it finishes accelerating to desired
29
- velocity, in motor EGUs"""
30
-
31
- cv_end: float
32
- """Absolute position of the motor once it begins decelerating from desired
33
- velocity, in EGUs"""
34
-
35
- cv_time: float = Field(gt=0)
36
- """Time taken for the motor to get from start_position to end_position, excluding
37
- run-up and run-down, in seconds."""
38
-
39
- @property
40
- def velocity(self) -> float:
41
- """Calculate the velocity of the constant velocity phase."""
42
- return (self.cv_end - self.cv_start) / self.cv_time
43
-
44
- def start_position(self, acceleration_time: float) -> float:
45
- """Calculate the start position with run-up distance added on."""
46
- return self.cv_start - acceleration_time * self.velocity / 2
47
-
48
- def end_position(self, acceleration_time: float) -> float:
49
- """Calculate the end position with run-down distance added on."""
50
- return self.cv_end + acceleration_time * self.velocity / 2
51
-
52
-
53
23
  class SimMotor(StandardReadable, Stoppable, Subscribable[float], Locatable[float]):
54
24
  """For usage when simulating a motor."""
55
25
 
@@ -74,7 +44,7 @@ class SimMotor(StandardReadable, Stoppable, Subscribable[float], Locatable[float
74
44
  self._set_success = True
75
45
  self._move_status: AsyncStatus | None = None
76
46
  # Stored in prepare
77
- self._fly_info: FlySimMotorInfo | None = None
47
+ self._fly_info: FlyMotorInfo | None = None
78
48
  # Set on kickoff(), complete when motor reaches end position
79
49
  self._fly_status: WatchableAsyncStatus | None = None
80
50
 
@@ -86,12 +56,14 @@ class SimMotor(StandardReadable, Stoppable, Subscribable[float], Locatable[float
86
56
  self.user_readback.set_name(name)
87
57
 
88
58
  @AsyncStatus.wrap
89
- async def prepare(self, value: FlySimMotorInfo):
59
+ async def prepare(self, value: FlyMotorInfo):
90
60
  """Calculate run-up and move there, setting fly velocity when there."""
91
61
  self._fly_info = value
92
62
  # Move to start as fast as we can
93
63
  await self.velocity.set(0)
94
- await self.set(value.start_position(await self.acceleration_time.get_value()))
64
+ await self.set(
65
+ value.ramp_up_start_pos(await self.acceleration_time.get_value())
66
+ )
95
67
  # Set the velocity for the actual move
96
68
  await self.velocity.set(value.velocity)
97
69
 
@@ -111,20 +83,18 @@ class SimMotor(StandardReadable, Stoppable, Subscribable[float], Locatable[float
111
83
  @AsyncStatus.wrap
112
84
  async def kickoff(self):
113
85
  """Begin moving motor from prepared position to final position."""
114
- if not self._fly_info:
115
- msg = "Motor must be prepared before attempting to kickoff"
116
- raise RuntimeError(msg)
86
+ fly_info = error_if_none(
87
+ self._fly_info, "Motor must be prepared before attempting to kickoff"
88
+ )
117
89
  acceleration_time = await self.acceleration_time.get_value()
118
- self._fly_status = self.set(self._fly_info.end_position(acceleration_time))
90
+ self._fly_status = self.set(fly_info.ramp_down_end_pos(acceleration_time))
119
91
  # Wait for the acceleration time to ensure we are at velocity
120
92
  await asyncio.sleep(acceleration_time)
121
93
 
122
94
  def complete(self) -> WatchableAsyncStatus:
123
95
  """Mark as complete once motor reaches completed position."""
124
- if not self._fly_status:
125
- msg = "kickoff not called"
126
- raise RuntimeError(msg)
127
- return self._fly_status
96
+ fly_status = error_if_none(self._fly_status, "kickoff not called")
97
+ return fly_status
128
98
 
129
99
  async def _move(self, old_position: float, new_position: float, velocity: float):
130
100
  if old_position == new_position:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ophyd-async
3
- Version: 0.10.0a4
3
+ Version: 0.11
4
4
  Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
5
5
  Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
6
6
  License: BSD 3-Clause License
@@ -43,7 +43,7 @@ Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
44
  Requires-Dist: numpy
45
45
  Requires-Dist: bluesky>=1.13.1rc2
46
- Requires-Dist: event-model>=1.22.1
46
+ Requires-Dist: event-model>=1.23
47
47
  Requires-Dist: pyyaml
48
48
  Requires-Dist: colorlog
49
49
  Requires-Dist: pydantic>=2.0
@@ -70,7 +70,7 @@ Requires-Dist: inflection; extra == "dev"
70
70
  Requires-Dist: import-linter; extra == "dev"
71
71
  Requires-Dist: myst-parser; extra == "dev"
72
72
  Requires-Dist: numpydoc; extra == "dev"
73
- Requires-Dist: ophyd; extra == "dev"
73
+ Requires-Dist: ophyd>=1.10.7; extra == "dev"
74
74
  Requires-Dist: pickleshare; extra == "dev"
75
75
  Requires-Dist: pipdeptree; extra == "dev"
76
76
  Requires-Dist: pre-commit; extra == "dev"