ophyd-async 0.9.0a2__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 +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 +37 -8
- 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 +137 -81
- ophyd_async/testing/_mock_signal_utils.py +56 -70
- ophyd_async/testing/_one_of_everything.py +41 -21
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a2.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.0a2.dist-info → ophyd_async-0.10.0a1.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.0a1.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from bluesky.protocols import Location, Reading, Stoppable, Subscribable
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from ophyd_async.core import (
|
|
10
|
+
AsyncStatus,
|
|
11
|
+
Callback,
|
|
12
|
+
StandardReadable,
|
|
13
|
+
WatchableAsyncStatus,
|
|
14
|
+
WatcherUpdate,
|
|
15
|
+
observe_value,
|
|
16
|
+
soft_signal_r_and_setter,
|
|
17
|
+
soft_signal_rw,
|
|
18
|
+
)
|
|
19
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FlySimMotorInfo(BaseModel):
|
|
23
|
+
"""Minimal set of information required to fly a [](#SimMotor)."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(frozen=True)
|
|
26
|
+
|
|
27
|
+
cv_start: float
|
|
28
|
+
"""Absolute position of the motor once it finishes accelerating to desired
|
|
29
|
+
velocity, in motor EGUs"""
|
|
30
|
+
|
|
31
|
+
cv_end: float
|
|
32
|
+
"""Absolute position of the motor once it begins decelerating from desired
|
|
33
|
+
velocity, in EGUs"""
|
|
34
|
+
|
|
35
|
+
cv_time: float = Field(gt=0)
|
|
36
|
+
"""Time taken for the motor to get from start_position to end_position, excluding
|
|
37
|
+
run-up and run-down, in seconds."""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def velocity(self) -> float:
|
|
41
|
+
"""Calculate the velocity of the constant velocity phase."""
|
|
42
|
+
return (self.cv_end - self.cv_start) / self.cv_time
|
|
43
|
+
|
|
44
|
+
def start_position(self, acceleration_time: float) -> float:
|
|
45
|
+
"""Calculate the start position with run-up distance added on."""
|
|
46
|
+
return self.cv_start - acceleration_time * self.velocity / 2
|
|
47
|
+
|
|
48
|
+
def end_position(self, acceleration_time: float) -> float:
|
|
49
|
+
"""Calculate the end position with run-down distance added on."""
|
|
50
|
+
return self.cv_end + acceleration_time * self.velocity / 2
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SimMotor(StandardReadable, Stoppable, Subscribable[float]):
|
|
54
|
+
"""For usage when simulating a motor."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, name="", instant=True) -> None:
|
|
57
|
+
"""Simulate a motor, with optional velocity.
|
|
58
|
+
|
|
59
|
+
:param name: name of device
|
|
60
|
+
:param instant: whether to move instantly or calculate move time using velocity
|
|
61
|
+
"""
|
|
62
|
+
# Define some signals
|
|
63
|
+
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
64
|
+
self.user_readback, self._user_readback_set = soft_signal_r_and_setter(
|
|
65
|
+
float, 0
|
|
66
|
+
)
|
|
67
|
+
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
68
|
+
self.velocity = soft_signal_rw(float, 0 if instant else 1.0)
|
|
69
|
+
self.acceleration_time = soft_signal_rw(float, 0.5)
|
|
70
|
+
self.units = soft_signal_rw(str, "mm")
|
|
71
|
+
self.user_setpoint = soft_signal_rw(float, 0)
|
|
72
|
+
|
|
73
|
+
# Whether set() should complete successfully or not
|
|
74
|
+
self._set_success = True
|
|
75
|
+
self._move_status: AsyncStatus | None = None
|
|
76
|
+
# Stored in prepare
|
|
77
|
+
self._fly_info: FlySimMotorInfo | None = None
|
|
78
|
+
# Set on kickoff(), complete when motor reaches end position
|
|
79
|
+
self._fly_status: WatchableAsyncStatus | None = None
|
|
80
|
+
|
|
81
|
+
super().__init__(name=name)
|
|
82
|
+
|
|
83
|
+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
84
|
+
super().set_name(name, child_name_separator=child_name_separator)
|
|
85
|
+
# Readback should be named the same as its parent in read()
|
|
86
|
+
self.user_readback.set_name(name)
|
|
87
|
+
|
|
88
|
+
@AsyncStatus.wrap
|
|
89
|
+
async def prepare(self, value: FlySimMotorInfo):
|
|
90
|
+
"""Calculate run-up and move there, setting fly velocity when there."""
|
|
91
|
+
self._fly_info = value
|
|
92
|
+
# Move to start as fast as we can
|
|
93
|
+
await self.velocity.set(0)
|
|
94
|
+
await self.set(value.start_position(await self.acceleration_time.get_value()))
|
|
95
|
+
# Set the velocity for the actual move
|
|
96
|
+
await self.velocity.set(value.velocity)
|
|
97
|
+
|
|
98
|
+
async def locate(self) -> Location[float]:
|
|
99
|
+
"""Return the current setpoint and readback of the motor."""
|
|
100
|
+
setpoint, readback = await asyncio.gather(
|
|
101
|
+
self.user_setpoint.get_value(), self.user_readback.get_value()
|
|
102
|
+
)
|
|
103
|
+
return Location(setpoint=setpoint, readback=readback)
|
|
104
|
+
|
|
105
|
+
def subscribe(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
106
|
+
self.user_readback.subscribe(function)
|
|
107
|
+
|
|
108
|
+
def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
109
|
+
self.user_readback.clear_sub(function)
|
|
110
|
+
|
|
111
|
+
@AsyncStatus.wrap
|
|
112
|
+
async def kickoff(self):
|
|
113
|
+
"""Begin moving motor from prepared position to final position."""
|
|
114
|
+
if not self._fly_info:
|
|
115
|
+
msg = "Motor must be prepared before attempting to kickoff"
|
|
116
|
+
raise RuntimeError(msg)
|
|
117
|
+
acceleration_time = await self.acceleration_time.get_value()
|
|
118
|
+
self._fly_status = self.set(self._fly_info.end_position(acceleration_time))
|
|
119
|
+
# Wait for the acceleration time to ensure we are at velocity
|
|
120
|
+
await asyncio.sleep(acceleration_time)
|
|
121
|
+
|
|
122
|
+
def complete(self) -> WatchableAsyncStatus:
|
|
123
|
+
"""Mark as complete once motor reaches completed position."""
|
|
124
|
+
if not self._fly_status:
|
|
125
|
+
msg = "kickoff not called"
|
|
126
|
+
raise RuntimeError(msg)
|
|
127
|
+
return self._fly_status
|
|
128
|
+
|
|
129
|
+
async def _move(self, old_position: float, new_position: float, velocity: float):
|
|
130
|
+
if old_position == new_position:
|
|
131
|
+
return
|
|
132
|
+
start = time.monotonic()
|
|
133
|
+
acceleration_time = abs(await self.acceleration_time.get_value())
|
|
134
|
+
sign = np.sign(new_position - old_position)
|
|
135
|
+
velocity = abs(velocity) * sign
|
|
136
|
+
# The total distance to move
|
|
137
|
+
total_distance = new_position - old_position
|
|
138
|
+
# The ramp distance is the distance taken to ramp up (the same distance
|
|
139
|
+
# is taken to ramp down). This is the area under the triangle of the
|
|
140
|
+
# velocity ramp up (base * height / 2)
|
|
141
|
+
ramp_distance = acceleration_time * velocity / 2
|
|
142
|
+
if abs(ramp_distance * 2) >= abs(total_distance):
|
|
143
|
+
# All time is ramp up and down, so recalculate the maximum velocity
|
|
144
|
+
# we get to. We know the area under the ramp up triangle is half the
|
|
145
|
+
# total distance, and we also know the ratio of velocity over
|
|
146
|
+
# acceleration_time is the same as the ration of max_velocity over
|
|
147
|
+
# ramp_time, so solve the simultaneous equations to get
|
|
148
|
+
# max_velocity and ramp_time.
|
|
149
|
+
max_velocity = np.sqrt(total_distance * velocity / acceleration_time) * sign
|
|
150
|
+
ramp_time = total_distance / max_velocity
|
|
151
|
+
# So move time is just the ramp up and ramp down with no constant
|
|
152
|
+
# velocity section
|
|
153
|
+
move_time = 2 * ramp_time
|
|
154
|
+
else:
|
|
155
|
+
# Middle segments of constant velocity
|
|
156
|
+
max_velocity = velocity
|
|
157
|
+
# Ramp up and down time is exactly the requested acceleration time
|
|
158
|
+
ramp_time = acceleration_time
|
|
159
|
+
# So move time is twice this, plus the time taken to move the
|
|
160
|
+
# remaining distance at constant velocity
|
|
161
|
+
move_time = ramp_time * 2 + (total_distance - ramp_distance * 2) / velocity
|
|
162
|
+
# Make an array of relative update times at 10Hz intervals
|
|
163
|
+
update_times = list(np.arange(0.1, move_time, 0.1, dtype=float))
|
|
164
|
+
# With the end position appended
|
|
165
|
+
if update_times and np.isclose(update_times[-1], move_time):
|
|
166
|
+
update_times[-1] = move_time
|
|
167
|
+
else:
|
|
168
|
+
update_times.append(move_time)
|
|
169
|
+
# Iterate through the update times, calculating new position for each
|
|
170
|
+
for t in update_times:
|
|
171
|
+
if t <= ramp_time:
|
|
172
|
+
# Ramp up phase, calculate area under the ramp up triangle
|
|
173
|
+
current_velocity = t / ramp_time * max_velocity
|
|
174
|
+
position = old_position + current_velocity * t / 2
|
|
175
|
+
elif t >= move_time - ramp_time:
|
|
176
|
+
# Ramp down phase, subtract area under the ramp down triangle
|
|
177
|
+
time_left = move_time - t
|
|
178
|
+
current_velocity = time_left / ramp_time * max_velocity
|
|
179
|
+
position = new_position - current_velocity * time_left / 2
|
|
180
|
+
else:
|
|
181
|
+
# Constant velocity phase
|
|
182
|
+
position = old_position + ramp_distance + (t - ramp_time) * max_velocity
|
|
183
|
+
# Calculate how long to wait to get there
|
|
184
|
+
relative_time = time.monotonic() - start
|
|
185
|
+
await asyncio.sleep(t - relative_time)
|
|
186
|
+
# Update the readback position
|
|
187
|
+
self._user_readback_set(position)
|
|
188
|
+
|
|
189
|
+
@WatchableAsyncStatus.wrap
|
|
190
|
+
async def set(self, value: float):
|
|
191
|
+
"""Asynchronously move the motor to a new position."""
|
|
192
|
+
new_position = value
|
|
193
|
+
# Make sure any existing move tasks are stopped
|
|
194
|
+
if self._move_status:
|
|
195
|
+
self._move_status.task.cancel()
|
|
196
|
+
self._move_status = None
|
|
197
|
+
# work out where we were
|
|
198
|
+
old_position, units, velocity = await asyncio.gather(
|
|
199
|
+
self.user_setpoint.get_value(),
|
|
200
|
+
self.units.get_value(),
|
|
201
|
+
self.velocity.get_value(),
|
|
202
|
+
)
|
|
203
|
+
# update the setpoint to where we want to be
|
|
204
|
+
await self.user_setpoint.set(new_position)
|
|
205
|
+
# If zero velocity, do instant move
|
|
206
|
+
if velocity == 0:
|
|
207
|
+
self._user_readback_set(new_position)
|
|
208
|
+
else:
|
|
209
|
+
self._move_status = AsyncStatus(
|
|
210
|
+
self._move(old_position, new_position, velocity)
|
|
211
|
+
)
|
|
212
|
+
# If stop is called then this will raise a CancelledError, ignore it
|
|
213
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
214
|
+
async for current_position in observe_value(
|
|
215
|
+
self.user_readback, done_status=self._move_status
|
|
216
|
+
):
|
|
217
|
+
yield WatcherUpdate(
|
|
218
|
+
current=current_position,
|
|
219
|
+
initial=old_position,
|
|
220
|
+
target=new_position,
|
|
221
|
+
name=self.name,
|
|
222
|
+
unit=units,
|
|
223
|
+
)
|
|
224
|
+
if not self._set_success:
|
|
225
|
+
raise RuntimeError("Motor was stopped")
|
|
226
|
+
|
|
227
|
+
async def stop(self, success=True):
|
|
228
|
+
"""Stop the motor if it is moving."""
|
|
229
|
+
self._set_success = success
|
|
230
|
+
if self._move_status:
|
|
231
|
+
self._move_status.task.cancel()
|
|
232
|
+
self._move_status = None
|
|
233
|
+
await self.user_setpoint.set(await self.user_readback.get_value())
|