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,152 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Callable, Mapping
5
+ from typing import Any
6
+
7
+ import bluesky.plan_stubs as bps
8
+ import numpy as np
9
+ from bluesky.utils import MsgGenerator, plan
10
+
11
+ from ophyd_async.core import (
12
+ Device,
13
+ Settings,
14
+ SettingsProvider,
15
+ SignalRW,
16
+ walk_config_signals,
17
+ walk_rw_signals,
18
+ )
19
+ from ophyd_async.core._table import Table
20
+
21
+ from ._utils import T
22
+ from ._wait_for_awaitable import wait_for_awaitable
23
+
24
+
25
+ @plan
26
+ def _get_values_of_signals(
27
+ signals: Mapping[T, SignalRW],
28
+ ) -> MsgGenerator[dict[T, Any]]:
29
+ coros = [sig.get_value() for sig in signals.values()]
30
+ values = yield from wait_for_awaitable(asyncio.gather(*coros))
31
+ named_values = dict(zip(signals, values, strict=True))
32
+ return named_values
33
+
34
+
35
+ @plan
36
+ def get_current_settings(
37
+ device: Device, only_config: bool = False
38
+ ) -> MsgGenerator[Settings]:
39
+ """Get current settings on `Device`.
40
+
41
+ If `only_config` is True, get current configuration settings on `Configurable`.
42
+ """
43
+ if only_config:
44
+ signals = yield from wait_for_awaitable(walk_config_signals(device))
45
+ else:
46
+ signals = walk_rw_signals(device)
47
+ named_values = yield from _get_values_of_signals(signals)
48
+ signal_values = {signals[name]: value for name, value in named_values.items()}
49
+ return Settings(device, signal_values)
50
+
51
+
52
+ @plan
53
+ def store_settings(
54
+ provider: SettingsProvider, name: str, device: Device, only_config: bool = False
55
+ ) -> MsgGenerator[None]:
56
+ """Walk a Device for SignalRWs and store their values.
57
+
58
+ If `only_config` is True, store only configuration settings on `Configurable`.
59
+
60
+ :param provider: The provider to store the settings with.
61
+ :param name: The name to store the settings under.
62
+ :param device: The Device to walk for SignalRWs.
63
+ :param only_config: If True, store only configuration settings.
64
+ """
65
+ if only_config:
66
+ signals = yield from wait_for_awaitable(walk_config_signals(device))
67
+ else:
68
+ signals = walk_rw_signals(device)
69
+ named_values = yield from _get_values_of_signals(signals)
70
+ yield from wait_for_awaitable(provider.store(name, named_values))
71
+
72
+
73
+ @plan
74
+ def retrieve_settings(
75
+ provider: SettingsProvider, name: str, device: Device, only_config: bool = False
76
+ ) -> MsgGenerator[Settings]:
77
+ """Retrieve named Settings for a Device from a provider.
78
+
79
+ If `only_config` is True, retrieve only configuration settings on `Configurable`.
80
+
81
+ :param provider: The provider to retrieve the settings from.
82
+ :param name: The name of the settings to retrieve.
83
+ :param device: The Device to retrieve the settings for.
84
+ :param only_config: If True, retrieve only configuration settings.
85
+ """
86
+ named_values = yield from wait_for_awaitable(provider.retrieve(name))
87
+ if only_config:
88
+ signals = yield from wait_for_awaitable(walk_config_signals(device))
89
+ else:
90
+ signals = walk_rw_signals(device)
91
+ unknown_names = set(named_values) - set(signals)
92
+ if unknown_names:
93
+ raise NameError(f"Unknown signal names {sorted(unknown_names)}")
94
+ signal_values = {signals[name]: value for name, value in named_values.items()}
95
+ return Settings(device, signal_values)
96
+
97
+
98
+ @plan
99
+ def apply_settings(settings: Settings) -> MsgGenerator[None]:
100
+ """Set every SignalRW to the given value in Settings. If value is None ignore it."""
101
+ signal_values = {
102
+ signal: value for signal, value in settings.items() if value is not None
103
+ }
104
+ if signal_values:
105
+ for signal, value in signal_values.items():
106
+ yield from bps.abs_set(signal, value, group="apply_settings")
107
+ yield from bps.wait("apply_settings")
108
+
109
+
110
+ @plan
111
+ def apply_settings_if_different(
112
+ settings: Settings,
113
+ apply_plan: Callable[[Settings], MsgGenerator[None]],
114
+ current_settings: Settings | None = None,
115
+ ) -> MsgGenerator[None]:
116
+ """Set every SignalRW in settings, only if it is different to the current value.
117
+
118
+ :param apply_plan:
119
+ A device specific plan which takes the Settings to apply and applies them to
120
+ the Device. Used to add device specific ordering to setting the signals.
121
+ :param current_settings:
122
+ If given, should be a superset of settings containing the current value of
123
+ the Settings in the Device. If not given it will be created by reading just
124
+ the signals given in settings.
125
+ """
126
+ if current_settings is None:
127
+ # If we aren't give the current settings, then get the
128
+ # values of just the signals we were asked to change.
129
+ # This allows us to use this plan with Settings for a subset
130
+ # of signals in the Device without retrieving them all
131
+ signal_values = yield from _get_values_of_signals(
132
+ {sig: sig for sig in settings}
133
+ )
134
+ current_settings = Settings(settings.device, signal_values)
135
+
136
+ def _is_different(current, required) -> bool:
137
+ if isinstance(current, Table):
138
+ current = current.model_dump()
139
+ if isinstance(required, Table):
140
+ required = required.model_dump()
141
+ return current.keys() != required.keys() or any(
142
+ _is_different(current[k], required[k]) for k in current
143
+ )
144
+ elif isinstance(current, np.ndarray):
145
+ return not np.array_equal(current, required)
146
+ else:
147
+ return current != required
148
+
149
+ settings_to_change, _ = settings.partition(
150
+ lambda sig: _is_different(current_settings[sig], settings[sig])
151
+ )
152
+ yield from apply_plan(settings_to_change)
@@ -0,0 +1,3 @@
1
+ from typing import TypeVar
2
+
3
+ T = TypeVar("T")
@@ -0,0 +1,13 @@
1
+ from collections.abc import Awaitable
2
+
3
+ import bluesky.plan_stubs as bps
4
+ from bluesky.utils import MsgGenerator, plan
5
+
6
+ from ._utils import T
7
+
8
+
9
+ @plan
10
+ def wait_for_awaitable(coro: Awaitable[T]) -> MsgGenerator[T]:
11
+ """Wait for a single awaitable to complete, and return the result."""
12
+ (task,) = yield from bps.wait_for([lambda: coro])
13
+ return task.result()
@@ -0,0 +1,29 @@
1
+ """Some simulated devices to be used in tutorials and testing."""
2
+
3
+ from ._blob_detector import SimBlobDetector
4
+ from ._mirror_horizontal import HorizontalMirror, HorizontalMirrorDerived
5
+ from ._mirror_vertical import (
6
+ TwoJackDerived,
7
+ TwoJackRaw,
8
+ TwoJackTransform,
9
+ VerticalMirror,
10
+ )
11
+ from ._motor import FlySimMotorInfo, SimMotor
12
+ from ._pattern_generator import PatternGenerator
13
+ from ._point_detector import SimPointDetector
14
+ from ._stage import SimStage
15
+
16
+ __all__ = [
17
+ "SimMotor",
18
+ "FlySimMotorInfo",
19
+ "SimStage",
20
+ "PatternGenerator",
21
+ "SimPointDetector",
22
+ "SimBlobDetector",
23
+ "VerticalMirror",
24
+ "HorizontalMirror",
25
+ "HorizontalMirrorDerived",
26
+ "TwoJackTransform",
27
+ "TwoJackDerived",
28
+ "TwoJackRaw",
29
+ ]
@@ -0,0 +1,43 @@
1
+ """Used for tutorial `Using Devices`."""
2
+
3
+ # Import bluesky and ophyd
4
+ from tempfile import mkdtemp
5
+
6
+ import bluesky.plan_stubs as bps # noqa: F401
7
+ import bluesky.plans as bp # noqa: F401
8
+ import bluesky.preprocessors as bpp # noqa: F401
9
+ from bluesky.callbacks.best_effort import BestEffortCallback
10
+ from bluesky.run_engine import RunEngine, autoawait_in_bluesky_event_loop
11
+
12
+ from ophyd_async import sim
13
+ from ophyd_async.core import StaticPathProvider, UUIDFilenameProvider, init_devices
14
+
15
+ # Create a run engine and make ipython use it for `await` commands
16
+ RE = RunEngine(call_returns_result=True)
17
+ autoawait_in_bluesky_event_loop()
18
+
19
+ # Add a callback for plotting
20
+ bec = BestEffortCallback()
21
+ RE.subscribe(bec)
22
+
23
+ # Make a pattern generator that uses the motor positions
24
+ # to make a test pattern. This simulates the real life process
25
+ # of X-ray scattering off a sample
26
+ pattern_generator = sim.PatternGenerator()
27
+
28
+ # Make a path provider that makes UUID filenames within a static
29
+ # temporary directory
30
+ path_provider = StaticPathProvider(UUIDFilenameProvider(), mkdtemp())
31
+
32
+ # All Devices created within this block will be
33
+ # connected and named at the end of the with block
34
+ with init_devices():
35
+ # Create a sample stage with X and Y motors that report their positions
36
+ # to the pattern generator
37
+ stage = sim.SimStage(pattern_generator)
38
+ # Make a detector device that gives the point value of the pattern generator
39
+ # when triggered
40
+ pdet = sim.SimPointDetector(pattern_generator)
41
+ # Make a detector device that gives a gaussian blob with intensity based
42
+ # on the point value of the pattern generator when triggered
43
+ bdet = sim.SimBlobDetector(path_provider, pattern_generator)
@@ -0,0 +1,33 @@
1
+ from collections.abc import Sequence
2
+
3
+ from ophyd_async.core import PathProvider, SignalR, StandardDetector
4
+
5
+ from ._blob_detector_controller import BlobDetectorController
6
+ from ._blob_detector_writer import BlobDetectorWriter
7
+ from ._pattern_generator import PatternGenerator
8
+
9
+
10
+ class SimBlobDetector(StandardDetector):
11
+ """Simulates a detector and writes Blobs to file."""
12
+
13
+ def __init__(
14
+ self,
15
+ path_provider: PathProvider,
16
+ pattern_generator: PatternGenerator | None = None,
17
+ config_sigs: Sequence[SignalR] = (),
18
+ name: str = "",
19
+ ) -> None:
20
+ self.pattern_generator = pattern_generator or PatternGenerator()
21
+
22
+ super().__init__(
23
+ controller=BlobDetectorController(
24
+ pattern_generator=self.pattern_generator,
25
+ ),
26
+ writer=BlobDetectorWriter(
27
+ pattern_generator=self.pattern_generator,
28
+ path_provider=path_provider,
29
+ name_provider=lambda: self.name,
30
+ ),
31
+ config_sigs=config_sigs,
32
+ name=name,
33
+ )
@@ -0,0 +1,48 @@
1
+ import asyncio
2
+ from contextlib import suppress
3
+
4
+ from ophyd_async.core import DetectorController, DetectorTrigger, TriggerInfo
5
+
6
+ from ._pattern_generator import PatternGenerator
7
+
8
+
9
+ class BlobDetectorController(DetectorController):
10
+ def __init__(self, pattern_generator: PatternGenerator):
11
+ self.pattern_generator = pattern_generator
12
+ self.trigger_info: TriggerInfo | None = None
13
+ self.task: asyncio.Task | None = None
14
+
15
+ def get_deadtime(self, exposure):
16
+ return 0.001
17
+
18
+ async def prepare(self, trigger_info: TriggerInfo):
19
+ # This is a simulation, so only support intenal triggering
20
+ if trigger_info.trigger != DetectorTrigger.INTERNAL:
21
+ raise RuntimeError(f"{trigger_info.trigger} not supported by {self}")
22
+ # Just hold onto the trigger info until we need it
23
+ self.trigger_info = trigger_info
24
+
25
+ async def arm(self):
26
+ if self.trigger_info is None:
27
+ raise RuntimeError(f"prepare() not called on {self}")
28
+ livetime = self.trigger_info.livetime or 0.1
29
+ # Start a background process off writing the images to file
30
+ coro = self.pattern_generator.write_images_to_file(
31
+ exposure=livetime,
32
+ period=livetime + self.trigger_info.deadtime,
33
+ number_of_frames=self.trigger_info.total_number_of_triggers,
34
+ )
35
+ self.task = asyncio.create_task(coro)
36
+
37
+ async def wait_for_idle(self):
38
+ # Wait for the background task to complete
39
+ if self.task:
40
+ await self.task
41
+
42
+ async def disarm(self):
43
+ # Stop the background task and wait for it to finish
44
+ if self.task:
45
+ self.task.cancel()
46
+ with suppress(asyncio.CancelledError):
47
+ await self.task
48
+ self.task = None
@@ -0,0 +1,105 @@
1
+ from collections.abc import AsyncGenerator, AsyncIterator
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ from bluesky.protocols import Hints, StreamAsset
6
+ from event_model import DataKey
7
+
8
+ from ophyd_async.core import (
9
+ DetectorWriter,
10
+ HDFDatasetDescription,
11
+ HDFDocumentComposer,
12
+ NameProvider,
13
+ PathProvider,
14
+ )
15
+
16
+ from ._pattern_generator import DATA_PATH, SUM_PATH, PatternGenerator
17
+
18
+ WIDTH = 320
19
+ HEIGHT = 240
20
+
21
+
22
+ class BlobDetectorWriter(DetectorWriter):
23
+ def __init__(
24
+ self,
25
+ pattern_generator: PatternGenerator,
26
+ path_provider: PathProvider,
27
+ name_provider: NameProvider,
28
+ ) -> None:
29
+ self.pattern_generator = pattern_generator
30
+ self.path_provider = path_provider
31
+ self.name_provider = name_provider
32
+ self.path: Path | None = None
33
+ self.composer: HDFDocumentComposer | None = None
34
+ self.datasets: list[HDFDatasetDescription] = []
35
+
36
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
37
+ name = self.name_provider()
38
+ path_info = self.path_provider(name)
39
+ self.path = path_info.directory_path / f"{path_info.filename}.h5"
40
+ self.pattern_generator.open_file(self.path, WIDTH, HEIGHT)
41
+ # We know it will write data and sum, so emit those
42
+ self.datasets = [
43
+ HDFDatasetDescription(
44
+ data_key=name,
45
+ dataset=DATA_PATH,
46
+ shape=(HEIGHT, WIDTH),
47
+ dtype_numpy=np.dtype(np.uint8).str,
48
+ chunk_shape=(HEIGHT, WIDTH),
49
+ multiplier=multiplier,
50
+ ),
51
+ HDFDatasetDescription(
52
+ data_key=f"{name}-sum",
53
+ dataset=SUM_PATH,
54
+ shape=(),
55
+ dtype_numpy=np.dtype(np.int64).str,
56
+ multiplier=multiplier,
57
+ chunk_shape=(1024,),
58
+ ),
59
+ ]
60
+ self.composer = None
61
+ outer_shape = (multiplier,) if multiplier > 1 else ()
62
+ describe = {
63
+ ds.data_key: DataKey(
64
+ source="sim://pattern-generator-hdf-file",
65
+ shape=list(outer_shape) + list(ds.shape),
66
+ dtype="array" if ds.shape else "number",
67
+ external="STREAM:",
68
+ )
69
+ for ds in self.datasets
70
+ }
71
+ return describe
72
+
73
+ @property
74
+ def hints(self) -> Hints:
75
+ """The hints to be used for the detector."""
76
+ return {"fields": [self.name_provider()]}
77
+
78
+ async def get_indices_written(self) -> int:
79
+ return self.pattern_generator.get_last_index()
80
+
81
+ async def observe_indices_written(
82
+ self, timeout: float
83
+ ) -> AsyncGenerator[int, None]:
84
+ while True:
85
+ yield self.pattern_generator.get_last_index()
86
+ await self.pattern_generator.wait_for_next_index(timeout)
87
+
88
+ async def collect_stream_docs(
89
+ self, indices_written: int
90
+ ) -> AsyncIterator[StreamAsset]:
91
+ # When we have written something to the file
92
+ if indices_written:
93
+ # Only emit stream resource the first time we see frames in
94
+ # the file
95
+ if not self.composer:
96
+ if not self.path:
97
+ raise RuntimeError(f"open() not called on {self}")
98
+ self.composer = HDFDocumentComposer(self.path, self.datasets)
99
+ for doc in self.composer.stream_resources():
100
+ yield "stream_resource", doc
101
+ for doc in self.composer.stream_data(indices_written):
102
+ yield "stream_datum", doc
103
+
104
+ async def close(self) -> None:
105
+ self.pattern_generator.close_file()
@@ -0,0 +1,46 @@
1
+ import asyncio
2
+ from typing import TypedDict
3
+
4
+ from bluesky.protocols import Movable
5
+
6
+ from ophyd_async.core import AsyncStatus, DerivedSignalFactory, Device, soft_signal_rw
7
+
8
+ from ._mirror_vertical import TwoJackDerived, TwoJackTransform
9
+ from ._motor import SimMotor
10
+
11
+
12
+ class HorizontalMirrorDerived(TypedDict):
13
+ x: float
14
+ roll: float
15
+
16
+
17
+ class HorizontalMirror(Device, Movable):
18
+ def __init__(self, name=""):
19
+ # Raw signals
20
+ self.x1 = SimMotor()
21
+ self.x2 = SimMotor()
22
+ # Parameter
23
+ self.x1_x2_distance = soft_signal_rw(float, initial_value=1)
24
+ # Derived signals
25
+ self._factory = DerivedSignalFactory(
26
+ TwoJackTransform,
27
+ self._set_mirror,
28
+ jack1=self.x1,
29
+ jack2=self.x2,
30
+ distance=self.x1_x2_distance,
31
+ )
32
+ self.x = self._factory.derived_signal_rw(float, "height")
33
+ self.roll = self._factory.derived_signal_rw(float, "angle")
34
+ super().__init__(name=name)
35
+
36
+ async def _set_mirror(self, derived: TwoJackDerived) -> None:
37
+ transform = await self._factory.transform()
38
+ raw = transform.derived_to_raw(**derived)
39
+ await asyncio.gather(
40
+ self.x1.set(raw["jack1"]),
41
+ self.x2.set(raw["jack2"]),
42
+ )
43
+
44
+ @AsyncStatus.wrap
45
+ async def set(self, value: HorizontalMirrorDerived) -> None:
46
+ await self._set_mirror(TwoJackDerived(height=value["x"], angle=value["roll"]))
@@ -0,0 +1,74 @@
1
+ import asyncio
2
+ import math
3
+ from typing import TypedDict
4
+
5
+ from bluesky.protocols import Movable
6
+
7
+ from ophyd_async.core import (
8
+ AsyncStatus,
9
+ DerivedSignalFactory,
10
+ Device,
11
+ Transform,
12
+ soft_signal_rw,
13
+ )
14
+
15
+ from ._motor import SimMotor
16
+
17
+
18
+ class TwoJackRaw(TypedDict):
19
+ jack1: float
20
+ jack2: float
21
+
22
+
23
+ class TwoJackDerived(TypedDict):
24
+ height: float
25
+ angle: float
26
+
27
+
28
+ class TwoJackTransform(Transform):
29
+ distance: float
30
+
31
+ def raw_to_derived(self, *, jack1: float, jack2: float) -> TwoJackDerived:
32
+ diff = jack2 - jack1
33
+ return TwoJackDerived(
34
+ height=jack1 + diff / 2,
35
+ # need the cast as returns numpy float rather than float64, but this
36
+ # is ok at runtime
37
+ angle=math.atan(diff / self.distance),
38
+ )
39
+
40
+ def derived_to_raw(self, *, height: float, angle: float) -> TwoJackRaw:
41
+ diff = math.tan(angle) * self.distance
42
+ return TwoJackRaw(
43
+ jack1=height - diff / 2,
44
+ jack2=height + diff / 2,
45
+ )
46
+
47
+
48
+ class VerticalMirror(Device, Movable[TwoJackDerived]):
49
+ def __init__(self, name=""):
50
+ # Raw signals
51
+ self.y1 = SimMotor()
52
+ self.y2 = SimMotor()
53
+ # Parameter
54
+ self.y1_y2_distance = soft_signal_rw(float, initial_value=1)
55
+ # Derived signals
56
+ self._factory = DerivedSignalFactory(
57
+ TwoJackTransform,
58
+ self.set,
59
+ jack1=self.y1,
60
+ jack2=self.y2,
61
+ distance=self.y1_y2_distance,
62
+ )
63
+ self.height = self._factory.derived_signal_rw(float, "height")
64
+ self.angle = self._factory.derived_signal_rw(float, "angle")
65
+ super().__init__(name=name)
66
+
67
+ @AsyncStatus.wrap
68
+ async def set(self, derived: TwoJackDerived) -> None: # type: ignore until bluesky 1.13.2
69
+ transform = await self._factory.transform()
70
+ raw = transform.derived_to_raw(**derived)
71
+ await asyncio.gather(
72
+ self.y1.set(raw["jack1"]),
73
+ self.y2.set(raw["jack2"]),
74
+ )