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.
- ophyd_async/__init__.py +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +102 -74
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +158 -153
- ophyd_async/core/_device.py +143 -115
- ophyd_async/core/_device_filler.py +82 -9
- ophyd_async/core/_flyer.py +16 -7
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +74 -58
- ophyd_async/core/_settings.py +113 -0
- ophyd_async/core/_signal.py +304 -174
- ophyd_async/core/_signal_backend.py +60 -14
- ophyd_async/core/_soft_signal_backend.py +18 -12
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +54 -17
- ophyd_async/core/_utils.py +101 -52
- ophyd_async/core/_yaml_settings.py +66 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +51 -0
- ophyd_async/epics/adandor/_andor_io.py +34 -0
- ophyd_async/epics/adaravis/__init__.py +8 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -41
- ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +36 -14
- ophyd_async/epics/adcore/_core_detector.py +81 -0
- ophyd_async/epics/adcore/_core_io.py +145 -95
- ophyd_async/epics/adcore/_core_logic.py +179 -88
- ophyd_async/epics/adcore/_core_writer.py +223 -0
- ophyd_async/epics/adcore/_hdf_writer.py +51 -92
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +6 -5
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +3 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +32 -27
- ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +7 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +22 -16
- ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +26 -25
- ophyd_async/epics/advimba/_vimba_controller.py +12 -24
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +66 -30
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +50 -18
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +5 -5
- ophyd_async/epics/demo/__init__.py +11 -49
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +83 -38
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +68 -73
- ophyd_async/epics/testing/_utils.py +19 -44
- ophyd_async/epics/testing/test_records.db +16 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -8
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +12 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
- ophyd_async/fastcs/panda/_table.py +13 -7
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +16 -0
- ophyd_async/plan_stubs/_ensure_connected.py +12 -17
- ophyd_async/plan_stubs/_fly.py +3 -5
- ophyd_async/plan_stubs/_nd_attributes.py +9 -5
- ophyd_async/plan_stubs/_panda.py +14 -0
- ophyd_async/plan_stubs/_settings.py +152 -0
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +29 -0
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +21 -33
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +148 -74
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/demo/_counter.py +2 -0
- ophyd_async/tango/demo/_detector.py +2 -0
- ophyd_async/tango/demo/_mover.py +10 -6
- ophyd_async/tango/demo/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +48 -7
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +200 -96
- ophyd_async/testing/_mock_signal_utils.py +59 -73
- ophyd_async/testing/_one_of_everything.py +146 -0
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/demo/_mover.py +0 -95
- ophyd_async/epics/demo/_sensor.py +0 -37
- ophyd_async/epics/demo/sensor.db +0 -19
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
- ophyd_async/sim/demo/_sim_motor.py +0 -107
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
- ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
- {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,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()
|
ophyd_async/sim/__init__.py
CHANGED
|
@@ -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
|
+
)
|