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 type annotations to plans and plan stubs, update docstrings #1610

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

callumforrester
Copy link
Contributor

@callumforrester callumforrester commented Aug 30, 2023

Description

Add type annotations to plans and plan stubs. IDEs such as vscode should now provide hints when using them. See screenshot below. Fix and update out-of-date docstrings on plans and plan stubs.

Motivation and Context

Makes development with plans easier with auto-fill etc. Also allows downstream projects to make use of static type checking utilities such as mypy.

How Has This Been Tested?

Docstrings and type annotations should not semantically change the code. All tests passing supports this.

Screenshots (if appropriate):

image
image

@dmgav
Copy link
Contributor

dmgav commented Aug 30, 2023

There is an existing PR with similar changes: #1600
This PR looks better organized and also covers stubs, so it may replace the existing PR.

bluesky/plans.py Outdated


def count(
detectors: List[Readable],
Copy link
Contributor

@dmgav dmgav Aug 31, 2023

Choose a reason for hiding this comment

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

This should probably be Union[List[Readable], Tuple[Readable]] for generality. Using Iterable[Readable] will cause issues with Queue Server parameter validation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I think Sequence works as a supertype of both. Would Sequence[Readable] work with Queue Server validation? It's much more concise.

Copy link
Contributor

Choose a reason for hiding this comment

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

I made some changes to the Queue Server. Using Iterable is good now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think iterable will work for these plans though. Some of them don't actually accept an iterable because they do random access. If you pass them an iterable they'll break. See example below: passing count a generator of detectors (a valid iterable) results in events that hold no information about those detectors:

>>> from ophyd.sim import det1, det2, motor1
>>> from bluesky import RunEngine
>>> import bluesky.plans as bp
>>> from pprint import pprint
>>> 
>>> def detector_generator():
...     yield det1
...     yield det2
... 
>>> RE = RunEngine()
>>> RE(bp.count([det1, det2]), lambda name, doc: pprint({"name": name, "doc": doc}))
{'doc': {'detectors': ['det1', 'det2'],
         'hints': {'dimensions': [(('time',), 'primary')]},
         'num_intervals': 0,
         'num_points': 1,
         'plan_args': {'detectors': ["SynGauss(prefix='', name='det1', "
                                     "read_attrs=['val'], "
                                     "configuration_attrs=['Imax', 'center', "
                                     "'sigma', 'noise', 'noise_multiplier'])",
                                     "SynGauss(prefix='', name='det2', "
                                     "read_attrs=['val'], "
                                     "configuration_attrs=['Imax', 'center', "
                                     "'sigma', 'noise', 'noise_multiplier'])"],
                       'num': 1},
         'plan_name': 'count',
         'plan_type': 'generator',
         'scan_id': 1,
         'time': 1694075006.16437,
         'uid': 'a76d7658-12b7-4296-b879-22513207a20a',
         'versions': {'bluesky': '1.10.0+91.gbf8d9fd4', 'ophyd': '1.8.0'}},
 'name': 'start'}
{'doc': {'configuration': {'det1': {'data': {'det1_Imax': 5,
                                             'det1_center': 0,
                                             'det1_noise': 'none',
                                             'det1_noise_multiplier': 1,
                                             'det1_sigma': 0.5},
                                    'data_keys': OrderedDict([('det1_Imax',
                                                               {'dtype': 'integer',
                                                                'shape': [],
                                                                'source': 'SIM:det1_Imax'}),
                                                              ('det1_center',
                                                               {'dtype': 'integer',
                                                                'shape': [],
                                                                'source': 'SIM:det1_center'}),
                                                              ('det1_sigma',
                                                               {'dtype': 'number',
                                                                'shape': [],
                                                                'source': 'SIM:det1_sigma'}),
                                                              ('det1_noise',
                                                               {'dtype': 'integer',
                                                                'enum_strs': ('none',
                                                                              'poisson',
                                                                              'uniform'),
                                                                'shape': [],
                                                                'source': 'SIM:det1_noise'}),
                                                              ('det1_noise_multiplier',
                                                               {'dtype': 'integer',
                                                                'shape': [],
                                                                'source': 'SIM:det1_noise_multiplier'})]),
                                    'timestamps': {'det1_Imax': 1694074921.2635274,
                                                   'det1_center': 1694074921.263523,
                                                   'det1_noise': 1694074921.2635052,
                                                   'det1_noise_multiplier': 1694074921.2635176,
                                                   'det1_sigma': 1694074921.2635317}},
                           'det2': {'data': {'det2_Imax': 2,
                                             'det2_center': 1,
                                             'det2_noise': 'none',
                                             'det2_noise_multiplier': 1,
                                             'det2_sigma': 2},
                                    'data_keys': OrderedDict([('det2_Imax',
                                                               {'dtype': 'integer',
                                                                'shape': [],
                                                                'source': 'SIM:det2_Imax'}),
                                                              ('det2_center',
                                                               {'dtype': 'integer',
                                                                'shape': [],
                                                                'source': 'SIM:det2_center'}),
                                                              ('det2_sigma',
                                                               {'dtype': 'integer',
                                                                'shape': [],
                                                                'source': 'SIM:det2_sigma'}),
                                                              ('det2_noise',
                                                               {'dtype': 'integer',
                                                                'enum_strs': ('none',
                                                                              'poisson',
                                                                              'uniform'),
                                                                'shape': [],
                                                                'source': 'SIM:det2_noise'}),
                                                              ('det2_noise_multiplier',
                                                               {'dtype': 'integer',
                                                                'shape': [],
                                                                'source': 'SIM:det2_noise_multiplier'})]),
                                    'timestamps': {'det2_Imax': 1694074921.2637875,
                                                   'det2_center': 1694074921.2637837,
                                                   'det2_noise': 1694074921.2637644,
                                                   'det2_noise_multiplier': 1694074921.2637773,
                                                   'det2_sigma': 1694074921.2637923}}},
         'data_keys': {'det1': {'dtype': 'number',
                                'object_name': 'det1',
                                'precision': 3,
                                'shape': [],
                                'source': 'SIM:det1'},
                       'det2': {'dtype': 'number',
                                'object_name': 'det2',
                                'precision': 3,
                                'shape': [],
                                'source': 'SIM:det2'}},
         'hints': {'det1': {'fields': ['det1']}, 'det2': {'fields': ['det2']}},
         'name': 'primary',
         'object_keys': {'det1': ['det1'], 'det2': ['det2']},
         'run_start': 'a76d7658-12b7-4296-b879-22513207a20a',
         'time': 1694075006.1660883,
         'uid': 'c84977c2-c35a-4a5e-8e1a-d4f18570b885'},
 'name': 'descriptor'}
{'doc': {'data': {'det1': 5.0, 'det2': 1.7649938051691907},
         'descriptor': 'c84977c2-c35a-4a5e-8e1a-d4f18570b885',
         'filled': {},
         'seq_num': 1,
         'time': 1694075006.1714554,
         'timestamps': {'det1': 1694075006.1706445, 'det2': 1694075006.1710362},
         'uid': 'f6507c44-3224-44f2-bfb2-6cf765dd20dd'},
 'name': 'event'}
{'doc': {'exit_status': 'success',
         'num_events': {'primary': 1},
         'reason': '',
         'run_start': 'a76d7658-12b7-4296-b879-22513207a20a',
         'time': 1694075006.1717513,
         'uid': '167a66b4-b58c-4cbf-9361-8f65c45fcf38'},
 'name': 'stop'}
('a76d7658-12b7-4296-b879-22513207a20a',)
>>> RE(bp.count(detector_generator()), lambda name, doc: pprint({"name": name, "doc": doc}))
{'doc': {'detectors': ['det1', 'det2'],
         'hints': {'dimensions': [(('time',), 'primary')]},
         'num_intervals': 0,
         'num_points': 1,
         'plan_args': {'detectors': [], 'num': 1},
         'plan_name': 'count',
         'plan_type': 'generator',
         'scan_id': 2,
         'time': 1694075016.1725795,
         'uid': 'cad1615b-f34f-4c17-bac7-c43eaaae8019',
         'versions': {'bluesky': '1.10.0+91.gbf8d9fd4', 'ophyd': '1.8.0'}},
 'name': 'start'}
{'doc': {'configuration': {},
         'data_keys': {},
         'hints': {},
         'name': 'primary',
         'object_keys': {},
         'run_start': 'cad1615b-f34f-4c17-bac7-c43eaaae8019',
         'time': 1694075016.1734006,
         'uid': '7a91a155-0fd6-439e-a0f3-58a4d1deaa62'},
 'name': 'descriptor'}
{'doc': {'exit_status': 'success',
         'num_events': {},
         'reason': '',
         'run_start': 'cad1615b-f34f-4c17-bac7-c43eaaae8019',
         'time': 1694075016.1739008,
         'uid': '36c8009d-13d6-411a-88a8-5507b7680fcb'},
 'name': 'stop'}
('cad1615b-f34f-4c17-bac7-c43eaaae8019',)
>>> 

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point. May be the right solution is to convert detectors (and motors) to a list in the plan wherever it is possible and annotate the respective parameters as Iterable for generality? @danielballan

Copy link
Contributor

Choose a reason for hiding this comment

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

We can also leave it as List. The Queue Server always using lists.

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 doesn't support Sequence then?

Copy link
Contributor

@dmgav dmgav Sep 7, 2023

Choose a reason for hiding this comment

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

It does. We can use Sequence

Copy link
Member

Choose a reason for hiding this comment

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

I'm generally in favor of "Accept lazy iterables (generators) everywhere!" but I think that it's pretty reasonable to expect a Sequence here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think make it Sequence here to reflect what it will accept and make a separate issue for making all plans accept lazy iterables. #1613



def grid_scan(detectors, *args, snake_axes=None, per_step=None, md=None):
def grid_scan(
detectors,
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CustomPlanMetadata = Dict[str, Any]

#: Scalar or iterable of values, one to be applied to each point in a scan
ScalarOrIterable = Union[float, Iterable[float]]
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it be ScalarOrIterableFloat or ScalarOrIterableNumber for clarity?

bluesky/plans.py Outdated
def rel_list_scan(detectors, *args, per_step=None, md=None):
def rel_list_scan(
detectors: List[Readable],
*args: Any,
Copy link
Contributor

Choose a reason for hiding this comment

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

Here is another issue with the Queue Server, since it will not attempt to convert strings to objects (motors in this particular example) if parameter is annotated with a type not associated with Ophyd devices (such as Readable, Movable etc). So in this case *args should have no annotation (by default the queue server tries to convert all strings to objects) or the type should be something like Union[Movable, Any]. It should also work with mypy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting, for blueapi we went the opposite way. Only attempts to convert strings if they are annotated with protocol types or things that follow protocol types.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is what the Queue Server does. But it also attempts to convert all strings it finds in values of parameters that are not annotated. This may lead to errors, since any string matching a plan or a device name will be converted to object, but this never happened in practice. Unfortunately, our local users do not want to learn about type annotations, which makes this feature very useful.

Copy link
Contributor

@dmgav dmgav Sep 13, 2023

Choose a reason for hiding this comment

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

So a parameter annotated with typing.Any is considered having type and the Queue Server does not try to convert strings to objects.

@callumforrester
Copy link
Contributor Author

@dmgav as an aside in terms of types the queueserver supports, I've been using the types supported by pydantic as a guide: https://docs.pydantic.dev/1.10/usage/types/, it provides a useful rule of thumb.

@dmgav
Copy link
Contributor

dmgav commented Sep 7, 2023

Unfortunately, Pydantic functionality is insufficient for proper type checking. The problem with Iterable, for example, is that if the parameter type is Iterable[str], Pydantic is absolutely happy with simply a string value. It also accepts a number represented as a string for parameters instead of float or int. Pydatic v1 is even less strict. The problem is not to make the queue server 'accept' the parameter values, but to detect mismatch of types and decide which values has to be converted to objects. There is some post-processing for this.

@callumforrester
Copy link
Contributor Author

Indeed, pydantic is a bit lenient, it's more using the list of types as a guide for what to support than actually using the pydantic library itself

Copy link
Member

@danielballan danielballan left a comment

Choose a reason for hiding this comment

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

Thanks for the docstring fix-ups as well. Is it worth declaring a custom type like MoveGroup or Group for group? I see Optional[Hashable] a lot. We may some day want to pin that down a bit more.

@callumforrester
Copy link
Contributor Author

CI issues fixed in: #1612

@callumforrester
Copy link
Contributor Author

@danielballan Could make a custom type but I wouldn't like to hide away the Optional, it seems perfectly reasonable to write a plan that takes a group that cannot be none, so it would just be Optional[Group] for the current plans rather than Optional[Hashable]. If that sounds useful I'm happy to add it in.

As an aside, I note that wait takes None as the default for group. Do you know why that is? It seems like a perfect candidate for requiring a value because it's just a noop with None, right?

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.

None yet

3 participants