ophyd-async 0.2.0__tar.gz → 0.3a1__tar.gz
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-0.2.0 → ophyd-async-0.3a1}/.codecov.yml +1 -1
- {ophyd-async-0.2.0/src/ophyd_async.egg-info → ophyd-async-0.3a1}/PKG-INFO +2 -2
- ophyd-async-0.3a1/docs/user/explanations/event-loop-choice.rst +52 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/user/index.rst +1 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/pyproject.toml +2 -2
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/_version.py +2 -2
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/core/__init__.py +5 -9
- ophyd-async-0.3a1/src/ophyd_async/core/_providers.py +66 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/core/async_status.py +3 -3
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/core/detector.py +159 -37
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/core/device.py +37 -38
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/core/device_save_loader.py +96 -23
- ophyd-async-0.3a1/src/ophyd_async/core/flyer.py +94 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/core/signal.py +11 -4
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/core/signal_backend.py +2 -2
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/core/sim_signal_backend.py +2 -2
- ophyd-async-0.3a1/src/ophyd_async/core/utils.py +150 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/_backend/_aioca.py +18 -26
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/_backend/_p4p.py +58 -27
- ophyd-async-0.3a1/src/ophyd_async/epics/_backend/common.py +20 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/controllers/pilatus_controller.py +1 -1
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/writers/_hdffile.py +17 -3
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/writers/hdf_writer.py +21 -15
- ophyd-async-0.3a1/src/ophyd_async/epics/pvi.py +70 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/signal/__init__.py +0 -2
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/panda/__init__.py +5 -2
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/panda/panda.py +41 -94
- ophyd-async-0.3a1/src/ophyd_async/panda/panda_controller.py +41 -0
- ophyd-async-0.3a1/src/ophyd_async/panda/utils.py +15 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1/src/ophyd_async.egg-info}/PKG-INFO +2 -2
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async.egg-info/SOURCES.txt +7 -1
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async.egg-info/requires.txt +1 -1
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/conftest.py +4 -1
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/core/test_async_status.py +13 -4
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/core/test_device.py +11 -4
- ophyd-async-0.3a1/tests/core/test_device_collector.py +153 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/core/test_device_save_loader.py +42 -10
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/core/test_flyer.py +99 -37
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/core/test_signal.py +2 -1
- ophyd-async-0.3a1/tests/core/test_utils.py +249 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/areadetector/test_controllers.py +4 -19
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/areadetector/test_scans.py +25 -32
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/demo/test_demo.py +12 -14
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/demo/test_demo_ad_sim_detector.py +38 -26
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/test_records.db +29 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/test_signals.py +133 -36
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/panda/db/panda.db +12 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/panda/test_panda.py +26 -9
- ophyd-async-0.3a1/tests/panda/test_panda_controller.py +28 -0
- ophyd-async-0.3a1/tests/panda/test_panda_utils.py +38 -0
- ophyd-async-0.2.0/src/ophyd_async/core/_providers.py +0 -35
- ophyd-async-0.2.0/src/ophyd_async/core/flyer.py +0 -299
- ophyd-async-0.2.0/src/ophyd_async/core/utils.py +0 -104
- ophyd-async-0.2.0/src/ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd-async-0.2.0/tests/core/test_device_collector.py +0 -16
- ophyd-async-0.2.0/tests/core/test_utils.py +0 -13
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.devcontainer/Dockerfile +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.devcontainer/devcontainer.json +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.git-blame-ignore-revs +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.github/CONTRIBUTING.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.github/actions/install_requirements/action.yml +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.github/dependabot.yml +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.github/pages/index.html +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.github/pages/make_switcher.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.github/workflows/code.yml +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.github/workflows/docs.yml +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.github/workflows/docs_clean.yml +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.github/workflows/linkcheck.yml +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.gitignore +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.mailmap +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/.pre-commit-config.yaml +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/LICENSE +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/README.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/_templates/README +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/_templates/custom-class-template.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/_templates/custom-module-template.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/conf.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/explanations/decisions/0003-ophyd-async-migration.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/explanations/decisions/0004-repository-structure.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/explanations/decisions/0005-respect-black-line-length.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/explanations/decisions/0006-procedural-device-definitions.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/explanations/decisions.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/how-to/build-docs.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/how-to/contribute.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/how-to/lint.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/how-to/make-release.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/how-to/pin-requirements.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/how-to/run-tests.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/how-to/static-analysis.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/how-to/test-container.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/how-to/update-tools.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/index.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/reference/standards.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/developer/tutorials/dev-install.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/genindex.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/images/bluesky_ophyd_epics_devices_logo.svg +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/images/bluesky_ophyd_logo.svg +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/images/ophyd_favicon.svg +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/index.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/user/examples/epics_demo.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/user/explanations/docs-structure.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/user/how-to/make-a-simple-device.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/user/how-to/run-container.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/user/reference/api.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/user/tutorials/installation.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/docs/user/tutorials/using-existing-devices.rst +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/setup.cfg +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/__main__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/core/standard_readable.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/_backend/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/controllers/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/drivers/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/drivers/ad_base.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/drivers/pilatus_driver.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/single_trigger_det.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/utils.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/writers/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/writers/_hdfdataset.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/writers/nd_file_hdf.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/areadetector/writers/nd_plugin.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/demo/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/demo/demo_ad_sim_detector.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/demo/mover.db +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/demo/sensor.db +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/motion/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/motion/motor.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/signal/_epics_transport.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/epics/signal/signal.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async/panda/table.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async.egg-info/dependency_links.txt +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async.egg-info/entry_points.txt +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/src/ophyd_async.egg-info/top_level.txt +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/core/test_sim.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/areadetector/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/areadetector/test_drivers.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/areadetector/test_single_trigger_det.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/areadetector/test_utils.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/areadetector/test_writers.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/motion/__init__.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/epics/motion/test_motor.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/panda/test_table.py +0 -0
- {ophyd-async-0.2.0 → ophyd-async-0.3a1}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ophyd-async
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3a1
|
|
4
4
|
Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
|
|
5
5
|
Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -46,7 +46,7 @@ Requires-Dist: networkx>=2.0
|
|
|
46
46
|
Requires-Dist: numpy
|
|
47
47
|
Requires-Dist: packaging
|
|
48
48
|
Requires-Dist: pint
|
|
49
|
-
Requires-Dist: bluesky
|
|
49
|
+
Requires-Dist: bluesky>=1.13.0a3
|
|
50
50
|
Requires-Dist: event-model
|
|
51
51
|
Requires-Dist: p4p
|
|
52
52
|
Requires-Dist: pyyaml
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Device Collector Event-Loop Choice
|
|
2
|
+
----------------------------------
|
|
3
|
+
|
|
4
|
+
In a sync context, the ophyd-async :python:`DeviceCollector` requires the bluesky event-loop
|
|
5
|
+
to connect to devices. In an async context, it does not.
|
|
6
|
+
|
|
7
|
+
Sync Context
|
|
8
|
+
============
|
|
9
|
+
|
|
10
|
+
In a sync context the run-engine must be initialized prior to connecting to devices.
|
|
11
|
+
We enfore usage of the bluesky event-loop in this context.
|
|
12
|
+
|
|
13
|
+
The following will fail if :python:`RE = RunEngine()` has not been called already:
|
|
14
|
+
|
|
15
|
+
.. code:: python
|
|
16
|
+
|
|
17
|
+
with DeviceCollector():
|
|
18
|
+
device1 = Device1(prefix)
|
|
19
|
+
device2 = Device2(prefix)
|
|
20
|
+
device3 = Device3(prefix)
|
|
21
|
+
|
|
22
|
+
The :python:`DeviceCollector` connects to devices in the event-loop created in the run-engine.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Async Context
|
|
26
|
+
=============
|
|
27
|
+
|
|
28
|
+
In an async context device connection is decoupled from the run-engine.
|
|
29
|
+
The following attempts connection to all the devices in the :python:`DeviceCollector`
|
|
30
|
+
before or after run-engine initialization.
|
|
31
|
+
|
|
32
|
+
.. code:: python
|
|
33
|
+
|
|
34
|
+
async def connection_function() :
|
|
35
|
+
async with DeviceCollector():
|
|
36
|
+
device1 = Device1(prefix)
|
|
37
|
+
device2 = Device2(prefix)
|
|
38
|
+
device3 = Device3(prefix)
|
|
39
|
+
|
|
40
|
+
asyncio.run(connection_function())
|
|
41
|
+
|
|
42
|
+
The devices will be unable to be used in the run-engine unless they share the same event-loop.
|
|
43
|
+
When the run-engine is initialised it will create a new background event-loop to use if one
|
|
44
|
+
is not passed in with :python:`RunEngine(loop=loop)`.
|
|
45
|
+
|
|
46
|
+
If the user wants to use devices in the async :python:`DeviceCollector` within the run-engine
|
|
47
|
+
they can either:
|
|
48
|
+
|
|
49
|
+
* Run the :python:`DeviceCollector` first and pass the event-loop into the run-engine.
|
|
50
|
+
* Initialize the run-engine first and run the :python:`DeviceCollector` using the bluesky event-loop.
|
|
51
|
+
|
|
52
|
+
|
|
@@ -18,11 +18,11 @@ dependencies = [
|
|
|
18
18
|
"numpy",
|
|
19
19
|
"packaging",
|
|
20
20
|
"pint",
|
|
21
|
-
"bluesky",
|
|
21
|
+
"bluesky>=1.13.0a3",
|
|
22
22
|
"event-model",
|
|
23
23
|
"p4p",
|
|
24
24
|
"pyyaml",
|
|
25
|
-
]
|
|
25
|
+
]
|
|
26
26
|
|
|
27
27
|
dynamic = ["version"]
|
|
28
28
|
license.file = "LICENSE"
|
|
@@ -10,18 +10,14 @@ from .detector import DetectorControl, DetectorTrigger, DetectorWriter, Standard
|
|
|
10
10
|
from .device import Device, DeviceCollector, DeviceVector
|
|
11
11
|
from .device_save_loader import (
|
|
12
12
|
get_signal_values,
|
|
13
|
+
load_device,
|
|
13
14
|
load_from_yaml,
|
|
15
|
+
save_device,
|
|
14
16
|
save_to_yaml,
|
|
15
17
|
set_signal_values,
|
|
16
18
|
walk_rw_signals,
|
|
17
19
|
)
|
|
18
|
-
from .flyer import
|
|
19
|
-
DetectorGroupLogic,
|
|
20
|
-
HardwareTriggeredFlyable,
|
|
21
|
-
SameTriggerDetectorGroupLogic,
|
|
22
|
-
TriggerInfo,
|
|
23
|
-
TriggerLogic,
|
|
24
|
-
)
|
|
20
|
+
from .flyer import HardwareTriggeredFlyable, TriggerInfo, TriggerLogic
|
|
25
21
|
from .signal import (
|
|
26
22
|
Signal,
|
|
27
23
|
SignalR,
|
|
@@ -79,8 +75,6 @@ __all__ = [
|
|
|
79
75
|
"StaticDirectoryProvider",
|
|
80
76
|
"StandardReadable",
|
|
81
77
|
"TriggerInfo",
|
|
82
|
-
"DetectorGroupLogic",
|
|
83
|
-
"SameTriggerDetectorGroupLogic",
|
|
84
78
|
"TriggerLogic",
|
|
85
79
|
"HardwareTriggeredFlyable",
|
|
86
80
|
"DEFAULT_TIMEOUT",
|
|
@@ -97,4 +91,6 @@ __all__ = [
|
|
|
97
91
|
"save_to_yaml",
|
|
98
92
|
"set_signal_values",
|
|
99
93
|
"walk_rw_signals",
|
|
94
|
+
"load_device",
|
|
95
|
+
"save_device",
|
|
100
96
|
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Protocol, Sequence, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class DirectoryInfo:
|
|
9
|
+
"""
|
|
10
|
+
Information about where and how to write a file.
|
|
11
|
+
|
|
12
|
+
The bluesky event model splits the URI for a resource into two segments to aid in
|
|
13
|
+
different applications mounting filesystems at different mount points.
|
|
14
|
+
The portion of this path which is relevant only for the writer is the 'root',
|
|
15
|
+
while the path from an agreed upon mutual mounting is the resource_path.
|
|
16
|
+
The resource_dir is used with the filename to construct the resource_path.
|
|
17
|
+
|
|
18
|
+
:param root: Path of a root directory, relevant only for the file writer
|
|
19
|
+
:param resource_dir: Directory into which files should be written, relative to root
|
|
20
|
+
:param prefix: Optional filename prefix to add to all files
|
|
21
|
+
:param suffix: Optional filename suffix to add to all files
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
root: Path
|
|
25
|
+
resource_dir: Path
|
|
26
|
+
prefix: Optional[str] = ""
|
|
27
|
+
suffix: Optional[str] = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DirectoryProvider(Protocol):
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def __call__(self) -> DirectoryInfo:
|
|
33
|
+
"""Get the current directory to write files into"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StaticDirectoryProvider(DirectoryProvider):
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
directory_path: Union[str, Path],
|
|
40
|
+
filename_prefix: str = "",
|
|
41
|
+
filename_suffix: str = "",
|
|
42
|
+
resource_dir: Path = Path("."),
|
|
43
|
+
) -> None:
|
|
44
|
+
if isinstance(directory_path, str):
|
|
45
|
+
directory_path = Path(directory_path)
|
|
46
|
+
self._directory_info = DirectoryInfo(
|
|
47
|
+
root=directory_path,
|
|
48
|
+
resource_dir=resource_dir,
|
|
49
|
+
prefix=filename_prefix,
|
|
50
|
+
suffix=filename_suffix,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def __call__(self) -> DirectoryInfo:
|
|
54
|
+
return self._directory_info
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NameProvider(Protocol):
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def __call__(self) -> str:
|
|
60
|
+
"""Get the name to be used as a data_key in the descriptor document"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ShapeProvider(Protocol):
|
|
64
|
+
@abstractmethod
|
|
65
|
+
async def __call__(self) -> Sequence[int]:
|
|
66
|
+
"""Get the shape of the data collection"""
|
|
@@ -85,12 +85,12 @@ class AsyncStatus(Status):
|
|
|
85
85
|
|
|
86
86
|
def __repr__(self) -> str:
|
|
87
87
|
if self.done:
|
|
88
|
-
if self.exception()
|
|
89
|
-
status = "errored"
|
|
88
|
+
if e := self.exception():
|
|
89
|
+
status = f"errored: {repr(e)}"
|
|
90
90
|
else:
|
|
91
91
|
status = "done"
|
|
92
92
|
else:
|
|
93
93
|
status = "pending"
|
|
94
|
-
return f"<{type(self).__name__} {status}>"
|
|
94
|
+
return f"<{type(self).__name__}, task: {self.task.get_coro()}, {status}>"
|
|
95
95
|
|
|
96
96
|
__str__ = __repr__
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
"""Module which defines abstract classes to work with detectors"""
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
4
|
+
import time
|
|
3
5
|
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
4
7
|
from enum import Enum
|
|
5
|
-
from typing import
|
|
8
|
+
from typing import (
|
|
9
|
+
AsyncGenerator,
|
|
10
|
+
AsyncIterator,
|
|
11
|
+
Callable,
|
|
12
|
+
Dict,
|
|
13
|
+
List,
|
|
14
|
+
Optional,
|
|
15
|
+
Sequence,
|
|
16
|
+
TypeVar,
|
|
17
|
+
)
|
|
6
18
|
|
|
7
19
|
from bluesky.protocols import (
|
|
8
|
-
|
|
20
|
+
Collectable,
|
|
9
21
|
Configurable,
|
|
10
22
|
Descriptor,
|
|
23
|
+
Flyable,
|
|
24
|
+
Preparable,
|
|
11
25
|
Readable,
|
|
12
26
|
Reading,
|
|
13
27
|
Stageable,
|
|
28
|
+
StreamAsset,
|
|
14
29
|
Triggerable,
|
|
15
|
-
|
|
30
|
+
WritesStreamAssets,
|
|
16
31
|
)
|
|
17
32
|
|
|
18
33
|
from .async_status import AsyncStatus
|
|
@@ -20,6 +35,8 @@ from .device import Device
|
|
|
20
35
|
from .signal import SignalR
|
|
21
36
|
from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
|
|
22
37
|
|
|
38
|
+
T = TypeVar("T")
|
|
39
|
+
|
|
23
40
|
|
|
24
41
|
class DetectorTrigger(str, Enum):
|
|
25
42
|
#: Detector generates internal trigger for given rate
|
|
@@ -32,6 +49,18 @@ class DetectorTrigger(str, Enum):
|
|
|
32
49
|
variable_gate = "variable_gate"
|
|
33
50
|
|
|
34
51
|
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class TriggerInfo:
|
|
54
|
+
#: Number of triggers that will be sent
|
|
55
|
+
num: int
|
|
56
|
+
#: Sort of triggers that will be sent
|
|
57
|
+
trigger: DetectorTrigger
|
|
58
|
+
#: What is the minimum deadtime between triggers
|
|
59
|
+
deadtime: float
|
|
60
|
+
#: What is the maximum high time of the triggers
|
|
61
|
+
livetime: float
|
|
62
|
+
|
|
63
|
+
|
|
35
64
|
class DetectorControl(ABC):
|
|
36
65
|
@abstractmethod
|
|
37
66
|
def get_deadtime(self, exposure: float) -> float:
|
|
@@ -40,8 +69,8 @@ class DetectorControl(ABC):
|
|
|
40
69
|
@abstractmethod
|
|
41
70
|
async def arm(
|
|
42
71
|
self,
|
|
72
|
+
num: int,
|
|
43
73
|
trigger: DetectorTrigger = DetectorTrigger.internal,
|
|
44
|
-
num: int = 0,
|
|
45
74
|
exposure: Optional[float] = None,
|
|
46
75
|
) -> AsyncStatus:
|
|
47
76
|
"""Arm the detector and return AsyncStatus.
|
|
@@ -68,17 +97,17 @@ class DetectorWriter(ABC):
|
|
|
68
97
|
"""
|
|
69
98
|
|
|
70
99
|
@abstractmethod
|
|
71
|
-
|
|
72
|
-
self,
|
|
73
|
-
) -> None:
|
|
74
|
-
"""
|
|
100
|
+
def observe_indices_written(
|
|
101
|
+
self, timeout=DEFAULT_TIMEOUT
|
|
102
|
+
) -> AsyncGenerator[int, None]:
|
|
103
|
+
"""Yield each index as it is written"""
|
|
75
104
|
|
|
76
105
|
@abstractmethod
|
|
77
106
|
async def get_indices_written(self) -> int:
|
|
78
107
|
"""Get the number of indices written"""
|
|
79
108
|
|
|
80
109
|
@abstractmethod
|
|
81
|
-
def collect_stream_docs(self, indices_written: int) -> AsyncIterator[
|
|
110
|
+
def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
|
|
82
111
|
"""Create Stream docs up to given number written"""
|
|
83
112
|
|
|
84
113
|
@abstractmethod
|
|
@@ -92,16 +121,18 @@ class StandardDetector(
|
|
|
92
121
|
Configurable,
|
|
93
122
|
Readable,
|
|
94
123
|
Triggerable,
|
|
95
|
-
|
|
124
|
+
Preparable,
|
|
125
|
+
Flyable,
|
|
126
|
+
Collectable,
|
|
127
|
+
WritesStreamAssets,
|
|
96
128
|
):
|
|
97
|
-
"""Detector with useful
|
|
129
|
+
"""Detector with useful step and flyscan behaviour.
|
|
98
130
|
|
|
99
131
|
Must be supplied instances of classes that inherit from DetectorControl and
|
|
100
132
|
DetectorData, to dictate how the detector will be controlled (i.e. arming and
|
|
101
133
|
disarming) as well as how the detector data will be written (i.e. opening and
|
|
102
134
|
closing the writer, and handling data writing indices).
|
|
103
135
|
|
|
104
|
-
NOTE: only for step-scans.
|
|
105
136
|
"""
|
|
106
137
|
|
|
107
138
|
def __init__(
|
|
@@ -127,6 +158,16 @@ class StandardDetector(
|
|
|
127
158
|
self._describe: Dict[str, Descriptor] = {}
|
|
128
159
|
self._config_sigs = list(config_sigs)
|
|
129
160
|
self._frame_writing_timeout = writer_timeout
|
|
161
|
+
# For prepare
|
|
162
|
+
self._arm_status: Optional[AsyncStatus] = None
|
|
163
|
+
self._trigger_info: Optional[TriggerInfo] = None
|
|
164
|
+
# For kickoff
|
|
165
|
+
self._watchers: List[Callable] = []
|
|
166
|
+
self._fly_status: Optional[AsyncStatus] = None
|
|
167
|
+
self._fly_start: float
|
|
168
|
+
|
|
169
|
+
self._intial_frame: int
|
|
170
|
+
self._last_frame: int
|
|
130
171
|
super().__init__(name)
|
|
131
172
|
|
|
132
173
|
@property
|
|
@@ -137,6 +178,13 @@ class StandardDetector(
|
|
|
137
178
|
def writer(self) -> DetectorWriter:
|
|
138
179
|
return self._writer
|
|
139
180
|
|
|
181
|
+
@AsyncStatus.wrap
|
|
182
|
+
async def stage(self) -> None:
|
|
183
|
+
"""Disarm the detector, stop filewriting, and open file for writing."""
|
|
184
|
+
await self.check_config_sigs()
|
|
185
|
+
await asyncio.gather(self.writer.close(), self.controller.disarm())
|
|
186
|
+
self._describe = await self.writer.open()
|
|
187
|
+
|
|
140
188
|
async def check_config_sigs(self):
|
|
141
189
|
"""Checks configuration signals are named and connected."""
|
|
142
190
|
for signal in self._config_sigs:
|
|
@@ -144,7 +192,6 @@ class StandardDetector(
|
|
|
144
192
|
raise Exception(
|
|
145
193
|
"config signal must be named before it is passed to the detector"
|
|
146
194
|
)
|
|
147
|
-
|
|
148
195
|
try:
|
|
149
196
|
await signal.get_value()
|
|
150
197
|
except NotImplementedError:
|
|
@@ -154,17 +201,20 @@ class StandardDetector(
|
|
|
154
201
|
)
|
|
155
202
|
|
|
156
203
|
@AsyncStatus.wrap
|
|
157
|
-
async def
|
|
158
|
-
"""
|
|
159
|
-
await self.
|
|
160
|
-
|
|
161
|
-
|
|
204
|
+
async def unstage(self) -> None:
|
|
205
|
+
"""Stop data writing."""
|
|
206
|
+
await self.writer.close()
|
|
207
|
+
|
|
208
|
+
async def read_configuration(self) -> Dict[str, Reading]:
|
|
209
|
+
return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
|
|
162
210
|
|
|
163
211
|
async def describe_configuration(self) -> Dict[str, Descriptor]:
|
|
164
212
|
return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
|
|
165
213
|
|
|
166
|
-
async def
|
|
167
|
-
|
|
214
|
+
async def read(self) -> Dict[str, Reading]:
|
|
215
|
+
"""Read the detector"""
|
|
216
|
+
# All data is in StreamResources, not Events, so nothing to output here
|
|
217
|
+
return {}
|
|
168
218
|
|
|
169
219
|
def describe(self) -> Dict[str, Descriptor]:
|
|
170
220
|
return self._describe
|
|
@@ -173,27 +223,99 @@ class StandardDetector(
|
|
|
173
223
|
async def trigger(self) -> None:
|
|
174
224
|
"""Arm the detector and wait for it to finish."""
|
|
175
225
|
indices_written = await self.writer.get_indices_written()
|
|
176
|
-
written_status = await self.controller.arm(
|
|
226
|
+
written_status = await self.controller.arm(
|
|
227
|
+
num=1,
|
|
228
|
+
trigger=DetectorTrigger.internal,
|
|
229
|
+
)
|
|
177
230
|
await written_status
|
|
178
|
-
|
|
179
|
-
|
|
231
|
+
end_observation = indices_written + 1
|
|
232
|
+
|
|
233
|
+
async for index in self.writer.observe_indices_written(
|
|
234
|
+
self._frame_writing_timeout
|
|
235
|
+
):
|
|
236
|
+
if index >= end_observation:
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
def prepare(
|
|
240
|
+
self,
|
|
241
|
+
value: T,
|
|
242
|
+
) -> AsyncStatus:
|
|
243
|
+
"""Arm detector"""
|
|
244
|
+
return AsyncStatus(self._prepare(value))
|
|
245
|
+
|
|
246
|
+
async def _prepare(self, value: T) -> None:
|
|
247
|
+
"""Arm detector.
|
|
248
|
+
|
|
249
|
+
Prepare the detector with trigger information. This is determined at and passed
|
|
250
|
+
in from the plan level.
|
|
251
|
+
|
|
252
|
+
This currently only prepares detectors for flyscans and stepscans just use the
|
|
253
|
+
trigger information determined in trigger.
|
|
254
|
+
|
|
255
|
+
To do: Unify prepare to be use for both fly and step scans.
|
|
256
|
+
"""
|
|
257
|
+
assert type(value) is TriggerInfo
|
|
258
|
+
self._trigger_info = value
|
|
259
|
+
self._initial_frame = await self.writer.get_indices_written()
|
|
260
|
+
self._last_frame = self._initial_frame + self._trigger_info.num
|
|
261
|
+
|
|
262
|
+
required = self.controller.get_deadtime(self._trigger_info.livetime)
|
|
263
|
+
assert required <= self._trigger_info.deadtime, (
|
|
264
|
+
f"Detector {self.controller} needs at least {required}s deadtime, "
|
|
265
|
+
f"but trigger logic provides only {self._trigger_info.deadtime}s"
|
|
180
266
|
)
|
|
181
267
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
268
|
+
self._arm_status = await self.controller.arm(
|
|
269
|
+
num=self._trigger_info.num,
|
|
270
|
+
trigger=self._trigger_info.trigger,
|
|
271
|
+
exposure=self._trigger_info.livetime,
|
|
272
|
+
)
|
|
186
273
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
274
|
+
@AsyncStatus.wrap
|
|
275
|
+
async def kickoff(self) -> None:
|
|
276
|
+
self._fly_status = AsyncStatus(self._fly(), self._watchers)
|
|
277
|
+
self._fly_start = time.monotonic()
|
|
278
|
+
|
|
279
|
+
async def _fly(self) -> None:
|
|
280
|
+
await self._observe_writer_indicies(self._last_frame)
|
|
281
|
+
|
|
282
|
+
async def _observe_writer_indicies(self, end_observation: int):
|
|
283
|
+
async for index in self.writer.observe_indices_written(
|
|
284
|
+
self._frame_writing_timeout
|
|
285
|
+
):
|
|
286
|
+
for watcher in self._watchers:
|
|
287
|
+
watcher(
|
|
288
|
+
name=self.name,
|
|
289
|
+
current=index,
|
|
290
|
+
initial=self._initial_frame,
|
|
291
|
+
target=end_observation,
|
|
292
|
+
unit="",
|
|
293
|
+
precision=0,
|
|
294
|
+
time_elapsed=time.monotonic() - self._fly_start,
|
|
295
|
+
)
|
|
296
|
+
if index >= end_observation:
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
@AsyncStatus.wrap
|
|
300
|
+
async def complete(self) -> AsyncStatus:
|
|
301
|
+
assert self._fly_status, "Kickoff not run"
|
|
302
|
+
return await self._fly_status
|
|
303
|
+
|
|
304
|
+
async def describe_collect(self) -> Dict[str, Descriptor]:
|
|
305
|
+
return self._describe
|
|
306
|
+
|
|
307
|
+
async def collect_asset_docs(
|
|
308
|
+
self, index: Optional[int] = None
|
|
309
|
+
) -> AsyncIterator[StreamAsset]:
|
|
310
|
+
"""Collect stream datum documents for all indices written.
|
|
190
311
|
|
|
191
|
-
|
|
312
|
+
The index is optional, and provided for flyscans, however this needs to be
|
|
313
|
+
retrieved for stepscans.
|
|
314
|
+
"""
|
|
315
|
+
if not index:
|
|
316
|
+
index = await self.writer.get_indices_written()
|
|
317
|
+
async for doc in self.writer.collect_stream_docs(index):
|
|
192
318
|
yield doc
|
|
193
|
-
# async for doc in self.writer.collect_stream_docs(indices_written):
|
|
194
|
-
# yield doc
|
|
195
319
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"""Stop data writing."""
|
|
199
|
-
await self.writer.close()
|
|
320
|
+
async def get_index(self) -> int:
|
|
321
|
+
return await self.writer.get_indices_written()
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
"""Base device"""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
|
-
import asyncio
|
|
5
|
-
import logging
|
|
6
5
|
import sys
|
|
7
|
-
from
|
|
8
|
-
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Coroutine,
|
|
9
|
+
Dict,
|
|
10
|
+
Generator,
|
|
11
|
+
Iterator,
|
|
12
|
+
Optional,
|
|
13
|
+
Set,
|
|
14
|
+
Tuple,
|
|
15
|
+
TypeVar,
|
|
16
|
+
)
|
|
9
17
|
|
|
10
18
|
from bluesky.protocols import HasName
|
|
11
19
|
from bluesky.run_engine import call_in_bluesky_event_loop
|
|
12
20
|
|
|
13
|
-
from .utils import NotConnected, wait_for_connection
|
|
21
|
+
from .utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
|
|
14
22
|
|
|
15
23
|
|
|
16
24
|
class Device(HasName):
|
|
@@ -50,16 +58,21 @@ class Device(HasName):
|
|
|
50
58
|
child.set_name(child_name)
|
|
51
59
|
child.parent = self
|
|
52
60
|
|
|
53
|
-
async def connect(self, sim: bool = False):
|
|
61
|
+
async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT):
|
|
54
62
|
"""Connect self and all child Devices.
|
|
55
63
|
|
|
64
|
+
Contains a timeout that gets propagated to child.connect methods.
|
|
65
|
+
|
|
56
66
|
Parameters
|
|
57
67
|
----------
|
|
58
68
|
sim:
|
|
59
69
|
If True then connect in simulation mode.
|
|
70
|
+
timeout:
|
|
71
|
+
Time to wait before failing with a TimeoutError.
|
|
60
72
|
"""
|
|
61
73
|
coros = {
|
|
62
|
-
name: child_device.connect(sim
|
|
74
|
+
name: child_device.connect(sim, timeout=timeout)
|
|
75
|
+
for name, child_device in self.children()
|
|
63
76
|
}
|
|
64
77
|
if coros:
|
|
65
78
|
await wait_for_connection(**coros)
|
|
@@ -140,41 +153,19 @@ class DeviceCollector:
|
|
|
140
153
|
|
|
141
154
|
async def _on_exit(self) -> None:
|
|
142
155
|
# Name and kick off connect for devices
|
|
143
|
-
|
|
156
|
+
connect_coroutines: Dict[str, Coroutine] = {}
|
|
144
157
|
for name, obj in self._objects_on_exit.items():
|
|
145
158
|
if name not in self._names_on_enter and isinstance(obj, Device):
|
|
146
159
|
if self._set_name and not obj.name:
|
|
147
160
|
obj.set_name(name)
|
|
148
161
|
if self._connect:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
done, pending = await asyncio.wait(tasks, timeout=self._timeout)
|
|
157
|
-
if pending:
|
|
158
|
-
msg = f"{len(pending)} Devices did not connect:"
|
|
159
|
-
for t in pending:
|
|
160
|
-
t.cancel()
|
|
161
|
-
with suppress(Exception):
|
|
162
|
-
await t
|
|
163
|
-
e = t.exception()
|
|
164
|
-
msg += f"\n {tasks[t]}: {type(e).__name__}"
|
|
165
|
-
lines = str(e).splitlines()
|
|
166
|
-
if len(lines) <= 1:
|
|
167
|
-
msg += f": {e}"
|
|
168
|
-
else:
|
|
169
|
-
msg += "".join(f"\n {line}" for line in lines)
|
|
170
|
-
logging.error(msg)
|
|
171
|
-
raised = [t for t in done if t.exception()]
|
|
172
|
-
if raised:
|
|
173
|
-
logging.error(f"{len(raised)} Devices raised an error:")
|
|
174
|
-
for t in raised:
|
|
175
|
-
logging.exception(f" {tasks[t]}:", exc_info=t.exception())
|
|
176
|
-
if pending or raised:
|
|
177
|
-
raise NotConnected("Not all Devices connected")
|
|
162
|
+
connect_coroutines[name] = obj.connect(
|
|
163
|
+
self._sim, timeout=self._timeout
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Connect to all the devices
|
|
167
|
+
if connect_coroutines:
|
|
168
|
+
await wait_for_connection(**connect_coroutines)
|
|
178
169
|
|
|
179
170
|
async def __aexit__(self, type, value, traceback):
|
|
180
171
|
self._objects_on_exit = self._caller_locals()
|
|
@@ -182,4 +173,12 @@ class DeviceCollector:
|
|
|
182
173
|
|
|
183
174
|
def __exit__(self, type_, value, traceback):
|
|
184
175
|
self._objects_on_exit = self._caller_locals()
|
|
185
|
-
|
|
176
|
+
try:
|
|
177
|
+
fut = call_in_bluesky_event_loop(self._on_exit())
|
|
178
|
+
except RuntimeError:
|
|
179
|
+
raise NotConnected(
|
|
180
|
+
"Could not connect devices. Is the bluesky event loop running? See "
|
|
181
|
+
"https://blueskyproject.io/ophyd-async/main/"
|
|
182
|
+
"user/explanations/event-loop-choice.html for more info."
|
|
183
|
+
)
|
|
184
|
+
return fut
|