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
ophyd_async/epics/core/_util.py
CHANGED
|
@@ -13,12 +13,12 @@ from ophyd_async.core import (
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
|
|
16
|
-
"""
|
|
17
|
-
|
|
16
|
+
"""Split PV into record name and field."""
|
|
18
17
|
if "." in pv:
|
|
19
|
-
|
|
18
|
+
record, field = pv.split(".", maxsplit=1)
|
|
20
19
|
else:
|
|
21
|
-
|
|
20
|
+
record, field = pv, None
|
|
21
|
+
return (record, field)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def get_supported_values(
|
|
@@ -47,7 +47,7 @@ def get_supported_values(
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def format_datatype(datatype: Any) -> str:
|
|
50
|
-
if get_origin(datatype) is np.ndarray and get_args(datatype)
|
|
50
|
+
if get_origin(datatype) is np.ndarray and get_args(datatype):
|
|
51
51
|
dtype = get_dtype(datatype)
|
|
52
52
|
return f"Array1D[np.{dtype.name}]"
|
|
53
53
|
elif get_origin(datatype) is Sequence:
|
|
@@ -1,54 +1,16 @@
|
|
|
1
|
-
"""Demo EPICS Devices for the tutorial"""
|
|
1
|
+
"""Demo EPICS Devices for the tutorial."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
from ._mover import Mover, SampleStage
|
|
11
|
-
from ._sensor import EnergyMode, Sensor, SensorGroup
|
|
3
|
+
from ._ioc import start_ioc_subprocess
|
|
4
|
+
from ._motor import DemoMotor
|
|
5
|
+
from ._point_detector import DemoPointDetector
|
|
6
|
+
from ._point_detector_channel import DemoPointDetectorChannel, EnergyMode
|
|
7
|
+
from ._stage import DemoStage
|
|
12
8
|
|
|
13
9
|
__all__ = [
|
|
14
|
-
"
|
|
15
|
-
"
|
|
10
|
+
"DemoMotor",
|
|
11
|
+
"DemoStage",
|
|
16
12
|
"EnergyMode",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
13
|
+
"DemoPointDetectorChannel",
|
|
14
|
+
"DemoPointDetector",
|
|
15
|
+
"start_ioc_subprocess",
|
|
19
16
|
]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def start_ioc_subprocess() -> str:
|
|
23
|
-
"""Start an IOC subprocess with EPICS database for sample stage and sensor
|
|
24
|
-
with the same pv prefix
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
pv_prefix = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + ":"
|
|
28
|
-
here = Path(__file__).absolute().parent
|
|
29
|
-
args = [sys.executable, "-m", "epicscorelibs.ioc"]
|
|
30
|
-
|
|
31
|
-
# Create standalone sensor
|
|
32
|
-
args += ["-m", f"P={pv_prefix}"]
|
|
33
|
-
args += ["-d", str(here / "sensor.db")]
|
|
34
|
-
|
|
35
|
-
# Create sensor group
|
|
36
|
-
for suffix in ["1", "2", "3"]:
|
|
37
|
-
args += ["-m", f"P={pv_prefix}{suffix}:"]
|
|
38
|
-
args += ["-d", str(here / "sensor.db")]
|
|
39
|
-
|
|
40
|
-
# Create X and Y motors
|
|
41
|
-
for suffix in ["X", "Y"]:
|
|
42
|
-
args += ["-m", f"P={pv_prefix}{suffix}:"]
|
|
43
|
-
args += ["-d", str(here / "mover.db")]
|
|
44
|
-
|
|
45
|
-
# Start IOC
|
|
46
|
-
process = subprocess.Popen(
|
|
47
|
-
args,
|
|
48
|
-
stdin=subprocess.PIPE,
|
|
49
|
-
stdout=subprocess.PIPE,
|
|
50
|
-
stderr=subprocess.STDOUT,
|
|
51
|
-
universal_newlines=True,
|
|
52
|
-
)
|
|
53
|
-
atexit.register(process.communicate, "exit")
|
|
54
|
-
return pv_prefix
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Used for tutorial `Implementing Devices`."""
|
|
2
|
+
|
|
3
|
+
# Import bluesky and ophyd
|
|
4
|
+
import bluesky.plan_stubs as bps # noqa: F401
|
|
5
|
+
import bluesky.plans as bp # noqa: F401
|
|
6
|
+
from bluesky.callbacks.best_effort import BestEffortCallback
|
|
7
|
+
from bluesky.run_engine import RunEngine, autoawait_in_bluesky_event_loop
|
|
8
|
+
|
|
9
|
+
from ophyd_async.core import init_devices
|
|
10
|
+
from ophyd_async.epics import demo, testing
|
|
11
|
+
|
|
12
|
+
# Create a run engine and make ipython use it for `await` commands
|
|
13
|
+
RE = RunEngine(call_returns_result=True)
|
|
14
|
+
autoawait_in_bluesky_event_loop()
|
|
15
|
+
|
|
16
|
+
# Add a callback for plotting
|
|
17
|
+
bec = BestEffortCallback()
|
|
18
|
+
RE.subscribe(bec)
|
|
19
|
+
|
|
20
|
+
# Start IOC with demo pvs in subprocess
|
|
21
|
+
prefix = testing.generate_random_pv_prefix()
|
|
22
|
+
ioc = demo.start_ioc_subprocess(prefix, num_channels=3)
|
|
23
|
+
|
|
24
|
+
# All Devices created within this block will be
|
|
25
|
+
# connected and named at the end of the with block
|
|
26
|
+
with init_devices():
|
|
27
|
+
# Create a sample stage with X and Y motors
|
|
28
|
+
stage = demo.DemoStage(f"{prefix}STAGE:")
|
|
29
|
+
# Create a multi channel counter with the same number
|
|
30
|
+
# of counters as the IOC
|
|
31
|
+
pdet = demo.DemoPointDetector(f"{prefix}DET:", num_channels=3)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ophyd_async.epics.testing import TestingIOC
|
|
5
|
+
|
|
6
|
+
HERE = Path(__file__).absolute().parent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def start_ioc_subprocess(prefix: str, num_channels: int) -> TestingIOC:
|
|
10
|
+
"""Start an IOC subprocess for sample stage and sensor.
|
|
11
|
+
|
|
12
|
+
:param prefix: The prefix for the IOC PVs.
|
|
13
|
+
:param num_channels: The number of point detector channels to create.
|
|
14
|
+
"""
|
|
15
|
+
ioc = TestingIOC()
|
|
16
|
+
# Create X and Y motors
|
|
17
|
+
for suffix in ["X", "Y"]:
|
|
18
|
+
ioc.add_database(HERE / "motor.db", P=f"{prefix}STAGE:{suffix}:")
|
|
19
|
+
# Create a multichannel counter with num_counters
|
|
20
|
+
ioc.add_database(HERE / "point_detector.db", P=f"{prefix}DET:")
|
|
21
|
+
for i in range(1, num_channels + 1):
|
|
22
|
+
ioc.add_database(
|
|
23
|
+
HERE / "point_detector_channel.db",
|
|
24
|
+
P=f"{prefix}DET:",
|
|
25
|
+
CHANNEL=str(i),
|
|
26
|
+
X=f"{prefix}STAGE:X:",
|
|
27
|
+
Y=f"{prefix}STAGE:Y:",
|
|
28
|
+
)
|
|
29
|
+
# Start IOC and register it to be stopped at exit
|
|
30
|
+
ioc.start()
|
|
31
|
+
atexit.register(ioc.stop)
|
|
32
|
+
return ioc
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Annotated as A
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from bluesky.protocols import Movable, Stoppable
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
CALCULATE_TIMEOUT,
|
|
9
|
+
DEFAULT_TIMEOUT,
|
|
10
|
+
CalculatableTimeout,
|
|
11
|
+
SignalR,
|
|
12
|
+
SignalRW,
|
|
13
|
+
SignalX,
|
|
14
|
+
StandardReadable,
|
|
15
|
+
WatchableAsyncStatus,
|
|
16
|
+
WatcherUpdate,
|
|
17
|
+
observe_value,
|
|
18
|
+
)
|
|
19
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
20
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DemoMotor(EpicsDevice, StandardReadable, Movable, Stoppable):
|
|
24
|
+
"""A demo movable that moves based on velocity."""
|
|
25
|
+
|
|
26
|
+
# Whether set() should complete successfully or not
|
|
27
|
+
_set_success = True
|
|
28
|
+
# Define some signals
|
|
29
|
+
readback: A[SignalR[float], PvSuffix("Readback"), Format.HINTED_SIGNAL]
|
|
30
|
+
velocity: A[SignalRW[float], PvSuffix("Velocity"), Format.CONFIG_SIGNAL]
|
|
31
|
+
units: A[SignalR[str], PvSuffix("Readback.EGU"), Format.CONFIG_SIGNAL]
|
|
32
|
+
setpoint: A[SignalRW[float], PvSuffix("Setpoint")]
|
|
33
|
+
precision: A[SignalR[int], PvSuffix("Readback.PREC")]
|
|
34
|
+
# If a signal name clashes with a bluesky verb add _ to the attribute name
|
|
35
|
+
stop_: A[SignalX, PvSuffix("Stop.PROC")]
|
|
36
|
+
|
|
37
|
+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
38
|
+
super().set_name(name, child_name_separator=child_name_separator)
|
|
39
|
+
# Readback should be named the same as its parent in read()
|
|
40
|
+
self.readback.set_name(name)
|
|
41
|
+
|
|
42
|
+
@WatchableAsyncStatus.wrap
|
|
43
|
+
async def set( # type: ignore
|
|
44
|
+
self, new_position: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
|
|
45
|
+
):
|
|
46
|
+
# The move should complete successfully unless stop(success=False) is called
|
|
47
|
+
self._set_success = True
|
|
48
|
+
# Get some variables for the progress bar reporting
|
|
49
|
+
old_position, units, precision, velocity = await asyncio.gather(
|
|
50
|
+
self.setpoint.get_value(),
|
|
51
|
+
self.units.get_value(),
|
|
52
|
+
self.precision.get_value(),
|
|
53
|
+
self.velocity.get_value(),
|
|
54
|
+
)
|
|
55
|
+
# If not supplied, calculate a suitable timeout for the move
|
|
56
|
+
if timeout == CALCULATE_TIMEOUT:
|
|
57
|
+
timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
|
|
58
|
+
# Wait for the value to set, but don't wait for put completion callback
|
|
59
|
+
await self.setpoint.set(new_position, wait=False)
|
|
60
|
+
# Observe the readback Signal, and on each new position...
|
|
61
|
+
async for current_position in observe_value(
|
|
62
|
+
self.readback, done_timeout=timeout
|
|
63
|
+
):
|
|
64
|
+
# Emit a progress bar update
|
|
65
|
+
yield WatcherUpdate(
|
|
66
|
+
current=current_position,
|
|
67
|
+
initial=old_position,
|
|
68
|
+
target=new_position,
|
|
69
|
+
name=self.name,
|
|
70
|
+
unit=units,
|
|
71
|
+
precision=precision,
|
|
72
|
+
)
|
|
73
|
+
# If we are at the desired position the break
|
|
74
|
+
if np.isclose(current_position, new_position):
|
|
75
|
+
break
|
|
76
|
+
# If we were told to stop and report an error then do so
|
|
77
|
+
if not self._set_success:
|
|
78
|
+
raise RuntimeError("Motor was stopped")
|
|
79
|
+
|
|
80
|
+
async def stop(self, success=True):
|
|
81
|
+
self._set_success = success
|
|
82
|
+
await self.stop_.trigger()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Annotated as A
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Triggerable
|
|
4
|
+
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
DEFAULT_TIMEOUT,
|
|
7
|
+
AsyncStatus,
|
|
8
|
+
DeviceVector,
|
|
9
|
+
SignalR,
|
|
10
|
+
SignalRW,
|
|
11
|
+
SignalX,
|
|
12
|
+
StandardReadable,
|
|
13
|
+
)
|
|
14
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
15
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
16
|
+
|
|
17
|
+
from ._point_detector_channel import DemoPointDetectorChannel
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DemoPointDetector(StandardReadable, EpicsDevice, Triggerable):
|
|
21
|
+
"""A demo detector that produces a point values based on X and Y motors."""
|
|
22
|
+
|
|
23
|
+
acquire_time: A[SignalRW[float], PvSuffix("AcquireTime"), Format.CONFIG_SIGNAL]
|
|
24
|
+
start: A[SignalX, PvSuffix("Start.PROC")]
|
|
25
|
+
acquiring: A[SignalR[bool], PvSuffix("Acquiring")]
|
|
26
|
+
reset: A[SignalX, PvSuffix("Reset.PROC")]
|
|
27
|
+
|
|
28
|
+
def __init__(self, prefix: str, num_channels: int = 3, name: str = "") -> None:
|
|
29
|
+
with self.add_children_as_readables():
|
|
30
|
+
self.channel = DeviceVector(
|
|
31
|
+
{
|
|
32
|
+
i: DemoPointDetectorChannel(f"{prefix}{i}:")
|
|
33
|
+
for i in range(1, num_channels + 1)
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
super().__init__(prefix=prefix, name=name)
|
|
37
|
+
|
|
38
|
+
@AsyncStatus.wrap
|
|
39
|
+
async def trigger(self):
|
|
40
|
+
await self.reset.trigger()
|
|
41
|
+
timeout = await self.acquire_time.get_value() + DEFAULT_TIMEOUT
|
|
42
|
+
await self.start.trigger(timeout=timeout)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Annotated as A
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import SignalR, SignalRW, StandardReadable, StrictEnum
|
|
4
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
5
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EnergyMode(StrictEnum):
|
|
9
|
+
"""Energy mode for `DemoPointDetectorChannel`."""
|
|
10
|
+
|
|
11
|
+
LOW = "Low Energy"
|
|
12
|
+
"""Low energy mode"""
|
|
13
|
+
|
|
14
|
+
HIGH = "High Energy"
|
|
15
|
+
"""High energy mode"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DemoPointDetectorChannel(StandardReadable, EpicsDevice):
|
|
19
|
+
"""A channel for `DemoPointDetector` with int value based on X and Y Motors."""
|
|
20
|
+
|
|
21
|
+
value: A[SignalR[int], PvSuffix("Value"), Format.HINTED_UNCACHED_SIGNAL]
|
|
22
|
+
mode: A[SignalRW[EnergyMode], PvSuffix("Mode"), Format.CONFIG_SIGNAL]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
|
+
|
|
3
|
+
from ._motor import DemoMotor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DemoStage(StandardReadable):
|
|
7
|
+
"""A simulated sample stage with X and Y movables."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, prefix: str, name="") -> None:
|
|
10
|
+
# Define some child Devices
|
|
11
|
+
with self.add_children_as_readables():
|
|
12
|
+
self.x = DemoMotor(prefix + "X:")
|
|
13
|
+
self.y = DemoMotor(prefix + "Y:")
|
|
14
|
+
# Set name of device and child devices
|
|
15
|
+
super().__init__(name=name)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
record(ao, "$(P)AcquireTime") {
|
|
2
|
+
field(DESC, "Time to acquire for")
|
|
3
|
+
field(VAL, "0.1")
|
|
4
|
+
field(OUT, "$(P)Start.DLY2")
|
|
5
|
+
field(PINI, "YES")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
record(seq, "$(P)Start") {
|
|
9
|
+
field(DESC, "Start sequence")
|
|
10
|
+
# Grab the start time
|
|
11
|
+
field(LNK0, "$(P)StartTime.PROC")
|
|
12
|
+
# Set it to be acquiring
|
|
13
|
+
field(LNK1, "$(P)Acquiring PP")
|
|
14
|
+
field(DO1, "1")
|
|
15
|
+
# Set it back to idle
|
|
16
|
+
field(LNK2, "$(P)Acquiring PP")
|
|
17
|
+
field(DO2, "0")
|
|
18
|
+
# Set the elapsed time to the full acquire time
|
|
19
|
+
field(LNK3, "$(P)Elapsed PP")
|
|
20
|
+
field(DOL3, "$(P)AcquireTime")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
record(ai, "$(P)StartTime") {
|
|
24
|
+
field(DTYP, "Soft Timestamp")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
record(bi, "$(P)Acquiring") {
|
|
28
|
+
field(DESC, "Currently acquiring")
|
|
29
|
+
field(ZNAM, "Idle")
|
|
30
|
+
field(ONAM, "Acquiring")
|
|
31
|
+
field(PINI, "YES")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
record(ai, "$(P)CurrentTime") {
|
|
35
|
+
field(DTYP, "Soft Timestamp")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
record(calcout, "$(P)Process") {
|
|
39
|
+
field(DESC, "Process elapsed time if acquiring")
|
|
40
|
+
field(INPA, "$(P)StartTime")
|
|
41
|
+
field(INPB, "$(P)CurrentTime PP")
|
|
42
|
+
field(SCAN, ".1 second")
|
|
43
|
+
field(CALC, "B-A")
|
|
44
|
+
field(OUT, "$(P)Elapsed PP")
|
|
45
|
+
field(SDIS, "$(P)Acquiring")
|
|
46
|
+
field(DISV, "0")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
record(ai, "$(P)Elapsed") {
|
|
50
|
+
field(DESC, "Elapsed time")
|
|
51
|
+
field(EGU, "s")
|
|
52
|
+
field(PREC, "1")
|
|
53
|
+
field(PINI, "YES")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
record(calcout, "$(P)Reset") {
|
|
57
|
+
field(OUT, "$(P)Elapsed PP")
|
|
58
|
+
field(CALC, "0")
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
record(mbbo, "$(P)$(CHANNEL):Mode") {
|
|
2
|
+
field(DESC, "Energy sensitivity of the image")
|
|
3
|
+
field(DTYP, "Raw Soft Channel")
|
|
4
|
+
field(PINI, "YES")
|
|
5
|
+
field(ZRVL, "10")
|
|
6
|
+
field(ZRST, "Low Energy")
|
|
7
|
+
field(ONVL, "100")
|
|
8
|
+
field(ONST, "High Energy")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
record(calc, "$(P)$(CHANNEL):Value") {
|
|
12
|
+
field(DESC, "Sensor value simulated from X and Y")
|
|
13
|
+
field(INPA, "$(X)Readback")
|
|
14
|
+
field(INPB, "$(Y)Readback")
|
|
15
|
+
field(INPC, "$(CHANNEL)")
|
|
16
|
+
field(INPD, "$(P)$(CHANNEL):Mode.RVAL")
|
|
17
|
+
field(INPE, "$(P)Elapsed CP")
|
|
18
|
+
field(CALC, "FLOOR((SIN(A)**C+COS(A*B+D)+2)*2500*E)")
|
|
19
|
+
field(EGU, "cts")
|
|
20
|
+
field(PREC, "0")
|
|
21
|
+
}
|
|
@@ -12,9 +12,7 @@ class EigerTriggerInfo(TriggerInfo):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class EigerDetector(StandardDetector):
|
|
15
|
-
"""
|
|
16
|
-
Ophyd-async implementation of an Eiger Detector.
|
|
17
|
-
"""
|
|
15
|
+
"""Ophyd-async implementation of an Eiger Detector."""
|
|
18
16
|
|
|
19
17
|
_controller: EigerController
|
|
20
18
|
_writer: Odin
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
|
+
AsyncStatus,
|
|
5
6
|
DetectorController,
|
|
6
7
|
DetectorTrigger,
|
|
7
8
|
TriggerInfo,
|
|
@@ -19,19 +20,24 @@ EIGER_TRIGGER_MODE_MAP = {
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class EigerController(DetectorController):
|
|
23
|
+
"""Controller for the Eiger detector."""
|
|
24
|
+
|
|
22
25
|
def __init__(
|
|
23
26
|
self,
|
|
24
27
|
driver: EigerDriverIO,
|
|
25
28
|
) -> None:
|
|
26
29
|
self._drv = driver
|
|
30
|
+
self._arm_status: AsyncStatus | None = None
|
|
27
31
|
|
|
28
32
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
29
33
|
# See https://media.dectris.com/filer_public/30/14/3014704e-5f3b-43ba-8ccf-8ef720e60d2a/240202_usermanual_eiger2.pdf
|
|
30
34
|
return 0.0001
|
|
31
35
|
|
|
32
36
|
async def set_energy(self, energy: float, tolerance: float = 0.1):
|
|
33
|
-
"""
|
|
34
|
-
|
|
37
|
+
"""Change photon energy if outside tolerance.
|
|
38
|
+
|
|
39
|
+
It takes some time so don't do it unless it is outside tolerance.
|
|
40
|
+
"""
|
|
35
41
|
current_energy = await self._drv.photon_energy.get_value()
|
|
36
42
|
if abs(current_energy - energy) > tolerance:
|
|
37
43
|
await self._drv.photon_energy.set(energy)
|
|
@@ -54,7 +60,7 @@ class EigerController(DetectorController):
|
|
|
54
60
|
|
|
55
61
|
async def arm(self):
|
|
56
62
|
# TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
|
|
57
|
-
self._arm_status = set_and_wait_for_other_value(
|
|
63
|
+
self._arm_status = await set_and_wait_for_other_value(
|
|
58
64
|
self._drv.arm,
|
|
59
65
|
1,
|
|
60
66
|
self._drv.state,
|
|
@@ -64,8 +70,9 @@ class EigerController(DetectorController):
|
|
|
64
70
|
)
|
|
65
71
|
|
|
66
72
|
async def wait_for_idle(self):
|
|
67
|
-
if self._arm_status:
|
|
73
|
+
if self._arm_status and not self._arm_status.done:
|
|
68
74
|
await self._arm_status
|
|
75
|
+
self._arm_status = None
|
|
69
76
|
|
|
70
77
|
async def disarm(self):
|
|
71
78
|
await self._drv.disarm.set(1)
|
|
@@ -9,6 +9,8 @@ class EigerTriggerMode(StrictEnum):
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class EigerDriverIO(Device):
|
|
12
|
+
"""Contains signals for handling IO on the Eiger detector."""
|
|
13
|
+
|
|
12
14
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
13
15
|
self.bit_depth = epics_signal_r(int, f"{prefix}BitDepthReadout")
|
|
14
16
|
self.stale_parameters = epics_signal_r(bool, f"{prefix}StaleParameters")
|
|
@@ -5,7 +5,6 @@ from bluesky.protocols import StreamAsset
|
|
|
5
5
|
from event_model import DataKey
|
|
6
6
|
|
|
7
7
|
from ophyd_async.core import (
|
|
8
|
-
DEFAULT_TIMEOUT,
|
|
9
8
|
DetectorWriter,
|
|
10
9
|
Device,
|
|
11
10
|
DeviceVector,
|
|
@@ -110,7 +109,7 @@ class OdinWriter(DetectorWriter):
|
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
async def observe_indices_written(
|
|
113
|
-
self, timeout
|
|
112
|
+
self, timeout: float
|
|
114
113
|
) -> AsyncGenerator[int, None]:
|
|
115
114
|
async for num_captured in observe_value(self._drv.num_captured, timeout):
|
|
116
115
|
yield num_captured
|