ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__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 (157) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +102 -74
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +158 -153
  8. ophyd_async/core/_device.py +143 -115
  9. ophyd_async/core/_device_filler.py +82 -9
  10. ophyd_async/core/_flyer.py +16 -7
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +74 -58
  17. ophyd_async/core/_settings.py +113 -0
  18. ophyd_async/core/_signal.py +304 -174
  19. ophyd_async/core/_signal_backend.py +60 -14
  20. ophyd_async/core/_soft_signal_backend.py +18 -12
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +54 -17
  23. ophyd_async/core/_utils.py +101 -52
  24. ophyd_async/core/_yaml_settings.py +66 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/__init__.py +9 -0
  27. ophyd_async/epics/adandor/_andor.py +45 -0
  28. ophyd_async/epics/adandor/_andor_controller.py +51 -0
  29. ophyd_async/epics/adandor/_andor_io.py +34 -0
  30. ophyd_async/epics/adaravis/__init__.py +8 -1
  31. ophyd_async/epics/adaravis/_aravis.py +23 -41
  32. ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
  33. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  34. ophyd_async/epics/adcore/__init__.py +36 -14
  35. ophyd_async/epics/adcore/_core_detector.py +81 -0
  36. ophyd_async/epics/adcore/_core_io.py +145 -95
  37. ophyd_async/epics/adcore/_core_logic.py +179 -88
  38. ophyd_async/epics/adcore/_core_writer.py +223 -0
  39. ophyd_async/epics/adcore/_hdf_writer.py +51 -92
  40. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  41. ophyd_async/epics/adcore/_single_trigger.py +6 -5
  42. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  43. ophyd_async/epics/adcore/_utils.py +3 -2
  44. ophyd_async/epics/adkinetix/__init__.py +2 -1
  45. ophyd_async/epics/adkinetix/_kinetix.py +32 -27
  46. ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
  47. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  48. ophyd_async/epics/adpilatus/__init__.py +7 -2
  49. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  50. ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
  51. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  52. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  53. ophyd_async/epics/adsimdetector/_sim.py +22 -16
  54. ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
  55. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  56. ophyd_async/epics/advimba/__init__.py +10 -1
  57. ophyd_async/epics/advimba/_vimba.py +26 -25
  58. ophyd_async/epics/advimba/_vimba_controller.py +12 -24
  59. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  60. ophyd_async/epics/core/_aioca.py +66 -30
  61. ophyd_async/epics/core/_epics_connector.py +4 -0
  62. ophyd_async/epics/core/_epics_device.py +2 -0
  63. ophyd_async/epics/core/_p4p.py +50 -18
  64. ophyd_async/epics/core/_pvi_connector.py +65 -8
  65. ophyd_async/epics/core/_signal.py +51 -51
  66. ophyd_async/epics/core/_util.py +5 -5
  67. ophyd_async/epics/demo/__init__.py +11 -49
  68. ophyd_async/epics/demo/__main__.py +31 -0
  69. ophyd_async/epics/demo/_ioc.py +32 -0
  70. ophyd_async/epics/demo/_motor.py +82 -0
  71. ophyd_async/epics/demo/_point_detector.py +42 -0
  72. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  73. ophyd_async/epics/demo/_stage.py +15 -0
  74. ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
  75. ophyd_async/epics/demo/point_detector.db +59 -0
  76. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  77. ophyd_async/epics/eiger/_eiger.py +1 -3
  78. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  79. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  80. ophyd_async/epics/eiger/_odin_io.py +1 -2
  81. ophyd_async/epics/motor.py +83 -38
  82. ophyd_async/epics/signal.py +4 -1
  83. ophyd_async/epics/testing/__init__.py +14 -14
  84. ophyd_async/epics/testing/_example_ioc.py +68 -73
  85. ophyd_async/epics/testing/_utils.py +19 -44
  86. ophyd_async/epics/testing/test_records.db +16 -0
  87. ophyd_async/epics/testing/test_records_pva.db +17 -16
  88. ophyd_async/fastcs/__init__.py +1 -0
  89. ophyd_async/fastcs/core.py +6 -0
  90. ophyd_async/fastcs/odin/__init__.py +1 -0
  91. ophyd_async/fastcs/panda/__init__.py +8 -8
  92. ophyd_async/fastcs/panda/_block.py +29 -9
  93. ophyd_async/fastcs/panda/_control.py +12 -2
  94. ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
  95. ophyd_async/fastcs/panda/_table.py +13 -7
  96. ophyd_async/fastcs/panda/_trigger.py +23 -9
  97. ophyd_async/fastcs/panda/_writer.py +27 -30
  98. ophyd_async/plan_stubs/__init__.py +16 -0
  99. ophyd_async/plan_stubs/_ensure_connected.py +12 -17
  100. ophyd_async/plan_stubs/_fly.py +3 -5
  101. ophyd_async/plan_stubs/_nd_attributes.py +9 -5
  102. ophyd_async/plan_stubs/_panda.py +14 -0
  103. ophyd_async/plan_stubs/_settings.py +152 -0
  104. ophyd_async/plan_stubs/_utils.py +3 -0
  105. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  106. ophyd_async/sim/__init__.py +29 -0
  107. ophyd_async/sim/__main__.py +43 -0
  108. ophyd_async/sim/_blob_detector.py +33 -0
  109. ophyd_async/sim/_blob_detector_controller.py +48 -0
  110. ophyd_async/sim/_blob_detector_writer.py +105 -0
  111. ophyd_async/sim/_mirror_horizontal.py +46 -0
  112. ophyd_async/sim/_mirror_vertical.py +74 -0
  113. ophyd_async/sim/_motor.py +233 -0
  114. ophyd_async/sim/_pattern_generator.py +124 -0
  115. ophyd_async/sim/_point_detector.py +86 -0
  116. ophyd_async/sim/_stage.py +19 -0
  117. ophyd_async/tango/__init__.py +1 -0
  118. ophyd_async/tango/core/__init__.py +6 -1
  119. ophyd_async/tango/core/_base_device.py +41 -33
  120. ophyd_async/tango/core/_converters.py +81 -0
  121. ophyd_async/tango/core/_signal.py +21 -33
  122. ophyd_async/tango/core/_tango_readable.py +2 -19
  123. ophyd_async/tango/core/_tango_transport.py +148 -74
  124. ophyd_async/tango/core/_utils.py +47 -0
  125. ophyd_async/tango/demo/_counter.py +2 -0
  126. ophyd_async/tango/demo/_detector.py +2 -0
  127. ophyd_async/tango/demo/_mover.py +10 -6
  128. ophyd_async/tango/demo/_tango/_servers.py +4 -0
  129. ophyd_async/tango/testing/__init__.py +6 -0
  130. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  131. ophyd_async/testing/__init__.py +48 -7
  132. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  133. ophyd_async/testing/_assert.py +200 -96
  134. ophyd_async/testing/_mock_signal_utils.py +59 -73
  135. ophyd_async/testing/_one_of_everything.py +146 -0
  136. ophyd_async/testing/_single_derived.py +87 -0
  137. ophyd_async/testing/_utils.py +3 -0
  138. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  139. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  140. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
  141. ophyd_async/core/_device_save_loader.py +0 -274
  142. ophyd_async/epics/demo/_mover.py +0 -95
  143. ophyd_async/epics/demo/_sensor.py +0 -37
  144. ophyd_async/epics/demo/sensor.db +0 -19
  145. ophyd_async/fastcs/panda/_utils.py +0 -16
  146. ophyd_async/sim/demo/__init__.py +0 -19
  147. ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
  148. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
  149. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
  150. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
  151. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
  152. ophyd_async/sim/demo/_sim_motor.py +0 -107
  153. ophyd_async/sim/testing/__init__.py +0 -0
  154. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  155. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  156. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
  157. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,233 @@
1
+ import asyncio
2
+ import contextlib
3
+ import time
4
+
5
+ import numpy as np
6
+ from bluesky.protocols import Location, Reading, Stoppable, Subscribable
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from ophyd_async.core import (
10
+ AsyncStatus,
11
+ Callback,
12
+ StandardReadable,
13
+ WatchableAsyncStatus,
14
+ WatcherUpdate,
15
+ observe_value,
16
+ soft_signal_r_and_setter,
17
+ soft_signal_rw,
18
+ )
19
+ from ophyd_async.core import StandardReadableFormat as Format
20
+
21
+
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
+ class SimMotor(StandardReadable, Stoppable, Subscribable[float]):
54
+ """For usage when simulating a motor."""
55
+
56
+ def __init__(self, name="", instant=True) -> None:
57
+ """Simulate a motor, with optional velocity.
58
+
59
+ :param name: name of device
60
+ :param instant: whether to move instantly or calculate move time using velocity
61
+ """
62
+ # Define some signals
63
+ with self.add_children_as_readables(Format.HINTED_SIGNAL):
64
+ self.user_readback, self._user_readback_set = soft_signal_r_and_setter(
65
+ float, 0
66
+ )
67
+ with self.add_children_as_readables(Format.CONFIG_SIGNAL):
68
+ self.velocity = soft_signal_rw(float, 0 if instant else 1.0)
69
+ self.acceleration_time = soft_signal_rw(float, 0.5)
70
+ self.units = soft_signal_rw(str, "mm")
71
+ self.user_setpoint = soft_signal_rw(float, 0)
72
+
73
+ # Whether set() should complete successfully or not
74
+ self._set_success = True
75
+ self._move_status: AsyncStatus | None = None
76
+ # Stored in prepare
77
+ self._fly_info: FlySimMotorInfo | None = None
78
+ # Set on kickoff(), complete when motor reaches end position
79
+ self._fly_status: WatchableAsyncStatus | None = None
80
+
81
+ super().__init__(name=name)
82
+
83
+ def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
84
+ super().set_name(name, child_name_separator=child_name_separator)
85
+ # Readback should be named the same as its parent in read()
86
+ self.user_readback.set_name(name)
87
+
88
+ @AsyncStatus.wrap
89
+ async def prepare(self, value: FlySimMotorInfo):
90
+ """Calculate run-up and move there, setting fly velocity when there."""
91
+ self._fly_info = value
92
+ # Move to start as fast as we can
93
+ await self.velocity.set(0)
94
+ await self.set(value.start_position(await self.acceleration_time.get_value()))
95
+ # Set the velocity for the actual move
96
+ await self.velocity.set(value.velocity)
97
+
98
+ async def locate(self) -> Location[float]:
99
+ """Return the current setpoint and readback of the motor."""
100
+ setpoint, readback = await asyncio.gather(
101
+ self.user_setpoint.get_value(), self.user_readback.get_value()
102
+ )
103
+ return Location(setpoint=setpoint, readback=readback)
104
+
105
+ def subscribe(self, function: Callback[dict[str, Reading[float]]]) -> None:
106
+ self.user_readback.subscribe(function)
107
+
108
+ def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
109
+ self.user_readback.clear_sub(function)
110
+
111
+ @AsyncStatus.wrap
112
+ async def kickoff(self):
113
+ """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)
117
+ acceleration_time = await self.acceleration_time.get_value()
118
+ self._fly_status = self.set(self._fly_info.end_position(acceleration_time))
119
+ # Wait for the acceleration time to ensure we are at velocity
120
+ await asyncio.sleep(acceleration_time)
121
+
122
+ def complete(self) -> WatchableAsyncStatus:
123
+ """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
128
+
129
+ async def _move(self, old_position: float, new_position: float, velocity: float):
130
+ if old_position == new_position:
131
+ return
132
+ start = time.monotonic()
133
+ acceleration_time = abs(await self.acceleration_time.get_value())
134
+ sign = np.sign(new_position - old_position)
135
+ velocity = abs(velocity) * sign
136
+ # The total distance to move
137
+ total_distance = new_position - old_position
138
+ # The ramp distance is the distance taken to ramp up (the same distance
139
+ # is taken to ramp down). This is the area under the triangle of the
140
+ # velocity ramp up (base * height / 2)
141
+ ramp_distance = acceleration_time * velocity / 2
142
+ if abs(ramp_distance * 2) >= abs(total_distance):
143
+ # All time is ramp up and down, so recalculate the maximum velocity
144
+ # we get to. We know the area under the ramp up triangle is half the
145
+ # total distance, and we also know the ratio of velocity over
146
+ # acceleration_time is the same as the ration of max_velocity over
147
+ # ramp_time, so solve the simultaneous equations to get
148
+ # max_velocity and ramp_time.
149
+ max_velocity = np.sqrt(total_distance * velocity / acceleration_time) * sign
150
+ ramp_time = total_distance / max_velocity
151
+ # So move time is just the ramp up and ramp down with no constant
152
+ # velocity section
153
+ move_time = 2 * ramp_time
154
+ else:
155
+ # Middle segments of constant velocity
156
+ max_velocity = velocity
157
+ # Ramp up and down time is exactly the requested acceleration time
158
+ ramp_time = acceleration_time
159
+ # So move time is twice this, plus the time taken to move the
160
+ # remaining distance at constant velocity
161
+ move_time = ramp_time * 2 + (total_distance - ramp_distance * 2) / velocity
162
+ # Make an array of relative update times at 10Hz intervals
163
+ update_times = list(np.arange(0.1, move_time, 0.1, dtype=float))
164
+ # With the end position appended
165
+ if update_times and np.isclose(update_times[-1], move_time):
166
+ update_times[-1] = move_time
167
+ else:
168
+ update_times.append(move_time)
169
+ # Iterate through the update times, calculating new position for each
170
+ for t in update_times:
171
+ if t <= ramp_time:
172
+ # Ramp up phase, calculate area under the ramp up triangle
173
+ current_velocity = t / ramp_time * max_velocity
174
+ position = old_position + current_velocity * t / 2
175
+ elif t >= move_time - ramp_time:
176
+ # Ramp down phase, subtract area under the ramp down triangle
177
+ time_left = move_time - t
178
+ current_velocity = time_left / ramp_time * max_velocity
179
+ position = new_position - current_velocity * time_left / 2
180
+ else:
181
+ # Constant velocity phase
182
+ position = old_position + ramp_distance + (t - ramp_time) * max_velocity
183
+ # Calculate how long to wait to get there
184
+ relative_time = time.monotonic() - start
185
+ await asyncio.sleep(t - relative_time)
186
+ # Update the readback position
187
+ self._user_readback_set(position)
188
+
189
+ @WatchableAsyncStatus.wrap
190
+ async def set(self, value: float):
191
+ """Asynchronously move the motor to a new position."""
192
+ new_position = value
193
+ # Make sure any existing move tasks are stopped
194
+ if self._move_status:
195
+ self._move_status.task.cancel()
196
+ self._move_status = None
197
+ # work out where we were
198
+ old_position, units, velocity = await asyncio.gather(
199
+ self.user_setpoint.get_value(),
200
+ self.units.get_value(),
201
+ self.velocity.get_value(),
202
+ )
203
+ # update the setpoint to where we want to be
204
+ await self.user_setpoint.set(new_position)
205
+ # If zero velocity, do instant move
206
+ if velocity == 0:
207
+ self._user_readback_set(new_position)
208
+ else:
209
+ self._move_status = AsyncStatus(
210
+ self._move(old_position, new_position, velocity)
211
+ )
212
+ # If stop is called then this will raise a CancelledError, ignore it
213
+ with contextlib.suppress(asyncio.CancelledError):
214
+ async for current_position in observe_value(
215
+ self.user_readback, done_status=self._move_status
216
+ ):
217
+ yield WatcherUpdate(
218
+ current=current_position,
219
+ initial=old_position,
220
+ target=new_position,
221
+ name=self.name,
222
+ unit=units,
223
+ )
224
+ if not self._set_success:
225
+ raise RuntimeError("Motor was stopped")
226
+
227
+ async def stop(self, success=True):
228
+ """Stop the motor if it is moving."""
229
+ self._set_success = success
230
+ if self._move_status:
231
+ self._move_status.task.cancel()
232
+ self._move_status = None
233
+ await self.user_setpoint.set(await self.user_readback.get_value())
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import h5py
8
+ import numpy as np
9
+
10
+ # raw data path
11
+ DATA_PATH = "/entry/data/data"
12
+
13
+ # pixel sum path
14
+ SUM_PATH = "/entry/sum"
15
+
16
+
17
+ def generate_gaussian_blob(height: int, width: int) -> np.ndarray:
18
+ """Make a Gaussian Blob with float values in range 0..1."""
19
+ x, y = np.meshgrid(np.linspace(-1, 1, width), np.linspace(-1, 1, height))
20
+ d = np.sqrt(x * x + y * y)
21
+ blob = np.exp(-(d**2))
22
+ return blob
23
+
24
+
25
+ def generate_interesting_pattern(
26
+ x: float, y: float, channel: int, offset: float
27
+ ) -> float:
28
+ """Return a float value in range 0..1.
29
+
30
+ Interesting in x and y in range -10..10
31
+ """
32
+ return (np.sin(x) ** channel + np.cos(x * y + offset) + 2) / 4
33
+
34
+
35
+ class PatternFile:
36
+ def __init__(
37
+ self,
38
+ path: Path,
39
+ width: int = 320,
40
+ height: int = 240,
41
+ ):
42
+ self.file = h5py.File(path, "w", libver="latest")
43
+ self.data = self.file.create_dataset(
44
+ name=DATA_PATH,
45
+ shape=(0, height, width),
46
+ dtype=np.uint8,
47
+ maxshape=(None, height, width),
48
+ )
49
+ self.sum = self.file.create_dataset(
50
+ name=SUM_PATH,
51
+ shape=(0,),
52
+ dtype=np.int64,
53
+ maxshape=(None,),
54
+ )
55
+ # Once datasets written, can switch the model to single writer multiple reader
56
+ self.file.swmr_mode = True
57
+ self.blob = generate_gaussian_blob(height, width) * np.iinfo(np.uint8).max
58
+ self.image_counter = 0
59
+ self.e = asyncio.Event()
60
+
61
+ def write_image_to_file(self, intensity: float):
62
+ data = np.floor(self.blob * intensity)
63
+ for dset, value in ((self.data, data), (self.sum, np.sum(data))):
64
+ dset.resize(self.image_counter + 1, axis=0)
65
+ dset[self.image_counter] = value
66
+ dset.flush()
67
+ self.image_counter += 1
68
+ self.e.set()
69
+ self.e.clear()
70
+
71
+ def close(self):
72
+ self.file.close()
73
+
74
+
75
+ class PatternGenerator:
76
+ """Generates pattern images in files."""
77
+
78
+ def __init__(self, sleep=asyncio.sleep):
79
+ self._x = 0.0
80
+ self._y = 0.0
81
+ self._file: PatternFile | None = None
82
+ self.sleep = sleep
83
+
84
+ def set_x(self, x: float):
85
+ self._x = x
86
+
87
+ def set_y(self, y: float):
88
+ self._y = y
89
+
90
+ def generate_point(self, channel: int = 1, high_energy: bool = False) -> float:
91
+ """Make a point between 0 and 1 based on x and y."""
92
+ offset = 100 if high_energy else 10
93
+ return generate_interesting_pattern(self._x, self._y, channel, offset)
94
+
95
+ def open_file(self, path: Path, width: int, height: int):
96
+ self._file = PatternFile(path, width, height)
97
+
98
+ def _get_file(self) -> PatternFile:
99
+ if not self._file:
100
+ raise RuntimeError("open_file not run")
101
+ return self._file
102
+
103
+ async def write_images_to_file(
104
+ self, exposure: float, period: float, number_of_frames: int
105
+ ):
106
+ file = self._get_file()
107
+ start = time.monotonic()
108
+ for i in range(1, number_of_frames + 1):
109
+ deadline = start + i * period
110
+ timeout = deadline - time.monotonic()
111
+ await self.sleep(timeout)
112
+ intensity = self.generate_point() * exposure
113
+ file.write_image_to_file(intensity)
114
+
115
+ async def wait_for_next_index(self, timeout: float):
116
+ await asyncio.wait_for(self._get_file().e.wait(), timeout)
117
+
118
+ def get_last_index(self) -> int:
119
+ return self._get_file().image_counter
120
+
121
+ def close_file(self):
122
+ if self._file:
123
+ self._file.close()
124
+ self._file = None
@@ -0,0 +1,86 @@
1
+ import asyncio
2
+ import time
3
+
4
+ import numpy as np
5
+
6
+ from ophyd_async.core import (
7
+ AsyncStatus,
8
+ DeviceVector,
9
+ SignalR,
10
+ StandardReadable,
11
+ StrictEnum,
12
+ gather_dict,
13
+ soft_signal_r_and_setter,
14
+ soft_signal_rw,
15
+ )
16
+ from ophyd_async.core import StandardReadableFormat as Format
17
+
18
+ from ._pattern_generator import PatternGenerator
19
+
20
+
21
+ class EnergyMode(StrictEnum):
22
+ """Energy mode for `SimPointDetector`."""
23
+
24
+ LOW = "Low Energy"
25
+ """Low energy mode"""
26
+
27
+ HIGH = "High Energy"
28
+ """High energy mode"""
29
+
30
+
31
+ class SimPointDetectorChannel(StandardReadable):
32
+ def __init__(self, value_signal: SignalR[int], name=""):
33
+ with self.add_children_as_readables(Format.HINTED_SIGNAL):
34
+ self.value = value_signal
35
+ with self.add_children_as_readables(Format.CONFIG_SIGNAL):
36
+ self.mode = soft_signal_rw(EnergyMode)
37
+ super().__init__(name)
38
+
39
+
40
+ class SimPointDetector(StandardReadable):
41
+ """Simalutes a point detector with multiple channels."""
42
+
43
+ def __init__(
44
+ self, generator: PatternGenerator, num_channels: int = 3, name: str = ""
45
+ ) -> None:
46
+ self._generator = generator
47
+ self.acquire_time = soft_signal_rw(float, 0.1)
48
+ self.acquiring, self._set_acquiring = soft_signal_r_and_setter(bool)
49
+ self._value_signals = dict(
50
+ soft_signal_r_and_setter(int) for _ in range(num_channels)
51
+ )
52
+ with self.add_children_as_readables():
53
+ self.channel = DeviceVector(
54
+ {
55
+ i + 1: SimPointDetectorChannel(value_signal)
56
+ for i, value_signal in enumerate(self._value_signals)
57
+ }
58
+ )
59
+ super().__init__(name=name)
60
+
61
+ async def _update_values(self, acquire_time: float):
62
+ # Get the modes
63
+ modes = await gather_dict(
64
+ {channel: channel.mode.get_value() for channel in self.channel.values()}
65
+ )
66
+ start = time.monotonic()
67
+ # Make an array of relative update times at 10Hz intervals
68
+ update_times = np.arange(0.1, acquire_time, 0.1)
69
+ # With the end position appended
70
+ update_times = np.concatenate((update_times, [acquire_time]))
71
+ for update_time in update_times:
72
+ # Calculate how long to wait to get there
73
+ relative_time = time.monotonic() - start
74
+ await asyncio.sleep(update_time - relative_time)
75
+ # Update the channel value
76
+ for i, channel in self.channel.items():
77
+ high_energy = modes[channel] == EnergyMode.HIGH
78
+ point = self._generator.generate_point(i, high_energy)
79
+ setter = self._value_signals[channel.value]
80
+ setter(int(point * 10000 * update_time))
81
+
82
+ @AsyncStatus.wrap
83
+ async def trigger(self):
84
+ for setter in self._value_signals.values():
85
+ setter(0)
86
+ await self._update_values(await self.acquire_time.get_value())
@@ -0,0 +1,19 @@
1
+ from ophyd_async.core import StandardReadable
2
+ from ophyd_async.sim._pattern_generator import PatternGenerator
3
+
4
+ from ._motor import SimMotor
5
+
6
+
7
+ class SimStage(StandardReadable):
8
+ """A simulated sample stage with X and Y movables."""
9
+
10
+ def __init__(self, pattern_generator: PatternGenerator, name="") -> None:
11
+ # Define some child Devices
12
+ with self.add_children_as_readables():
13
+ self.x = SimMotor(instant=False)
14
+ self.y = SimMotor(instant=False)
15
+ # Tell the pattern generator about the motor positions
16
+ self.x.user_readback.subscribe_value(pattern_generator.set_x)
17
+ self.y.user_readback.subscribe_value(pattern_generator.set_y)
18
+ # Set name of device and child devices
19
+ super().__init__(name=name)
@@ -0,0 +1 @@
1
+ """Tango support for Signals, and Devices that use them."""
@@ -1,4 +1,4 @@
1
- from ._base_device import TangoDevice, TangoPolling
1
+ from ._base_device import TangoDevice, TangoDeviceConnector, TangoPolling
2
2
  from ._signal import (
3
3
  infer_python_type,
4
4
  infer_signal_type,
@@ -19,10 +19,12 @@ from ._tango_transport import (
19
19
  get_tango_trl,
20
20
  get_trl_descriptor,
21
21
  )
22
+ from ._utils import DevStateEnum, get_device_trl_and_attr, get_full_attr_trl
22
23
 
23
24
  __all__ = [
24
25
  "AttributeProxy",
25
26
  "CommandProxy",
27
+ "DevStateEnum",
26
28
  "ensure_proper_executor",
27
29
  "TangoSignalBackend",
28
30
  "get_python_type",
@@ -39,4 +41,7 @@ __all__ = [
39
41
  "TangoDevice",
40
42
  "TangoReadable",
41
43
  "TangoPolling",
44
+ "TangoDeviceConnector",
45
+ "get_device_trl_and_attr",
46
+ "get_full_attr_trl",
42
47
  ]
@@ -4,27 +4,23 @@ from dataclasses import dataclass
4
4
  from typing import Any, Generic, TypeVar
5
5
 
6
6
  from ophyd_async.core import Device, DeviceConnector, DeviceFiller, LazyMock
7
- from tango import DeviceProxy as DeviceProxy
7
+ from tango import DeviceProxy
8
8
  from tango.asyncio import DeviceProxy as AsyncDeviceProxy
9
9
 
10
10
  from ._signal import TangoSignalBackend, infer_python_type, infer_signal_type
11
+ from ._utils import get_full_attr_trl
11
12
 
12
13
  T = TypeVar("T")
13
14
 
14
15
 
15
16
  class TangoDevice(Device):
16
- """
17
- General class for TangoDevices. Extends Device to provide attributes for Tango
18
- devices.
19
-
20
- Parameters
21
- ----------
22
- trl: str
23
- Tango resource locator, typically of the device server.
24
- device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]]
25
- Asynchronous or synchronous DeviceProxy object for the device. If not provided,
26
- an asynchronous DeviceProxy object will be created using the trl and awaited
27
- when the device is connected.
17
+ """General class for TangoDevices.
18
+
19
+ Extends Device to provide attributes for Tango devices.
20
+
21
+ :param trl: Tango resource locator, typically of the device server.
22
+ An asynchronous DeviceProxy object will be created using the
23
+ trl and awaited when the device is connected.
28
24
  """
29
25
 
30
26
  trl: str = ""
@@ -32,13 +28,15 @@ class TangoDevice(Device):
32
28
 
33
29
  def __init__(
34
30
  self,
35
- trl: str | None = None,
36
- device_proxy: DeviceProxy | None = None,
31
+ trl: str | None,
37
32
  support_events: bool = False,
38
33
  name: str = "",
34
+ auto_fill_signals: bool = True,
39
35
  ) -> None:
40
36
  connector = TangoDeviceConnector(
41
- trl=trl, device_proxy=device_proxy, support_events=support_events
37
+ trl=trl,
38
+ support_events=support_events,
39
+ auto_fill_signals=auto_fill_signals,
42
40
  )
43
41
  super().__init__(name=name, connector=connector)
44
42
 
@@ -74,12 +72,12 @@ class TangoDeviceConnector(DeviceConnector):
74
72
  def __init__(
75
73
  self,
76
74
  trl: str | None,
77
- device_proxy: DeviceProxy | None,
78
75
  support_events: bool,
76
+ auto_fill_signals: bool = True,
79
77
  ) -> None:
80
78
  self.trl = trl
81
- self.proxy = device_proxy
82
79
  self._support_events = support_events
80
+ self._auto_fill_signals = auto_fill_signals
83
81
 
84
82
  def create_children_from_annotations(self, device: Device):
85
83
  if not hasattr(self, "filler"):
@@ -87,7 +85,7 @@ class TangoDeviceConnector(DeviceConnector):
87
85
  device=device,
88
86
  signal_backend_factory=TangoSignalBackend,
89
87
  device_connector_factory=lambda: TangoDeviceConnector(
90
- None, None, self._support_events
88
+ None, self._support_events
91
89
  ),
92
90
  )
93
91
  list(self.filler.create_devices_from_annotations(filled=False))
@@ -105,28 +103,38 @@ class TangoDeviceConnector(DeviceConnector):
105
103
  return await super().connect_mock(device, mock)
106
104
 
107
105
  async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
108
- if self.trl and self.proxy is None:
109
- self.proxy = await AsyncDeviceProxy(self.trl)
110
- elif self.proxy and not self.trl:
111
- self.trl = self.proxy.name()
112
- else:
113
- raise TypeError("Neither proxy nor trl supplied")
114
-
106
+ if not self.trl:
107
+ raise RuntimeError(f"Could not created Device Proxy for TRL {self.trl}")
108
+ self.proxy = await AsyncDeviceProxy(self.trl)
115
109
  children = sorted(
116
110
  set()
117
111
  .union(self.proxy.get_attribute_list())
118
112
  .union(self.proxy.get_command_list())
119
113
  )
114
+
115
+ children = [
116
+ child for child in children if child not in self.filler.ignored_signals
117
+ ]
118
+
119
+ not_filled = {unfilled for unfilled, _ in device.children()}
120
+
121
+ # If auto_fill_signals is True, fill all children inferred from the device
122
+ # else fill only the children that are annotated
120
123
  for name in children:
121
- # TODO: strip attribute name
122
- full_trl = f"{self.trl}/{name}"
123
- signal_type = await infer_signal_type(full_trl, self.proxy)
124
- if signal_type:
125
- backend = self.filler.fill_child_signal(name, signal_type)
126
- backend.datatype = await infer_python_type(full_trl, self.proxy)
127
- backend.set_trl(full_trl)
124
+ if self._auto_fill_signals or name in not_filled:
125
+ # TODO: strip attribute name
126
+ full_trl = get_full_attr_trl(self.trl, name)
127
+ signal_type = await infer_signal_type(full_trl, self.proxy)
128
+ if signal_type:
129
+ backend = self.filler.fill_child_signal(name, signal_type)
130
+ # don't overlaod datatype if provided by annotation
131
+ if backend.datatype is None:
132
+ backend.datatype = await infer_python_type(full_trl, self.proxy)
133
+ backend.set_trl(full_trl)
134
+
128
135
  # Check that all the requested children have been filled
129
136
  self.filler.check_filled(f"{self.trl}: {children}")
137
+
130
138
  # Set the name of the device to name all children
131
139
  device.set_name(device.name)
132
140
  return await super().connect_real(device, timeout, force_reconnect)