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 functions for saving device #18

Closed
wants to merge 13 commits into from
Closed

Add functions for saving device #18

wants to merge 13 commits into from

Conversation

olliesilvester
Copy link
Contributor

@olliesilvester olliesilvester commented Sep 20, 2023

For the first part of #16. Adds a set of functions used to build save plans of a device.

Comment on lines 37 to 47
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
Copy link
Collaborator

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.

Suggested change
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(
Copy link
Collaborator

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?

src/ophyd_async/core/_device/device_save_loader.py Outdated Show resolved Hide resolved
value[inner_key] = inner_value.tolist()
# Convert enums to their values
elif isinstance(signal_values[index], Enum):
signal_values[index] = value.value
Copy link
Collaborator

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] = []):
Copy link
Collaborator

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:

Suggested change
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]]:

@olliesilvester
Copy link
Contributor Author

Now you make a plan using a series of utility functions in device_save_loader.py. I've put some examples in device_save_loader_test.py
In general a save plan should:

  • Get the RW signals with walk_rw_signals()
  • Locate the values of each of these with get_signal_values(), with an optional parameter to ignore specific signals
  • Optionally sort the values into separate phases with a device specific function
  • Save these values to a yaml file with save_to_yaml()

@olliesilvester olliesilvester changed the title Add save_device plan Add functions for saving device Sep 22, 2023
if ignore is None:
ignore = [""]

values = yield Msg("locate", *signals.values())
Copy link
Collaborator

@coretl coretl Sep 22, 2023

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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)
Copy link
Collaborator

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...

Copy link
Contributor Author

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

Copy link
Collaborator

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?

Copy link
Contributor Author

@olliesilvester olliesilvester Sep 29, 2023

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.

Copy link
Collaborator

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

Copy link
Contributor Author

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

@olliesilvester olliesilvester requested a review from coretl October 3, 2023 08:16

RE(save_my_device())

yaml.add_constructor("tag:!numpy.ndarray", ndarray_constructor)
Copy link
Collaborator

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?

Copy link
Collaborator

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

@jsouter
Copy link
Contributor

jsouter commented Oct 13, 2023

I have made a PR from my fork. Still using PyYAML - ruamel.yaml was more difficult to get pretty formatting out of.
#27
Now using a custom Dumper subclass that checks if the data is an Enum and if so represents its value member. A custom representer was also added for np.ndarrays to cast it to a list when representing; if needed this logic could be moved into the OphydDumper class's represent_data overload as well.

@coretl coretl mentioned this pull request Oct 23, 2023
@coretl
Copy link
Collaborator

coretl commented Oct 23, 2023

Superseded by #36

@coretl coretl closed this Oct 23, 2023
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.

3 participants