Skip to content

Commit

Permalink
Rewrite sim and epics.demo devices to do the same
Browse files Browse the repository at this point in the history
  • Loading branch information
coretl committed Jan 20, 2025
1 parent 4bbcb0b commit 20b4aa7
Show file tree
Hide file tree
Showing 41 changed files with 934 additions and 1,052 deletions.
12 changes: 5 additions & 7 deletions src/ophyd_async/core/_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class TriggerInfo(BaseModel):
#: Sort of triggers that will be sent
trigger: DetectorTrigger = Field(default=DetectorTrigger.INTERNAL)
#: What is the minimum deadtime between triggers
deadtime: float | None = Field(default=None, ge=0)
deadtime: float = Field(default=0.0, ge=0)
#: What is the maximum high time of the triggers
livetime: float | None = Field(default=None, ge=0)
#: What is the maximum timeout on waiting for a frame
Expand Down Expand Up @@ -281,9 +281,6 @@ async def trigger(self) -> None:
TriggerInfo(
number_of_triggers=1,
trigger=DetectorTrigger.INTERNAL,
deadtime=None,
livetime=None,
frame_timeout=None,
)
)

Expand All @@ -301,7 +298,7 @@ async def trigger(self) -> None:
async for index in self._writer.observe_indices_written(
DEFAULT_TIMEOUT
+ (self._trigger_info.livetime or 0)
+ (self._trigger_info.deadtime or 0)
+ self._trigger_info.deadtime
):
if index >= end_observation:
break
Expand Down Expand Up @@ -332,17 +329,18 @@ async def prepare(self, value: TriggerInfo) -> None:
f"deadtime, but trigger logic provides only {value.deadtime}s"
)
raise ValueError(msg)

elif not value.deadtime:
value.deadtime = self._controller.get_deadtime(value.livetime)
self._trigger_info = value
self._number_of_triggers_iter = iter(
self._trigger_info.number_of_triggers
if isinstance(self._trigger_info.number_of_triggers, list)
else [self._trigger_info.number_of_triggers]
)
self._initial_frame = await self._writer.get_indices_written()
self._describe, _ = await asyncio.gather(
self._writer.open(value.multiplier), self._controller.prepare(value)
)
self._initial_frame = await self._writer.get_indices_written()
if value.trigger != DetectorTrigger.INTERNAL:
await self._controller.arm()
self._fly_start = time.monotonic()
Expand Down
4 changes: 2 additions & 2 deletions src/ophyd_async/core/_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ class StaticPathProvider(PathProvider):
def __init__(
self,
filename_provider: FilenameProvider,
directory_path: Path,
directory_path: Path | str,
create_dir_depth: int = 0,
) -> None:
self._filename_provider = filename_provider
self._directory_path = directory_path
self._directory_path = Path(directory_path)
self._create_dir_depth = create_dir_depth

def __call__(self, device_name: str | None = None) -> PathInfo:
Expand Down
6 changes: 4 additions & 2 deletions src/ophyd_async/core/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import numpy as np

T = TypeVar("T")
V = TypeVar("V")
P = ParamSpec("P")
Callback = Callable[[T], None]
DEFAULT_TIMEOUT = 10.0
Expand Down Expand Up @@ -229,8 +230,9 @@ async def merge_gathered_dicts(
return ret


async def gather_list(coros: Iterable[Awaitable[T]]) -> list[T]:
return await asyncio.gather(*coros)
async def gather_dict(coros: dict[T, Awaitable[V]]) -> dict[T, V]:
values = await asyncio.gather(*coros.values())
return dict(zip(coros, values, strict=True))


def in_micros(t: float) -> int:
Expand Down
58 changes: 10 additions & 48 deletions src/ophyd_async/epics/demo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,16 @@
"""Demo EPICS Devices for the tutorial"""

import atexit
import random
import string
import subprocess
import sys
from pathlib import Path

from ._mover import Mover, SampleStage
from ._sensor import EnergyMode, Sensor, SensorGroup
from ._ioc import start_ioc_subprocess
from ._motor import DemoMotor
from ._point_detector import DemoPointDetector
from ._point_detector_channel import DemoPointDetectorChannel, EnergyMode
from ._stage import DemoStage

__all__ = [
"Mover",
"SampleStage",
"DemoMotor",
"DemoStage",
"EnergyMode",
"Sensor",
"SensorGroup",
"DemoPointDetectorChannel",
"DemoPointDetector",
"start_ioc_subprocess",
]


def start_ioc_subprocess() -> str:
"""Start an IOC subprocess with EPICS database for sample stage and sensor
with the same pv prefix
"""

pv_prefix = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + ":"
here = Path(__file__).absolute().parent
args = [sys.executable, "-m", "epicscorelibs.ioc"]

# Create standalone sensor
args += ["-m", f"P={pv_prefix}"]
args += ["-d", str(here / "sensor.db")]

# Create sensor group
for suffix in ["1", "2", "3"]:
args += ["-m", f"P={pv_prefix}{suffix}:"]
args += ["-d", str(here / "sensor.db")]

# Create X and Y motors
for suffix in ["X", "Y"]:
args += ["-m", f"P={pv_prefix}{suffix}:"]
args += ["-d", str(here / "mover.db")]

# Start IOC
process = subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
)
atexit.register(process.communicate, "exit")
return pv_prefix
7 changes: 3 additions & 4 deletions src/ophyd_async/epics/demo/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@

# Start IOC with demo pvs in subprocess
prefix = testing.generate_random_pv_prefix()
prefix = "foo:"
demo.start_ioc_subprocess(prefix, num_counters=3)
demo.start_ioc_subprocess(prefix, num_channels=3)

# All Devices created within this block will be
# connected and named at the end of the with block
with init_devices():
# Create a sample stage with X and Y motors
stage = demo.Stage(f"{prefix}STAGE:")
stage = demo.DemoStage(f"{prefix}STAGE:")
# Create a multi channel counter with the same number
# of counters as the IOC
mcc = demo.MultiChannelCounter(f"{prefix}MCC:")
det1 = demo.DemoPointDetector(f"{prefix}DET:")
12 changes: 6 additions & 6 deletions src/ophyd_async/epics/demo/_ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@
HERE = Path(__file__).absolute().parent


def start_ioc_subprocess(prefix: str, num_counters: int):
def start_ioc_subprocess(prefix: str, num_channels: int):
"""Start an IOC subprocess with EPICS database for sample stage and sensor
with the same pv prefix
"""
ioc = TestingIOC()
# Create X and Y motors
for suffix in ["X", "Y"]:
ioc.add_database(HERE / "mover.db", P=f"{prefix}STAGE:{suffix}:")
ioc.add_database(HERE / "motor.db", P=f"{prefix}STAGE:{suffix}:")
# Create a multichannel counter with num_counters
ioc.add_database(HERE / "multichannelcounter.db", P=f"{prefix}MCC:")
for i in range(1, num_counters + 1):
ioc.add_database(HERE / "point_detector.db", P=f"{prefix}DET:")
for i in range(1, num_channels + 1):
ioc.add_database(
HERE / "counter.db",
P=f"{prefix}MCC:",
HERE / "point_detector_channel.db",
P=f"{prefix}DET:",
CHANNEL=str(i),
X=f"{prefix}STAGE:X:",
Y=f"{prefix}STAGE:Y:",
Expand Down
75 changes: 75 additions & 0 deletions src/ophyd_async/epics/demo/_motor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import asyncio
from typing import Annotated as A

import numpy as np
from bluesky.protocols import Movable, Stoppable

from ophyd_async.core import (
CALCULATE_TIMEOUT,
DEFAULT_TIMEOUT,
CalculatableTimeout,
SignalR,
SignalRW,
SignalX,
StandardReadable,
WatchableAsyncStatus,
WatcherUpdate,
observe_value,
)
from ophyd_async.core import StandardReadableFormat as Format
from ophyd_async.epics.core import EpicsDevice, PvSuffix


class DemoMotor(EpicsDevice, StandardReadable, Movable, Stoppable):
"""A demo movable that moves based on velocity"""

# Whether set() should complete successfully or not
_set_success = True
# Define some signals
readback: A[SignalR[float], PvSuffix("Readback"), Format.HINTED_SIGNAL]
velocity: A[SignalRW[float], PvSuffix("Velocity"), Format.CONFIG_SIGNAL]
units: A[SignalR[str], PvSuffix("Readback.EGU"), Format.CONFIG_SIGNAL]
setpoint: A[SignalRW[float], PvSuffix("Setpoint")]
precision: A[SignalR[int], PvSuffix("Readback.PREC")]
# If a signal name clashes with a bluesky verb add _ to the attribute name
stop_: A[SignalX, PvSuffix("Stop.PROC")]

def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
super().set_name(name, child_name_separator=child_name_separator)
# Readback should be named the same as its parent in read()
self.readback.set_name(name)

@WatchableAsyncStatus.wrap
async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
new_position = value
self._set_success = True
old_position, units, precision, velocity = await asyncio.gather(
self.setpoint.get_value(),
self.units.get_value(),
self.precision.get_value(),
self.velocity.get_value(),
)
if timeout == CALCULATE_TIMEOUT:
timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
# Wait for the value to set, but don't wait for put completion callback
await self.setpoint.set(new_position, wait=False)
async for current_position in observe_value(
self.readback, done_timeout=timeout
):
yield WatcherUpdate(
current=current_position,
initial=old_position,
target=new_position,
name=self.name,
unit=units,
precision=precision,
)
if np.isclose(current_position, new_position):
break
if not self._set_success:
raise RuntimeError("Motor was stopped")

async def stop(self, success=True):
self._set_success = success
status = self.stop_.trigger()
await status
101 changes: 0 additions & 101 deletions src/ophyd_async/epics/demo/_mover.py

This file was deleted.

Loading

0 comments on commit 20b4aa7

Please sign in to comment.