Skip to content

Commit

Permalink
Convert PMAC Programrunner to a Flyable (#792)
Browse files Browse the repository at this point in the history
* Make ProgramRunner a flyable so that it can use kickoff and complete

* Make FGS also a flyable as it has kickoff

* First go at having a soft signal for program number

* Add soft signal for collection time

* Add units

* Change program_number soft signal to take an int and work out the string later

* Add an initial value for collection time sfot signal

* Use default timeout for kickoff
  • Loading branch information
noemifrisina authored Sep 20, 2024
1 parent 3f59ce0 commit ee5da2b
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 25 deletions.
3 changes: 2 additions & 1 deletion src/dodal/devices/fast_grid_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import numpy as np
from bluesky.plan_stubs import mv
from bluesky.protocols import Flyable
from numpy import ndarray
from ophyd_async.core import (
AsyncStatus,
Expand Down Expand Up @@ -193,7 +194,7 @@ async def get_value(self, cached: bool | None = None):
return first_grid + second_grid


class FastGridScanCommon(StandardReadable, ABC, Generic[ParamType]):
class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
"""Device for a general fast grid scan
When the motion program is started, the goniometer will move in a snake-like grid trajectory,
Expand Down
61 changes: 42 additions & 19 deletions src/dodal/devices/i24/pmac.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from asyncio import sleep
from enum import Enum, IntEnum
from typing import SupportsFloat

from bluesky.protocols import Triggerable
from bluesky.protocols import Flyable, Triggerable
from ophyd_async.core import (
CALCULATE_TIMEOUT,
DEFAULT_TIMEOUT,
Expand All @@ -12,6 +11,7 @@
SignalRW,
SoftSignalBackend,
StandardReadable,
soft_signal_rw,
wait_for_value,
)
from ophyd_async.epics.motor import Motor
Expand Down Expand Up @@ -118,8 +118,8 @@ async def set(
await self.signal.set(value.value, wait, timeout)


class ProgramRunner(SignalRW):
"""Trigger the collection by setting the program number on the PMAC string.
class ProgramRunner(SignalRW, Flyable):
"""Run the collection by setting the program number on the PMAC string.
Once the program number has been set, wait for the collection to be complete.
This will only be true when the status becomes 0.
Expand All @@ -129,34 +129,48 @@ def __init__(
self,
pmac_str_sig: SignalRW,
status_sig: SignalR,
prog_num_sig: SignalRW,
collection_time_sig: SignalRW,
backend: SignalBackend,
timeout: float | None = DEFAULT_TIMEOUT,
name: str = "",
) -> None:
self.signal = pmac_str_sig
self.status = status_sig
self.prog_num = prog_num_sig

self.collection_time = collection_time_sig
self.KICKOFF_TIMEOUT = timeout

super().__init__(backend, timeout, name)

async def _get_prog_number_string(self) -> str:
prog_num = await self.prog_num.get_value()
return f"&2b{prog_num}r"

@AsyncStatus.wrap
async def set(self, value: int, wait=True, timeout=None):
""" Set the pmac string to the program number and then wait for the scan to \
finish running.
This is done by checking the scan status PV which will go to 1 once the motion \
program starts and back to 0 when it's done. The timeout passed to this set \
should then be the total time required by the scan to finish.
async def kickoff(self):
"""Kick off the collection by sending a program number to the pmac_string and \
wait for the scan status PV to go to 1.
"""
prog_str = f"&2b{value}r"
assert isinstance(timeout, SupportsFloat) or (
timeout is None
), f"ProgramRunner does not support calculating timeout itself, {timeout}"
await self.signal.set(prog_str, wait=wait)
# First wait for signal to go to 1, then wait for the scan to finish.
prog_num_str = await self._get_prog_number_string()
await self.signal.set(prog_num_str, wait=True)
await wait_for_value(
self.status,
ScanState.RUNNING,
timeout=DEFAULT_TIMEOUT,
timeout=self.KICKOFF_TIMEOUT,
)
await wait_for_value(self.status, ScanState.DONE, timeout)

@AsyncStatus.wrap
async def complete(self):
"""Stop collecting when the scan status PV goes to 0.
Args:
complete_time (float): total time required by the collection to \
finish correctly.
"""
scan_complete_time = await self.collection_time.get_value()
await wait_for_value(self.status, ScanState.DONE, timeout=scan_complete_time)


class ProgramAbort(Triggerable):
Expand Down Expand Up @@ -210,8 +224,17 @@ def __init__(self, prefix: str, name: str = "") -> None:
self.scanstatus = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2401")
self.counter = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2402")

# A couple of soft signals for running a collection: program number to send to
# the PMAC_STRING and expected collection time.
self.program_number = soft_signal_rw(int)
self.collection_time = soft_signal_rw(float, initial_value=600.0, units="s")

self.run_program = ProgramRunner(
self.pmac_string, self.scanstatus, backend=SoftSignalBackend(str)
self.pmac_string,
self.scanstatus,
self.program_number,
self.collection_time,
backend=SoftSignalBackend(str),
)
self.abort_program = ProgramAbort(self.pmac_string, self.scanstatus)

Expand Down
18 changes: 13 additions & 5 deletions tests/devices/unit_tests/i24/test_pmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
from bluesky.run_engine import RunEngine
from ophyd_async.core import callback_on_mock_put, get_mock_put, set_mock_value

from dodal.devices.i24.pmac import HOME_STR, PMAC, EncReset, LaserSettings
from dodal.devices.i24.pmac import (
HOME_STR,
PMAC,
EncReset,
LaserSettings,
)
from dodal.devices.util.test_utils import patch_motor


Expand Down Expand Up @@ -66,20 +71,23 @@ async def test_set_pmac_string_for_enc_reset(fake_pmac: PMAC, RE):
assert await fake_pmac.pmac_string.get_value() == "m708=100 m709=150"


async def test_run_proogram(fake_pmac: PMAC, RE):
async def test_run_program(fake_pmac: PMAC, RE):
async def go_high_then_low():
set_mock_value(fake_pmac.scanstatus, 1)
await asyncio.sleep(0.01)
set_mock_value(fake_pmac.scanstatus, 0)

prog_num = 10
callback_on_mock_put(
fake_pmac.pmac_string,
lambda *args, **kwargs: asyncio.create_task(go_high_then_low()), # type: ignore
)
RE(bps.abs_set(fake_pmac.run_program, prog_num, timeout=1, wait=True))

assert await fake_pmac.pmac_string.get_value() == f"&2b{prog_num}r"
set_mock_value(fake_pmac.program_number, 11)
set_mock_value(fake_pmac.collection_time, 2.0)
await fake_pmac.run_program.kickoff()
await fake_pmac.run_program.complete()

assert await fake_pmac.pmac_string.get_value() == "&2b11r"


@patch("dodal.devices.i24.pmac.sleep")
Expand Down

0 comments on commit ee5da2b

Please sign in to comment.