ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a2__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 +97 -62
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +106 -125
- ophyd_async/core/_device.py +69 -63
- ophyd_async/core/_device_filler.py +65 -1
- ophyd_async/core/_flyer.py +14 -5
- 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 +44 -35
- ophyd_async/core/_settings.py +36 -27
- ophyd_async/core/_signal.py +262 -170
- ophyd_async/core/_signal_backend.py +56 -13
- ophyd_async/core/_soft_signal_backend.py +16 -11
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +41 -11
- ophyd_async/core/_utils.py +96 -49
- ophyd_async/core/_yaml_settings.py +2 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/_andor.py +2 -2
- ophyd_async/epics/adandor/_andor_controller.py +4 -2
- ophyd_async/epics/adandor/_andor_io.py +2 -4
- ophyd_async/epics/adaravis/__init__.py +5 -0
- ophyd_async/epics/adaravis/_aravis.py +4 -8
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +23 -8
- ophyd_async/epics/adcore/_core_detector.py +42 -2
- ophyd_async/epics/adcore/_core_io.py +124 -99
- ophyd_async/epics/adcore/_core_logic.py +106 -27
- ophyd_async/epics/adcore/_core_writer.py +12 -8
- ophyd_async/epics/adcore/_hdf_writer.py +21 -38
- ophyd_async/epics/adcore/_single_trigger.py +2 -2
- ophyd_async/epics/adcore/_utils.py +2 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +3 -3
- ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +5 -0
- ophyd_async/epics/adpilatus/_pilatus.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +4 -14
- ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +3 -2
- ophyd_async/epics/advimba/_vimba_controller.py +4 -2
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +35 -16
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +10 -2
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +4 -4
- ophyd_async/epics/demo/__init__.py +16 -0
- 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/{sim/mover.db → demo/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 +65 -28
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/_example_ioc.py +21 -9
- ophyd_async/epics/testing/_utils.py +3 -0
- ophyd_async/epics/testing/test_records.db +8 -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 -6
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +5 -0
- ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
- ophyd_async/fastcs/panda/_table.py +9 -6
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +2 -0
- ophyd_async/plan_stubs/_ensure_connected.py +1 -0
- ophyd_async/plan_stubs/_fly.py +2 -4
- ophyd_async/plan_stubs/_nd_attributes.py +2 -0
- ophyd_async/plan_stubs/_panda.py +1 -0
- ophyd_async/plan_stubs/_settings.py +43 -16
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
- ophyd_async/sim/__init__.py +24 -14
- 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 +18 -32
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +136 -60
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/{sim → demo}/_counter.py +2 -0
- ophyd_async/tango/{sim → demo}/_detector.py +2 -0
- ophyd_async/tango/{sim → demo}/_mover.py +5 -4
- ophyd_async/tango/{sim → 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 +29 -7
- ophyd_async/testing/_assert.py +145 -83
- ophyd_async/testing/_mock_signal_utils.py +56 -70
- ophyd_async/testing/_one_of_everything.py +41 -21
- ophyd_async/testing/_single_derived.py +89 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/sim/__init__.py +0 -54
- ophyd_async/epics/sim/_ioc.py +0 -29
- ophyd_async/epics/sim/_mover.py +0 -101
- ophyd_async/epics/sim/_sensor.py +0 -37
- ophyd_async/epics/sim/sensor.db +0 -19
- ophyd_async/sim/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
- ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
- ophyd_async/sim/_sim_motor.py +0 -107
- ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
- /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
- /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/top_level.txt +0 -0
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"""Demo EPICS Devices for the tutorial"""
|
|
2
|
-
|
|
3
|
-
import atexit
|
|
4
|
-
import random
|
|
5
|
-
import string
|
|
6
|
-
import subprocess
|
|
7
|
-
import sys
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
from ._mover import Mover, SampleStage
|
|
11
|
-
from ._sensor import EnergyMode, Sensor, SensorGroup
|
|
12
|
-
|
|
13
|
-
__all__ = [
|
|
14
|
-
"Mover",
|
|
15
|
-
"SampleStage",
|
|
16
|
-
"EnergyMode",
|
|
17
|
-
"Sensor",
|
|
18
|
-
"SensorGroup",
|
|
19
|
-
]
|
|
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
|
ophyd_async/epics/sim/_ioc.py
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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_counters: int):
|
|
10
|
-
"""Start an IOC subprocess with EPICS database for sample stage and sensor
|
|
11
|
-
with the same pv prefix
|
|
12
|
-
"""
|
|
13
|
-
ioc = TestingIOC()
|
|
14
|
-
# Create X and Y motors
|
|
15
|
-
for suffix in ["X", "Y"]:
|
|
16
|
-
ioc.add_database(HERE / "mover.db", P=f"{prefix}STAGE:{suffix}:")
|
|
17
|
-
# Create a multichannel counter with num_counters
|
|
18
|
-
ioc.add_database(HERE / "multichannelcounter.db", P=f"{prefix}MCC:")
|
|
19
|
-
for i in range(1, num_counters + 1):
|
|
20
|
-
ioc.add_database(
|
|
21
|
-
HERE / "counter.db",
|
|
22
|
-
P=f"{prefix}MCC:",
|
|
23
|
-
CHANNEL=str(i),
|
|
24
|
-
X=f"{prefix}STAGE:X:",
|
|
25
|
-
Y=f"{prefix}STAGE:Y:",
|
|
26
|
-
)
|
|
27
|
-
# Start IOC and register it to be stopped at exit
|
|
28
|
-
ioc.start()
|
|
29
|
-
atexit.register(ioc.stop)
|
ophyd_async/epics/sim/_mover.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
from bluesky.protocols import Movable, Stoppable
|
|
5
|
-
|
|
6
|
-
from ophyd_async.core import (
|
|
7
|
-
CALCULATE_TIMEOUT,
|
|
8
|
-
DEFAULT_TIMEOUT,
|
|
9
|
-
AsyncStatus,
|
|
10
|
-
CalculatableTimeout,
|
|
11
|
-
Device,
|
|
12
|
-
StandardReadable,
|
|
13
|
-
WatchableAsyncStatus,
|
|
14
|
-
WatcherUpdate,
|
|
15
|
-
observe_value,
|
|
16
|
-
)
|
|
17
|
-
from ophyd_async.core import StandardReadableFormat as Format
|
|
18
|
-
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class Mover(StandardReadable, Movable, Stoppable):
|
|
22
|
-
"""A demo movable that moves based on velocity"""
|
|
23
|
-
|
|
24
|
-
def __init__(self, prefix: str, name="") -> None:
|
|
25
|
-
# Define some signals
|
|
26
|
-
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
27
|
-
self.readback = epics_signal_r(float, prefix + "Readback")
|
|
28
|
-
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
29
|
-
self.velocity = epics_signal_rw(float, prefix + "Velocity")
|
|
30
|
-
self.units = epics_signal_r(str, prefix + "Readback.EGU")
|
|
31
|
-
self.setpoint = epics_signal_rw(float, prefix + "Setpoint")
|
|
32
|
-
self.precision = epics_signal_r(int, prefix + "Readback.PREC")
|
|
33
|
-
# Signals that collide with standard methods should have a trailing underscore
|
|
34
|
-
self.stop_ = epics_signal_x(prefix + "Stop.PROC")
|
|
35
|
-
# Whether set() should complete successfully or not
|
|
36
|
-
self._set_success = True
|
|
37
|
-
|
|
38
|
-
super().__init__(name=name)
|
|
39
|
-
|
|
40
|
-
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
41
|
-
super().set_name(name, child_name_separator=child_name_separator)
|
|
42
|
-
# Readback should be named the same as its parent in read()
|
|
43
|
-
self.readback.set_name(name)
|
|
44
|
-
|
|
45
|
-
@WatchableAsyncStatus.wrap
|
|
46
|
-
async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
|
|
47
|
-
new_position = value
|
|
48
|
-
self._set_success = True
|
|
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 timeout is CALCULATE_TIMEOUT:
|
|
56
|
-
try:
|
|
57
|
-
timeout = (
|
|
58
|
-
abs((new_position - old_position) / velocity) + DEFAULT_TIMEOUT
|
|
59
|
-
)
|
|
60
|
-
except ZeroDivisionError as error:
|
|
61
|
-
msg = "Mover has zero velocity"
|
|
62
|
-
raise ValueError(msg) from error
|
|
63
|
-
|
|
64
|
-
# Make an Event that will be set on completion, and a Status that will
|
|
65
|
-
# error if not done in time
|
|
66
|
-
done = asyncio.Event()
|
|
67
|
-
done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout)) # type: ignore
|
|
68
|
-
# Wait for the value to set, but don't wait for put completion callback
|
|
69
|
-
await self.setpoint.set(new_position, wait=False)
|
|
70
|
-
async for current_position in observe_value(
|
|
71
|
-
self.readback, done_status=done_status
|
|
72
|
-
):
|
|
73
|
-
yield WatcherUpdate(
|
|
74
|
-
current=current_position,
|
|
75
|
-
initial=old_position,
|
|
76
|
-
target=new_position,
|
|
77
|
-
name=self.name,
|
|
78
|
-
unit=units,
|
|
79
|
-
precision=precision,
|
|
80
|
-
)
|
|
81
|
-
if np.isclose(current_position, new_position):
|
|
82
|
-
done.set()
|
|
83
|
-
break
|
|
84
|
-
if not self._set_success:
|
|
85
|
-
raise RuntimeError("Motor was stopped")
|
|
86
|
-
|
|
87
|
-
async def stop(self, success=True):
|
|
88
|
-
self._set_success = success
|
|
89
|
-
status = self.stop_.trigger()
|
|
90
|
-
await status
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class SampleStage(Device):
|
|
94
|
-
"""A demo sample stage with X and Y movables"""
|
|
95
|
-
|
|
96
|
-
def __init__(self, prefix: str, name="") -> None:
|
|
97
|
-
# Define some child Devices
|
|
98
|
-
self.x = Mover(prefix + "X:")
|
|
99
|
-
self.y = Mover(prefix + "Y:")
|
|
100
|
-
# Set name of device and child devices
|
|
101
|
-
super().__init__(name=name)
|
ophyd_async/epics/sim/_sensor.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from typing import Annotated as A
|
|
2
|
-
|
|
3
|
-
from ophyd_async.core import (
|
|
4
|
-
DeviceVector,
|
|
5
|
-
SignalR,
|
|
6
|
-
SignalRW,
|
|
7
|
-
StandardReadable,
|
|
8
|
-
StrictEnum,
|
|
9
|
-
)
|
|
10
|
-
from ophyd_async.core import StandardReadableFormat as Format
|
|
11
|
-
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class EnergyMode(StrictEnum):
|
|
15
|
-
"""Energy mode for `Sensor`"""
|
|
16
|
-
|
|
17
|
-
#: Low energy mode
|
|
18
|
-
LOW = "Low Energy"
|
|
19
|
-
#: High energy mode
|
|
20
|
-
HIGH = "High Energy"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class Sensor(StandardReadable, EpicsDevice):
|
|
24
|
-
"""A demo sensor that produces a scalar value based on X and Y Movers"""
|
|
25
|
-
|
|
26
|
-
value: A[SignalR[float], PvSuffix("Value"), Format.HINTED_SIGNAL]
|
|
27
|
-
mode: A[SignalRW[EnergyMode], PvSuffix("Mode"), Format.CONFIG_SIGNAL]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class SensorGroup(StandardReadable):
|
|
31
|
-
def __init__(self, prefix: str, name: str = "", sensor_count: int = 3) -> None:
|
|
32
|
-
with self.add_children_as_readables():
|
|
33
|
-
self.sensors = DeviceVector(
|
|
34
|
-
{i: Sensor(f"{prefix}{i}:") for i in range(1, sensor_count + 1)}
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
super().__init__(name)
|
ophyd_async/epics/sim/sensor.db
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
record(mbbo, "$(P)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)Value") {
|
|
12
|
-
field(DESC, "Sensor value simulated from X and Y")
|
|
13
|
-
field(INPA, "$(P)X:Readback CP")
|
|
14
|
-
field(INPB, "$(P)Y:Readback CP")
|
|
15
|
-
field(INPC, "$(P)Mode.RVAL CP")
|
|
16
|
-
field(CALC, "SIN(A)**10+COS(C+B*A)*COS(A)")
|
|
17
|
-
field(EGU, "$(EGU=cts/s)")
|
|
18
|
-
field(PREC, "$(PREC=3)")
|
|
19
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from ._pattern_detector import PatternDetector
|
|
2
|
-
from ._pattern_detector_controller import PatternDetectorController
|
|
3
|
-
from ._pattern_detector_writer import PatternDetectorWriter
|
|
4
|
-
from ._pattern_generator import DATA_PATH, SUM_PATH, PatternGenerator
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"PatternDetector",
|
|
8
|
-
"PatternDetectorController",
|
|
9
|
-
"PatternDetectorWriter",
|
|
10
|
-
"DATA_PATH",
|
|
11
|
-
"SUM_PATH",
|
|
12
|
-
"PatternGenerator",
|
|
13
|
-
]
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
from collections.abc import Sequence
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from ophyd_async.core import (
|
|
5
|
-
FilenameProvider,
|
|
6
|
-
PathProvider,
|
|
7
|
-
SignalR,
|
|
8
|
-
StandardDetector,
|
|
9
|
-
StaticFilenameProvider,
|
|
10
|
-
StaticPathProvider,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
from ._pattern_detector_controller import PatternDetectorController
|
|
14
|
-
from ._pattern_detector_writer import PatternDetectorWriter
|
|
15
|
-
from ._pattern_generator import PatternGenerator
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class PatternDetector(StandardDetector):
|
|
19
|
-
def __init__(
|
|
20
|
-
self,
|
|
21
|
-
path: Path,
|
|
22
|
-
config_sigs: Sequence[SignalR] = (),
|
|
23
|
-
name: str = "",
|
|
24
|
-
) -> None:
|
|
25
|
-
fp: FilenameProvider = StaticFilenameProvider(name)
|
|
26
|
-
self.path_provider: PathProvider = StaticPathProvider(fp, path)
|
|
27
|
-
self.pattern_generator = PatternGenerator()
|
|
28
|
-
writer = PatternDetectorWriter(
|
|
29
|
-
pattern_generator=self.pattern_generator,
|
|
30
|
-
path_provider=self.path_provider,
|
|
31
|
-
name_provider=lambda: self.name,
|
|
32
|
-
)
|
|
33
|
-
controller = PatternDetectorController(
|
|
34
|
-
pattern_generator=self.pattern_generator,
|
|
35
|
-
path_provider=self.path_provider,
|
|
36
|
-
)
|
|
37
|
-
super().__init__(
|
|
38
|
-
controller=controller,
|
|
39
|
-
writer=writer,
|
|
40
|
-
config_sigs=config_sigs,
|
|
41
|
-
name=name,
|
|
42
|
-
)
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from ophyd_async.core import DetectorController, PathProvider, TriggerInfo
|
|
4
|
-
|
|
5
|
-
from ._pattern_generator import PatternGenerator
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class PatternDetectorController(DetectorController):
|
|
9
|
-
def __init__(
|
|
10
|
-
self,
|
|
11
|
-
pattern_generator: PatternGenerator,
|
|
12
|
-
path_provider: PathProvider,
|
|
13
|
-
exposure: float = 0.1,
|
|
14
|
-
) -> None:
|
|
15
|
-
self.pattern_generator: PatternGenerator = pattern_generator
|
|
16
|
-
self.pattern_generator.set_exposure(exposure)
|
|
17
|
-
self.path_provider: PathProvider = path_provider
|
|
18
|
-
self.task: asyncio.Task | None = None
|
|
19
|
-
super().__init__()
|
|
20
|
-
|
|
21
|
-
async def prepare(self, trigger_info: TriggerInfo):
|
|
22
|
-
self._trigger_info = trigger_info
|
|
23
|
-
if self._trigger_info.livetime is None:
|
|
24
|
-
self._trigger_info.livetime = 0.01
|
|
25
|
-
self.period: float = self._trigger_info.livetime + self.get_deadtime(
|
|
26
|
-
trigger_info.livetime
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
async def arm(self):
|
|
30
|
-
if not hasattr(self, "_trigger_info"):
|
|
31
|
-
msg = "TriggerInfo information is missing, has 'prepare' been called?"
|
|
32
|
-
raise RuntimeError(msg)
|
|
33
|
-
if not self._trigger_info.livetime:
|
|
34
|
-
msg = "Livetime information is missing in trigger info"
|
|
35
|
-
raise ValueError(msg)
|
|
36
|
-
if not self.period:
|
|
37
|
-
msg = "Period is not set"
|
|
38
|
-
raise ValueError(msg)
|
|
39
|
-
self.task = asyncio.create_task(
|
|
40
|
-
self._coroutine_for_image_writing(
|
|
41
|
-
self._trigger_info.livetime,
|
|
42
|
-
self.period,
|
|
43
|
-
self._trigger_info.total_number_of_triggers,
|
|
44
|
-
)
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
async def wait_for_idle(self):
|
|
48
|
-
if self.task:
|
|
49
|
-
await self.task
|
|
50
|
-
|
|
51
|
-
async def disarm(self):
|
|
52
|
-
if self.task and not self.task.done():
|
|
53
|
-
self.task.cancel()
|
|
54
|
-
try:
|
|
55
|
-
await self.task
|
|
56
|
-
except asyncio.CancelledError:
|
|
57
|
-
pass
|
|
58
|
-
self.task = None
|
|
59
|
-
|
|
60
|
-
def get_deadtime(self, exposure: float | None) -> float:
|
|
61
|
-
return 0.001
|
|
62
|
-
|
|
63
|
-
async def _coroutine_for_image_writing(
|
|
64
|
-
self, exposure: float, period: float, frames_number: int
|
|
65
|
-
):
|
|
66
|
-
for _ in range(frames_number):
|
|
67
|
-
self.pattern_generator.set_exposure(exposure)
|
|
68
|
-
await asyncio.sleep(period)
|
|
69
|
-
await self.pattern_generator.write_image_to_file()
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from collections.abc import AsyncGenerator, AsyncIterator
|
|
2
|
-
|
|
3
|
-
from event_model import DataKey
|
|
4
|
-
|
|
5
|
-
from ophyd_async.core import DEFAULT_TIMEOUT, DetectorWriter, NameProvider, PathProvider
|
|
6
|
-
|
|
7
|
-
from ._pattern_generator import PatternGenerator
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class PatternDetectorWriter(DetectorWriter):
|
|
11
|
-
pattern_generator: PatternGenerator
|
|
12
|
-
|
|
13
|
-
def __init__(
|
|
14
|
-
self,
|
|
15
|
-
pattern_generator: PatternGenerator,
|
|
16
|
-
path_provider: PathProvider,
|
|
17
|
-
name_provider: NameProvider,
|
|
18
|
-
) -> None:
|
|
19
|
-
self.pattern_generator = pattern_generator
|
|
20
|
-
self.path_provider = path_provider
|
|
21
|
-
self.name_provider = name_provider
|
|
22
|
-
|
|
23
|
-
async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
|
|
24
|
-
return await self.pattern_generator.open_file(
|
|
25
|
-
self.path_provider, self.name_provider(), multiplier
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
async def close(self) -> None:
|
|
29
|
-
self.pattern_generator.close()
|
|
30
|
-
|
|
31
|
-
def collect_stream_docs(self, indices_written: int) -> AsyncIterator:
|
|
32
|
-
return self.pattern_generator.collect_stream_docs(indices_written)
|
|
33
|
-
|
|
34
|
-
async def observe_indices_written(
|
|
35
|
-
self, timeout=DEFAULT_TIMEOUT
|
|
36
|
-
) -> AsyncGenerator[int, None]:
|
|
37
|
-
async for index in self.pattern_generator.observe_indices_written(timeout):
|
|
38
|
-
yield index
|
|
39
|
-
|
|
40
|
-
async def get_indices_written(self) -> int:
|
|
41
|
-
return self.pattern_generator.image_counter
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
from collections.abc import AsyncGenerator, AsyncIterator
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import h5py
|
|
5
|
-
import numpy as np
|
|
6
|
-
from bluesky.protocols import StreamAsset
|
|
7
|
-
from event_model import DataKey
|
|
8
|
-
|
|
9
|
-
from ophyd_async.core import (
|
|
10
|
-
DEFAULT_TIMEOUT,
|
|
11
|
-
HDFDataset,
|
|
12
|
-
HDFFile,
|
|
13
|
-
PathProvider,
|
|
14
|
-
observe_value,
|
|
15
|
-
soft_signal_r_and_setter,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
# raw data path
|
|
19
|
-
DATA_PATH = "/entry/data/data"
|
|
20
|
-
|
|
21
|
-
# pixel sum path
|
|
22
|
-
SUM_PATH = "/entry/sum"
|
|
23
|
-
|
|
24
|
-
MAX_UINT8_VALUE = np.iinfo(np.uint8).max
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def generate_gaussian_blob(height: int, width: int) -> np.ndarray:
|
|
28
|
-
"""Make a Gaussian Blob with float values in range 0..1"""
|
|
29
|
-
x, y = np.meshgrid(np.linspace(-1, 1, width), np.linspace(-1, 1, height))
|
|
30
|
-
d = np.sqrt(x * x + y * y)
|
|
31
|
-
blob = np.exp(-(d**2))
|
|
32
|
-
return blob
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def generate_interesting_pattern(x: float, y: float) -> float:
|
|
36
|
-
"""This function is interesting in x and y in range -10..10, returning
|
|
37
|
-
a float value in range 0..1
|
|
38
|
-
"""
|
|
39
|
-
z = 0.5 + (np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)) / 2
|
|
40
|
-
return z
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class PatternGenerator:
|
|
44
|
-
def __init__(
|
|
45
|
-
self,
|
|
46
|
-
saturation_exposure_time: float = 0.1,
|
|
47
|
-
detector_width: int = 320,
|
|
48
|
-
detector_height: int = 240,
|
|
49
|
-
) -> None:
|
|
50
|
-
self.saturation_exposure_time = saturation_exposure_time
|
|
51
|
-
self.exposure = saturation_exposure_time
|
|
52
|
-
self.x = 0.0
|
|
53
|
-
self.y = 0.0
|
|
54
|
-
self.height = detector_height
|
|
55
|
-
self.width = detector_width
|
|
56
|
-
self.image_counter: int = 0
|
|
57
|
-
|
|
58
|
-
# it automatically initializes to 0
|
|
59
|
-
self.counter_signal, self._set_counter_signal = soft_signal_r_and_setter(int)
|
|
60
|
-
self._full_intensity_blob = (
|
|
61
|
-
generate_gaussian_blob(width=detector_width, height=detector_height)
|
|
62
|
-
* MAX_UINT8_VALUE
|
|
63
|
-
)
|
|
64
|
-
self._hdf_stream_provider: HDFFile | None = None
|
|
65
|
-
self._handle_for_h5_file: h5py.File | None = None
|
|
66
|
-
self.target_path: Path | None = None
|
|
67
|
-
|
|
68
|
-
def write_data_to_dataset(self, path: str, data_shape: tuple[int, ...], data):
|
|
69
|
-
"""Write data to named dataset, resizing to fit and flushing after."""
|
|
70
|
-
if not self._handle_for_h5_file:
|
|
71
|
-
msg = "No file has been opened!"
|
|
72
|
-
raise OSError(msg)
|
|
73
|
-
|
|
74
|
-
dset = self._handle_for_h5_file[path]
|
|
75
|
-
if not isinstance(dset, h5py.Dataset):
|
|
76
|
-
msg = f"Expected {path} to be a dataset, got {type(dset).__name__}"
|
|
77
|
-
raise TypeError(msg)
|
|
78
|
-
dset.resize((self.image_counter + 1,) + data_shape)
|
|
79
|
-
dset[self.image_counter] = data
|
|
80
|
-
dset.flush()
|
|
81
|
-
|
|
82
|
-
async def write_image_to_file(self) -> None:
|
|
83
|
-
# generate the simulated data
|
|
84
|
-
intensity: float = generate_interesting_pattern(self.x, self.y)
|
|
85
|
-
detector_data = (
|
|
86
|
-
self._full_intensity_blob
|
|
87
|
-
* intensity
|
|
88
|
-
* self.exposure
|
|
89
|
-
/ self.saturation_exposure_time
|
|
90
|
-
).astype(np.uint8)
|
|
91
|
-
|
|
92
|
-
# Write the data and sum
|
|
93
|
-
self.write_data_to_dataset(DATA_PATH, (self.height, self.width), detector_data)
|
|
94
|
-
self.write_data_to_dataset(SUM_PATH, (), np.sum(detector_data))
|
|
95
|
-
|
|
96
|
-
# counter increment is last
|
|
97
|
-
# as only at this point the new data is visible from the outside
|
|
98
|
-
self.image_counter += 1
|
|
99
|
-
self._set_counter_signal(self.image_counter)
|
|
100
|
-
|
|
101
|
-
def set_exposure(self, value: float) -> None:
|
|
102
|
-
self.exposure = value
|
|
103
|
-
|
|
104
|
-
def set_x(self, value: float) -> None:
|
|
105
|
-
self.x = value
|
|
106
|
-
|
|
107
|
-
def set_y(self, value: float) -> None:
|
|
108
|
-
self.y = value
|
|
109
|
-
|
|
110
|
-
async def open_file(
|
|
111
|
-
self, path_provider: PathProvider, name: str, multiplier: int = 1
|
|
112
|
-
) -> dict[str, DataKey]:
|
|
113
|
-
await self.counter_signal.connect()
|
|
114
|
-
|
|
115
|
-
self.target_path = self._get_new_path(path_provider)
|
|
116
|
-
self._path_provider = path_provider
|
|
117
|
-
|
|
118
|
-
self._handle_for_h5_file = h5py.File(self.target_path, "w", libver="latest")
|
|
119
|
-
|
|
120
|
-
if not self._handle_for_h5_file:
|
|
121
|
-
msg = f"Problem opening file {self.target_path}"
|
|
122
|
-
raise OSError(msg)
|
|
123
|
-
|
|
124
|
-
self._handle_for_h5_file.create_dataset(
|
|
125
|
-
name=DATA_PATH,
|
|
126
|
-
shape=(0, self.height, self.width),
|
|
127
|
-
dtype=np.uint8,
|
|
128
|
-
maxshape=(None, self.height, self.width),
|
|
129
|
-
)
|
|
130
|
-
self._handle_for_h5_file.create_dataset(
|
|
131
|
-
name=SUM_PATH,
|
|
132
|
-
shape=(0,),
|
|
133
|
-
dtype=np.float64,
|
|
134
|
-
maxshape=(None,),
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
# once datasets written, can switch the model to single writer multiple reader
|
|
138
|
-
self._handle_for_h5_file.swmr_mode = True
|
|
139
|
-
self.multiplier = multiplier
|
|
140
|
-
|
|
141
|
-
outer_shape = (multiplier,) if multiplier > 1 else ()
|
|
142
|
-
|
|
143
|
-
# cache state to self
|
|
144
|
-
# Add the main data
|
|
145
|
-
self._datasets = [
|
|
146
|
-
HDFDataset(
|
|
147
|
-
data_key=name,
|
|
148
|
-
dataset=DATA_PATH,
|
|
149
|
-
shape=(self.height, self.width),
|
|
150
|
-
multiplier=multiplier,
|
|
151
|
-
),
|
|
152
|
-
HDFDataset(
|
|
153
|
-
f"{name}-sum",
|
|
154
|
-
dataset=SUM_PATH,
|
|
155
|
-
shape=(),
|
|
156
|
-
multiplier=multiplier,
|
|
157
|
-
),
|
|
158
|
-
]
|
|
159
|
-
|
|
160
|
-
describe = {
|
|
161
|
-
ds.data_key: DataKey(
|
|
162
|
-
source="sim://pattern-generator-hdf-file",
|
|
163
|
-
shape=list(outer_shape) + list(ds.shape),
|
|
164
|
-
dtype="array" if ds.shape else "number",
|
|
165
|
-
external="STREAM:",
|
|
166
|
-
)
|
|
167
|
-
for ds in self._datasets
|
|
168
|
-
}
|
|
169
|
-
return describe
|
|
170
|
-
|
|
171
|
-
def _get_new_path(self, path_provider: PathProvider) -> Path:
|
|
172
|
-
info = path_provider(device_name="pattern")
|
|
173
|
-
new_path: Path = info.directory_path / info.filename
|
|
174
|
-
return new_path
|
|
175
|
-
|
|
176
|
-
async def collect_stream_docs(
|
|
177
|
-
self, indices_written: int
|
|
178
|
-
) -> AsyncIterator[StreamAsset]:
|
|
179
|
-
"""
|
|
180
|
-
stream resource says "here is a dataset",
|
|
181
|
-
stream datum says "here are N frames in that stream resource",
|
|
182
|
-
you get one stream resource and many stream datums per scan
|
|
183
|
-
"""
|
|
184
|
-
if self._handle_for_h5_file:
|
|
185
|
-
self._handle_for_h5_file.flush()
|
|
186
|
-
# when already something was written to the file
|
|
187
|
-
if indices_written:
|
|
188
|
-
# if no frames arrived yet, there's no file to speak of
|
|
189
|
-
# cannot get the full filename the HDF writer will write
|
|
190
|
-
# until the first frame comes in
|
|
191
|
-
if not self._hdf_stream_provider:
|
|
192
|
-
if self.target_path is None:
|
|
193
|
-
msg = "open file has not been called"
|
|
194
|
-
raise RuntimeError(msg)
|
|
195
|
-
self._hdf_stream_provider = HDFFile(
|
|
196
|
-
self.target_path,
|
|
197
|
-
self._datasets,
|
|
198
|
-
)
|
|
199
|
-
for doc in self._hdf_stream_provider.stream_resources():
|
|
200
|
-
yield "stream_resource", doc
|
|
201
|
-
if self._hdf_stream_provider:
|
|
202
|
-
for doc in self._hdf_stream_provider.stream_data(indices_written):
|
|
203
|
-
yield "stream_datum", doc
|
|
204
|
-
|
|
205
|
-
def close(self) -> None:
|
|
206
|
-
if self._handle_for_h5_file:
|
|
207
|
-
self._handle_for_h5_file.close()
|
|
208
|
-
self._handle_for_h5_file = None
|
|
209
|
-
|
|
210
|
-
async def observe_indices_written(
|
|
211
|
-
self, timeout=DEFAULT_TIMEOUT
|
|
212
|
-
) -> AsyncGenerator[int, None]:
|
|
213
|
-
async for num_captured in observe_value(self.counter_signal, timeout=timeout):
|
|
214
|
-
yield num_captured // self.multiplier
|