-
Notifications
You must be signed in to change notification settings - Fork 28
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 functions for saving device #18
Conversation
for attr_name, attr in device.children(): | ||
dot = "" | ||
# Place a dot inbetween the upper and lower class. | ||
# Don't do this for highest level class. | ||
if path_prefix: | ||
dot = "." | ||
dot_path = f"{path_prefix}{dot}{attr_name}" | ||
if type(attr) is SignalRW: | ||
signalRWs[dot_path] = attr | ||
get_signal_RWs_from_device(attr, path_prefix=dot_path) | ||
return signalRWs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a mutable default arg will cause problems:
https://web.archive.org/web/20200221224620id_/http://effbot.org/zone/default-values.htm
I suggest you return a dict and update with the child devices.
for attr_name, attr in device.children(): | |
dot = "" | |
# Place a dot inbetween the upper and lower class. | |
# Don't do this for highest level class. | |
if path_prefix: | |
dot = "." | |
dot_path = f"{path_prefix}{dot}{attr_name}" | |
if type(attr) is SignalRW: | |
signalRWs[dot_path] = attr | |
get_signal_RWs_from_device(attr, path_prefix=dot_path) | |
return signalRWs | |
signals: Dict[str, SignalRW] = {} | |
for attr_name, attr in device.children(): | |
dot_path = f"{path_prefix}{attr_name}" | |
if isinstance(attr, SignalRW): | |
signals[dot_path] = attr | |
attr_signals = get_signal_RWs_from_device(attr, path_prefix = dot_path + ".") | |
signals.update(attr_signals) | |
return signals |
from ophyd_async.core import Device, SignalRW | ||
|
||
|
||
def get_signal_RWs_from_device( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think PEP8 doesn't like uppercase in functions names. How about walk_rw_signals(device, path_prefix)
instead?
value[inner_key] = inner_value.tolist() | ||
# Convert enums to their values | ||
elif isinstance(signal_values[index], Enum): | ||
signal_values[index] = value.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we should assert isinstance(str)
as all Enums should be instances of strings
return signalRWs | ||
|
||
|
||
def save_device(device: Device, savename: str, ignore: List[str] = []): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would put this phase sorting and the ignore list in a device specific plan, and also move the yaml saving to another function:
def save_device(device: Device, savename: str, ignore: List[str] = []): | |
def save_signals(signals: List[Dict[str, SignalRW]], ignore: Sequence[str] = ()) -> List[Dict[str, Any]]: |
5b98f9f
to
30346d5
Compare
Now you make a plan using a series of utility functions in
|
if ignore is None: | ||
ignore = [""] | ||
|
||
values = yield Msg("locate", *signals.values()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will locate signals in the ignore list, so probably nest to exclude them. Also we want to make sure the values are None
in case we want to use a save file as a source of ignores for the future.
How about something like:
ignore = ignore or []
selected_signals = {key: signal for key, signal in signals.items() if key not in ignore}
selected_values = yield Msg("locate", *selected_signals.values())
named_values = {key, value["setpoint"] for key, value in zip(selected_signals, selected_values)}
# Ignored values place in with value None so we know which ones were ignored
named_values.update({key: None for key in ignore})
|
||
|
||
def walk_rw_signals( | ||
device: Device, path_prefix: Optional[str] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
device: Device, path_prefix: Optional[str] = None | |
device: Device, path_prefix: str = "" |
Strings aren't mutable so fine to use the empty string as a default
phase_outputs.append(phase) | ||
|
||
with open(save_path, "w") as file: | ||
yaml.dump(phase_outputs, file) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can do the numpy formatting here with a custom yaml dumper, at least you can in ruamel.yaml, not sure about pyyaml. That might be cleaner than preprocessing the structures by hand...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense - Do you think it's worth doing this for Enums as well? I don't think this can be done in the same way since yaml.add_representer
doesn't appear to work for subclasses
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe? Although if we make sure we define MyEnum(str, Enum)
then it should just serialize as it is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do this, it will automatically serialize it as, eg
my_enum: !!python/object/apply:test_device_save_loader.EnumTest
- val1
and can then reads this value as a string when doing yaml.load()
. Otherwise I can keep the logic which converts the enum object to a simple string when serialising.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, we want to make sure the yaml contains a plain string, so I guess we do need to do that pre-processing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, this is back now
ab8e478
to
5fbb60a
Compare
|
||
RE(save_my_device()) | ||
|
||
yaml.add_constructor("tag:!numpy.ndarray", ndarray_constructor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the YAML file have these tags in it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it does, then we should use a dumper that allows transformation of the data by type (which may fix the enums too):
https://stackoverflow.com/questions/65866546/how-to-make-ruamel-yaml-dump-formatted-numpy-arrays-as-simple-list
I have made a PR from my fork. Still using PyYAML - ruamel.yaml was more difficult to get pretty formatting out of. |
Superseded by #36 |
For the first part of #16. Adds a set of functions used to build save plans of a device.