Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Watchable AsyncStatus and extend the wrap decorator #176

Merged
merged 24 commits into from
May 17, 2024

Conversation

dperl-dls
Copy link
Contributor

Fixes #117, fixes #45
As according to discussion in #117, adds a WatchableAsyncStatus, and improves the wrappers so that they can be applied to any function returning an Awaitable or an AsyncIterator

Will break anything previously passing watchers to AsyncStatus which will need to update to WatchableAsyncStatus (but there hopefully isn't much out there yet)

src/ophyd_async/core/async_status.py Outdated Show resolved Hide resolved
src/ophyd_async/core/utils.py Outdated Show resolved Hide resolved
src/ophyd_async/core/async_status.py Outdated Show resolved Hide resolved
src/ophyd_async/core/async_status.py Outdated Show resolved Hide resolved
Copy link
Collaborator

@coretl coretl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had more thoughts...

src/ophyd_async/core/utils.py Outdated Show resolved Hide resolved
src/ophyd_async/core/async_status.py Outdated Show resolved Hide resolved
Copy link
Collaborator

@coretl coretl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a general preference for timeout rather than timeout_s as it matches all the asyncio arguments, and also the ophyd convention

src/ophyd_async/core/async_status.py Outdated Show resolved Hide resolved
src/ophyd_async/core/async_status.py Outdated Show resolved Hide resolved
src/ophyd_async/core/async_status.py Outdated Show resolved Hide resolved
src/ophyd_async/core/async_status.py Show resolved Hide resolved
@dperl-dls dperl-dls force-pushed the 117_45_extend_asyncstatus_wrap branch 5 times, most recently from 335a125 to db312ac Compare April 19, 2024 12:51
@dperl-dls dperl-dls force-pushed the 117_45_extend_asyncstatus_wrap branch from db312ac to c0ab271 Compare April 19, 2024 12:53
@dperl-dls dperl-dls force-pushed the 117_45_extend_asyncstatus_wrap branch from 64452fc to 493ba2a Compare April 19, 2024 15:05
src/ophyd_async/epics/motion/motor.py Outdated Show resolved Hide resolved
src/ophyd_async/core/utils.py Outdated Show resolved Hide resolved
@dperl-dls dperl-dls requested a review from coretl April 23, 2024 10:19
src/ophyd_async/core/signal.py Show resolved Hide resolved
src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/utils.py Outdated Show resolved Hide resolved
src/ophyd_async/epics/demo/__init__.py Outdated Show resolved Hide resolved
src/ophyd_async/epics/demo/__init__.py Outdated Show resolved Hide resolved
return AsyncStatus(coro, watchers)
@WatchableAsyncStatus.wrap
async def set(self, new_position: float, timeout: float = 0.0):
update, move_status = await self._move(new_position)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and inlining _move

update, move_status = await self._move(new_position)
start = time.monotonic()
async for current_position in observe_value(
self.user_readback, done_status=move_status
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But here the done_status=move_status is correct as we are talking to a motor record

@@ -54,7 +54,7 @@ def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStat
"""
watchers: List[Callable] = []
coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout)
return AsyncStatus(coro, watchers)
return AsyncStatus(coro)

async def _move(self, new_position: float, watchers: List[Callable] = []):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please can we use the same wrap logic here? Something like

Suggested change
async def _move(self, new_position: float, watchers: List[Callable] = []):
@WatchableAsyncStatus.wrap
async def set(self, new_position: float) -> :

then yielding WatcherUpdate instead of calling watchers

tests/core/test_async_status.py Show resolved Hide resolved
src/ophyd_async/core/async_status.py Show resolved Hide resolved
Copy link
Collaborator

@coretl coretl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just spotted this...

class ASTestDevice(StandardReadable, Movable):
def __init__(self, name: str = "") -> None:
self._staged: bool = False
self.sig = SignalR(backend=SimSignalBackend(datatype=int))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be soft_signal_r_and_backend

@dperl-dls dperl-dls changed the title Improve AsyncStatus (and wrap) Add a Watchable AsyncStatus and extend the wrap decorator May 1, 2024
tests/core/test_async_status.py Show resolved Hide resolved
src/ophyd_async/core/async_status.py Show resolved Hide resolved
src/ophyd_async/core/async_status.py Outdated Show resolved Hide resolved
@AsyncStatus.wrap
async def kickoff(self) -> None:
self._fly_status = AsyncStatus(self._fly(), self._watchers)
def kickoff(self, timeout: Optional[float] = None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original code kicked off self._observe_writer_indices and returned immediately, then returned this status in complete. This code now actually awaits this Status, which is wrong.

However, I think a simplification of the code is the better fix:

    @AsyncStatus.wrap
    async def kickoff(self) -> None:
        # Nothing to do in kickoff, we already armed in prepare
        pass

    @WatchableAsyncStatus.wrap
    async def complete(self):
        async for index in self.writer.observe_indices_written(self._frame_writing_timeout):    
            yield WatcherUpdate(
                name=self.name,
                current=index,
                initial=self._initial_frame,
                target=end_observation,
                unit="",
                precision=0,
                time_elapsed=time.monotonic() - self._fly_start,
            )
            if index >= end_observation:
                break

Then can delete _fly and _observe_writer_indices...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flyable tells us that kickoff and complete must both return statuses.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, they're both being wrapped by *AsyncStatus so return a Status. It's just that kickoff returns one that is immediately done...

Comment on lines +112 to +113
@WatchableAsyncStatus.wrap # uses the timeout argument from the function it wraps
async def set(self, new_position: float, timeout: float | None = None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't see timeout being used below...

Suggested change
@WatchableAsyncStatus.wrap # uses the timeout argument from the function it wraps
async def set(self, new_position: float, timeout: float | None = None):
@WatchableAsyncStatus.wrap
async def set(self, new_position: float):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the WatchableAsyncStatus wrapper uses the timeout argument from functions that it wraps, as we discussed in detail earlier in this PR

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but doesn't it add that to the signature itself? So the function doesn't need to mention timeout at all...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It pulls it from kwargs, so the function still needs to either take timeout or **kwargs

tests/core/test_device_collector.py Outdated Show resolved Hide resolved
@@ -76,8 +100,9 @@ async def wait_for_call(self, *args, **kwargs):


async def test_mover_moving_well(sim_mover: demo.Mover) -> None:
set_sim_put_proceeds(sim_mover.setpoint, False)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These shouldn't be needed

Suggested change
set_sim_put_proceeds(sim_mover.setpoint, False)

tests/epics/demo/test_demo.py Outdated Show resolved Hide resolved
Comment on lines +92 to +93
# Trigger any callbacks
await self.user_readback._backend.put(await self.user_readback.get_value())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be needed now we added the done_status argument above?

Suggested change
# Trigger any callbacks
await self.user_readback._backend.put(await self.user_readback.get_value())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed for that specific status, but if you have any other callbacks here and call stop any other way it is

if time.monotonic() > timeout_time:
raise TimeoutError


async def test_motor_moving_well(sim_motor: motor.Motor) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please could you revert this test and check it still passes?

Copy link
Contributor Author

@dperl-dls dperl-dls May 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't the way it is in main - it needs a sleep after the set_mock_value(readback ...) to process the call to the watcher. This slight alteration does pass:

async def test_motor_moving_well_2(sim_motor: motor.Motor) -> None:
    set_mock_put_proceeds(sim_motor.user_setpoint, False)
    s = sim_motor.set(0.55)
    watcher = Mock()
    s.watch(watcher)
    done = Mock()
    s.add_callback(done)
    await asyncio.sleep(A_BIT)
    assert watcher.call_count == 1
    assert watcher.call_args == call(
        name="sim_motor",
        current=0.0,
        initial=0.0,
        target=0.55,
        unit="mm",
        precision=3,
        time_elapsed=pytest.approx(0.0, abs=0.05),
    )
    watcher.reset_mock()
    assert 0.55 == await sim_motor.user_setpoint.get_value()
    assert not s.done
    await asyncio.sleep(0.1)
    set_mock_value(sim_motor.user_readback, 0.1)
    await asyncio.sleep(0.1)
    assert watcher.call_count == 1
    assert watcher.call_args == call(
        name="sim_motor",
        current=0.1,
        initial=0.0,
        target=0.55,
        unit="mm",
        precision=3,
        time_elapsed=pytest.approx(0.11, abs=0.05),
    )
    set_mock_put_proceeds(sim_motor.user_setpoint, True)
    await asyncio.sleep(A_BIT)
    assert s.done
    done.assert_called_once_with(s)

@dperl-dls dperl-dls force-pushed the 117_45_extend_asyncstatus_wrap branch from a61afc0 to 2a12be3 Compare May 16, 2024 10:40
@dperl-dls dperl-dls requested a review from coretl May 16, 2024 10:41
@dperl-dls dperl-dls force-pushed the 117_45_extend_asyncstatus_wrap branch from f373fd8 to 5691fcf Compare May 17, 2024 10:06
@dperl-dls dperl-dls merged commit 1c0e20e into main May 17, 2024
18 checks passed
@dperl-dls dperl-dls deleted the 117_45_extend_asyncstatus_wrap branch May 17, 2024 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for watchers into AsyncStatus.wrap limited functionality of AsyncStatus wrap
2 participants