bluesky / bluesky-enhancement-proposals

0 stars 4 forks source link

Add wait_any command and consider accepting status objects directly #9

Open danielballan opened 6 years ago

danielballan commented 6 years ago

It would be useful to be able to express "wait for any of the status objects in this group to complete". Here is an example that is annoyingly convoluted and could be made simpler with 'wait_any':

import bluesky.preprocessors as bpp
import bluesky.plan_stubs as bps
@bpp.run_decorator(md={'plan_name': 'oscillation_plan'})
def oscillation_plan(motor, amplitude=15):
    """
    Ooscillate motor between -ampltiude and amplitude continuously
    while taking sequential readings from pe1.
    """
    # Start the motor moving toward amplitude. Do not wait.
    target_pos = amplitude
    motor_status = yield from bps.abs_set(motor, target_pos, group='foo')

    # Trigger the detector.
    trigger_status = yield from bps.trigger(pe1, group='foo')

    while True:
        # Wait for either the motor to arrive or triggering to finish.
        ### CLEVER BUT ABSTRACTION-BREAKING CODE ###
        or_status = OrStatus(motor_status, trigger_status)
        p_event = asyncio.Event(loop=RE.loop)
        or_status.add_callback(p_event.set)
        yield from bps.wait_for([p_event.wait()])  # waits for EITHER motor to arrive or trigger to finish
        ### END ###
        if motor_status.done:
            # The motor has arrived. Send it to the opposite side.
            target_pos = -target_pos
            motor_status = yield from bps.abs_set(motor, target_pos, group='foo')
        if trigger_status.done:
            # The detector is finished triggering. Read it and then trigger it again.
            yield from bps.create()
            yield from bps.read(pe1)
            yield from bps.save()
            trigger_status = yield from bps.trigger(pe1, group='foo')

See this gist for the original and implementation of OrStatus.

If we had 'wait_any' command, we could replace the troubling code block with:

yield from bps.wait_any('foo')

This raises another issue. The RunEngine expects us to assign each status object to one group at creation time and then address those status objects indirectly via their group labels (in this example, 'foo'). At the very beginning, when we sort of imagined users writings thing like:

def plan():
    yield Msg('trigger', det1, group='foo')
    yield Msg('trigger', det2, group='foo')
    yield Msg('wait', None, 'foo')  # wait for both

that seemed more convenient than

def plan():
    status1 = yield Msg('trigger', det1)
    status2 = yield Msg('trigger', det2)
    yield Msg('wait', None, [status1, status2])

With more perspective, we know that users are rarely acting that this low of a level. I wonder if handling the status objects directly might actually be better. What if I want to wait for different "groups" of status objects at different points in the plan? Groups don't let me do that.

What if Msg('wait', ...) and Msg('wait_any', ...) accepted groups or status objects? I don't think we explicitly require groups to be strings (they can be any hashable object) but in practice they always are. This change could therefore be made in a non-breaking way: if the Msg has a string, that's a group; if the Msg has some other Iterable, treat them as status objects.

danielballan commented 6 years ago

P.S. Tangentially related is this two-year-old issue in ophyd: https://github.com/NSLS-II/ophyd/pull/241. See this comment in particular for why OrStatus didn't get merged. AndStatus was later merged in a different PR.

jrmlhermitte commented 6 years ago

wait_any sounds very useful.

Nice catch about the groups idea. Indeed, one may want to wait for a group, then find the finished element in the group, and wait again with a smaller subset. Static groups don't let that happen.

I'm not so experienced with bluesky but I definitely see these things being useful you've mentioned :-)