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.
Files changed (151) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +97 -62
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +106 -125
  8. ophyd_async/core/_device.py +69 -63
  9. ophyd_async/core/_device_filler.py +65 -1
  10. ophyd_async/core/_flyer.py +14 -5
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +44 -35
  17. ophyd_async/core/_settings.py +36 -27
  18. ophyd_async/core/_signal.py +262 -170
  19. ophyd_async/core/_signal_backend.py +56 -13
  20. ophyd_async/core/_soft_signal_backend.py +16 -11
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +41 -11
  23. ophyd_async/core/_utils.py +96 -49
  24. ophyd_async/core/_yaml_settings.py +2 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/_andor.py +2 -2
  27. ophyd_async/epics/adandor/_andor_controller.py +4 -2
  28. ophyd_async/epics/adandor/_andor_io.py +2 -4
  29. ophyd_async/epics/adaravis/__init__.py +5 -0
  30. ophyd_async/epics/adaravis/_aravis.py +4 -8
  31. ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
  32. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  33. ophyd_async/epics/adcore/__init__.py +23 -8
  34. ophyd_async/epics/adcore/_core_detector.py +42 -2
  35. ophyd_async/epics/adcore/_core_io.py +124 -99
  36. ophyd_async/epics/adcore/_core_logic.py +106 -27
  37. ophyd_async/epics/adcore/_core_writer.py +12 -8
  38. ophyd_async/epics/adcore/_hdf_writer.py +21 -38
  39. ophyd_async/epics/adcore/_single_trigger.py +2 -2
  40. ophyd_async/epics/adcore/_utils.py +2 -2
  41. ophyd_async/epics/adkinetix/__init__.py +2 -1
  42. ophyd_async/epics/adkinetix/_kinetix.py +3 -3
  43. ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
  44. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  45. ophyd_async/epics/adpilatus/__init__.py +5 -0
  46. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  47. ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
  48. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  49. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  50. ophyd_async/epics/adsimdetector/_sim.py +4 -14
  51. ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
  52. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  53. ophyd_async/epics/advimba/__init__.py +10 -1
  54. ophyd_async/epics/advimba/_vimba.py +3 -2
  55. ophyd_async/epics/advimba/_vimba_controller.py +4 -2
  56. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  57. ophyd_async/epics/core/_aioca.py +35 -16
  58. ophyd_async/epics/core/_epics_connector.py +4 -0
  59. ophyd_async/epics/core/_epics_device.py +2 -0
  60. ophyd_async/epics/core/_p4p.py +10 -2
  61. ophyd_async/epics/core/_pvi_connector.py +65 -8
  62. ophyd_async/epics/core/_signal.py +51 -51
  63. ophyd_async/epics/core/_util.py +4 -4
  64. ophyd_async/epics/demo/__init__.py +16 -0
  65. ophyd_async/epics/demo/__main__.py +31 -0
  66. ophyd_async/epics/demo/_ioc.py +32 -0
  67. ophyd_async/epics/demo/_motor.py +82 -0
  68. ophyd_async/epics/demo/_point_detector.py +42 -0
  69. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  70. ophyd_async/epics/demo/_stage.py +15 -0
  71. ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
  72. ophyd_async/epics/demo/point_detector.db +59 -0
  73. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  74. ophyd_async/epics/eiger/_eiger.py +1 -3
  75. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  76. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  77. ophyd_async/epics/eiger/_odin_io.py +1 -2
  78. ophyd_async/epics/motor.py +65 -28
  79. ophyd_async/epics/signal.py +4 -1
  80. ophyd_async/epics/testing/_example_ioc.py +21 -9
  81. ophyd_async/epics/testing/_utils.py +3 -0
  82. ophyd_async/epics/testing/test_records.db +8 -0
  83. ophyd_async/epics/testing/test_records_pva.db +17 -16
  84. ophyd_async/fastcs/__init__.py +1 -0
  85. ophyd_async/fastcs/core.py +6 -0
  86. ophyd_async/fastcs/odin/__init__.py +1 -0
  87. ophyd_async/fastcs/panda/__init__.py +8 -6
  88. ophyd_async/fastcs/panda/_block.py +29 -9
  89. ophyd_async/fastcs/panda/_control.py +5 -0
  90. ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
  91. ophyd_async/fastcs/panda/_table.py +9 -6
  92. ophyd_async/fastcs/panda/_trigger.py +23 -9
  93. ophyd_async/fastcs/panda/_writer.py +27 -30
  94. ophyd_async/plan_stubs/__init__.py +2 -0
  95. ophyd_async/plan_stubs/_ensure_connected.py +1 -0
  96. ophyd_async/plan_stubs/_fly.py +2 -4
  97. ophyd_async/plan_stubs/_nd_attributes.py +2 -0
  98. ophyd_async/plan_stubs/_panda.py +1 -0
  99. ophyd_async/plan_stubs/_settings.py +43 -16
  100. ophyd_async/plan_stubs/_utils.py +3 -0
  101. ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
  102. ophyd_async/sim/__init__.py +24 -14
  103. ophyd_async/sim/__main__.py +43 -0
  104. ophyd_async/sim/_blob_detector.py +33 -0
  105. ophyd_async/sim/_blob_detector_controller.py +48 -0
  106. ophyd_async/sim/_blob_detector_writer.py +105 -0
  107. ophyd_async/sim/_mirror_horizontal.py +46 -0
  108. ophyd_async/sim/_mirror_vertical.py +74 -0
  109. ophyd_async/sim/_motor.py +233 -0
  110. ophyd_async/sim/_pattern_generator.py +124 -0
  111. ophyd_async/sim/_point_detector.py +86 -0
  112. ophyd_async/sim/_stage.py +19 -0
  113. ophyd_async/tango/__init__.py +1 -0
  114. ophyd_async/tango/core/__init__.py +6 -1
  115. ophyd_async/tango/core/_base_device.py +41 -33
  116. ophyd_async/tango/core/_converters.py +81 -0
  117. ophyd_async/tango/core/_signal.py +18 -32
  118. ophyd_async/tango/core/_tango_readable.py +2 -19
  119. ophyd_async/tango/core/_tango_transport.py +136 -60
  120. ophyd_async/tango/core/_utils.py +47 -0
  121. ophyd_async/tango/{sim → demo}/_counter.py +2 -0
  122. ophyd_async/tango/{sim → demo}/_detector.py +2 -0
  123. ophyd_async/tango/{sim → demo}/_mover.py +5 -4
  124. ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
  125. ophyd_async/tango/testing/__init__.py +6 -0
  126. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  127. ophyd_async/testing/__init__.py +29 -7
  128. ophyd_async/testing/_assert.py +145 -83
  129. ophyd_async/testing/_mock_signal_utils.py +56 -70
  130. ophyd_async/testing/_one_of_everything.py +41 -21
  131. ophyd_async/testing/_single_derived.py +89 -0
  132. ophyd_async/testing/_utils.py +3 -0
  133. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
  134. ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
  135. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
  136. ophyd_async/epics/sim/__init__.py +0 -54
  137. ophyd_async/epics/sim/_ioc.py +0 -29
  138. ophyd_async/epics/sim/_mover.py +0 -101
  139. ophyd_async/epics/sim/_sensor.py +0 -37
  140. ophyd_async/epics/sim/sensor.db +0 -19
  141. ophyd_async/sim/_pattern_detector/__init__.py +0 -13
  142. ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
  143. ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
  144. ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
  145. ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
  146. ophyd_async/sim/_sim_motor.py +0 -107
  147. ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
  148. /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
  149. /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
  150. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
  151. {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
@@ -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)
@@ -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)
@@ -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)
@@ -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