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

Rebase add_save_load on add-detector-logic #31

Closed
wants to merge 21 commits into from

Conversation

jsouter
Copy link
Contributor

@jsouter jsouter commented Oct 16, 2023

Rebased on add-detector-logic, tests are passing on my end but let me know if things still need moving around

@coretl
Copy link
Collaborator

coretl commented Oct 16, 2023

Please can you fix linting. @rosesyrett when that is fixed shall we merge?

@jsouter jsouter force-pushed the add_save_load_rb branch 3 times, most recently from a37320c to c0e6a0a Compare October 16, 2023 12:38
@jsouter
Copy link
Contributor Author

jsouter commented Oct 16, 2023

Please can you fix linting. @rosesyrett when that is fixed shall we merge?

Linting is fixed now

Copy link
Collaborator

@rosesyrett rosesyrett left a comment

Choose a reason for hiding this comment

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

Looks good, I like how only SignalRW's are saved, that's very neat. I have a few comments about docstrings and a few about the tests, as I use tests to work out how to use a new bit of code so to me it's important they're readable and clear.
Very nearly there.

Comment on lines 39 to 40
async def get_setpoint(self) -> T:
"""The current setpoint"""
Copy link
Collaborator

Choose a reason for hiding this comment

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

without going into bluesky.protocols and looking at the definition of a Location protocol and its comments for setpoint and readback, this docstring is a little confusing. Could we perhaps replace it with:

Suggested change
async def get_setpoint(self) -> T:
"""The current setpoint"""
async def get_setpoint(self) -> T:
"""The point that a signal was requested to move to."""

Comment on lines 35 to 36
async def get_value(self) -> T:
"""The current value"""
Copy link
Collaborator

Choose a reason for hiding this comment

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

In line with making setpoint and readback slightly less confusing, could we replace this with:

Suggested change
async def get_value(self) -> T:
"""The current value"""
async def get_value(self) -> T:
"""The readback value of the signal."""

Comment on lines 168 to 169
async def get_setpoint(self) -> T:
return self.converter.value(self._value)
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it worth making this just call get_value, and adding a comment that for a simsignalbackend these will always be the same?

i.e.:

Suggested change
async def get_setpoint(self) -> T:
return self.converter.value(self._value)
async def get_setpoint(self) -> T:
"""For a simulated backend, the setpoint and readback values are the same."""
return await self.get_value()



async def test_enum_yaml_formatting(tmp_path):
enums = [EnumTest(EnumTest.VAL1), EnumTest(EnumTest.VAL2)]
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 it's easier to read this as:

Suggested change
enums = [EnumTest(EnumTest.VAL1), EnumTest(EnumTest.VAL2)]
enums = [EnumTest.VAL1, EnumTest.VAL2]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah you're right, was mistaken about how Python Enums work

Comment on lines 67 to 73
assert isinstance(enums[0], EnumTest)
save_to_yaml(enums, path.join(tmp_path, "test_file.yaml"))
with open(path.join(tmp_path, "test_file.yaml"), "r") as file:
yaml_content = yaml.load(file, yaml.Loader)
assert isinstance(yaml_content, list)
assert yaml_content[0] == EnumTest.VAL1
assert yaml_content[1] == EnumTest.VAL2
Copy link
Collaborator

Choose a reason for hiding this comment

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

these assertions all make sense, but I'd shorten them for readability:

Suggested change
assert isinstance(enums[0], EnumTest)
save_to_yaml(enums, path.join(tmp_path, "test_file.yaml"))
with open(path.join(tmp_path, "test_file.yaml"), "r") as file:
yaml_content = yaml.load(file, yaml.Loader)
assert isinstance(yaml_content, list)
assert yaml_content[0] == EnumTest.VAL1
assert yaml_content[1] == EnumTest.VAL2
save_to_yaml(enums, path.join(tmp_path, "test_file.yaml"))
with open(path.join(tmp_path, "test_file.yaml"), "r") as file:
saved_enums = yaml.load(file, yaml.Loader)
assert saved_enums == enums

This effectively combines the assert statements you currently have on lines 71-73, and removes the assert statement on 67 since that will never raise an AssertionError unless there is something very wrong with python or someone's redefined EnumTest... Up to you if you want to rename yaml_content or not

Copy link
Contributor Author

Choose a reason for hiding this comment

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

async def test_enum_yaml_formatting(tmp_path):
    enums = [EnumTest.VAL1, EnumTest.VAL2]
    save_to_yaml(enums, path.join(tmp_path, "test_file.yaml"))
    with open(path.join(tmp_path, "test_file.yaml"), "r") as file:
        saved_enums = yaml.load(file, yaml.Loader)
    # check that save/load reduces from enum to str
    assert all(isinstance(value, str) for value in saved_enums)
    # check values of enums same
    assert saved_enums == enums

How about this? I'd like to have something that shows that reduction to a string when saving to yaml

Comment on lines 127 to 197
async def test_save_device_with_phase(device_with_phases, tmp_path):
RE = RunEngine()
await device_with_phases.child1.sig1.set("string")
table_pv = {"VAL1": np.array([1, 1, 1, 1, 1]), "VAL2": np.array([1, 1, 1, 1, 1])}
await device_with_phases.child2.sig1.set(table_pv)

# Create save plan from utility functions
def save_my_device():
signalRWs = walk_rw_signals(device_with_phases)
values = yield from get_signal_values(signalRWs)
phases = device_with_phases.sort_signal_by_phase(device_with_phases, values)
save_to_yaml(phases, path.join(tmp_path, "test_file.yaml"))

RE(save_my_device())

with open(path.join(tmp_path, "test_file.yaml"), "r") as file:
yaml_content = yaml.load(file, yaml.Loader)
assert yaml_content[0] == {"child1.sig1": "string"}
assert np.array_equal(
yaml_content[1]["child2.sig1"]["VAL1"], np.array([1, 1, 1, 1, 1])
)
assert np.array_equal(
yaml_content[1]["child2.sig1"]["VAL2"], np.array([1, 1, 1, 1, 1])
)


async def test_yaml_formatting_no_phase(device_with_phases, tmp_path):
RE = RunEngine()
await device_with_phases.child1.sig1.set("test_string")
table_pv = {"VAL1": np.array([1, 2, 3, 4, 5]), "VAL2": np.array([6, 7, 8, 9, 10])}
await device_with_phases.child2.sig1.set(table_pv)

# Create save plan from utility functions
def save_my_device():
signalRWs = walk_rw_signals(device_with_phases)
values = yield from get_signal_values(signalRWs)
phases = device_with_phases.sort_signal_by_phase(device_with_phases, values)
save_to_yaml(phases, path.join(tmp_path, "test_file.yaml"))

RE(save_my_device())

with open(path.join(tmp_path, "test_file.yaml"), "r") as file:
expected = """\
- {child1.sig1: test_string}
- child2.sig1:
VAL1: [1, 2, 3, 4, 5]
VAL2: [6, 7, 8, 9, 10]
"""
assert file.read() == expected


async def test_saved_types_with_phase(device_with_phases, tmp_path):
RE = RunEngine()
await device_with_phases.child1.sig1.set("string")
table_pv = {"VAL1": np.array([1, 1, 1, 1, 1]), "VAL2": np.array([1, 1, 1, 1, 1])}
await device_with_phases.child2.sig1.set(table_pv)

# Create save plan from utility functions
def save_my_device():
signalRWs = walk_rw_signals(device_with_phases)
values = yield from get_signal_values(signalRWs)
phases = device_with_phases.sort_signal_by_phase(device_with_phases, values)
save_to_yaml(phases, path.join(tmp_path, "test_file.yaml"))

RE(save_my_device())

with open(path.join(tmp_path, "test_file.yaml"), "r") as file:
yaml_content = yaml.load(file, yaml.Loader)
assert type(yaml_content[0]["child1.sig1"]) is str
assert type(yaml_content[1]["child2.sig1"]["VAL1"]) is list
assert type(yaml_content[1]["child2.sig1"]["VAL2"]) is list
Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure about the language here r.e. phases. It seems like these three tests share a lot in common, and can probably be combined into one test and still cover the same lines.

to me, these tests only differ from the previous few because they test if save_to_yaml can handle a List[Dict[str, Any]] as well as a Dict[str, Any]. Which is fine, but why the word 'phase'? Would changing 'phase' to 'list' be better in this instance?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I can reduce these tests down. I believe the idea with "phases" is that when loading values from a yaml to a Device (which is yet to be implemented) we want to load the groups of signals in a specified order, so all the phase 1 signals first, then phase 2 signals etc. There's possibly a clearer way to express this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I suggested the word "phase" because I imagine the loading being done in phases:

def how_i_imaging_load(device: Device, phases: Sequence[Dict[str, Any]]):
    signals = walk_rw_signals(device)
    for phase in phases:
        for name, value in phase.items():
            yield from bps.abs_set(signals[name], value, group="load", wait=False)
        yield from bps.wait(group="load")

I agree that device_with_phases is a bit confusing though, as the phases are in the saved set, not the device. I don't think we will be attaching the sort_signals_by_phase method to a device, so maybe just leave that free and ditch the fixture?

@jsouter jsouter force-pushed the add_save_load_rb branch 2 times, most recently from e1cd6fa to 7e13c22 Compare October 17, 2023 08:39
@jsouter
Copy link
Contributor Author

jsouter commented Oct 17, 2023

I've made those changes and added in Ollie's load_device plan as load_from_yaml, with a new test.


def get_signal_values(
signals: Dict[str, SignalRW[Any]], ignore: Optional[List[str]] = None
) -> Union[Generator[Dict[str, Any], None, None], Dict[str, Any]]:
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 this is always a generator. @callumforrester is this right for a plan?

Suggested change
) -> Union[Generator[Dict[str, Any], None, None], Dict[str, Any]]:
) -> Generator[Msg, Sequence[Location], Dict[str, Any]]:

src/ophyd_async/core/device_save_loader.py Outdated Show resolved Hide resolved
@jsouter
Copy link
Contributor Author

jsouter commented Oct 18, 2023

Made those changes, and added a return hint for load_from_yaml (Generator[Msg, None, Sequence[Dict[str[Any]])

:func:`ophyd_async.core.save_to_yaml`
"""
with open(save_path, "r") as file:
data_by_phase: Sequence[Dict[str, Any]] = yaml.full_load(file)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we split this function here please. load_from_yaml(save_path: str) -> Sequence[Dict[str, Any]]:, then a different plan set_signal_values(signals: Dict[str, SignalRW], values: Sequence[Dict[str, Any]]) -> Generator[Msg, None, None]: that takes signals (as provided by walk_rw_signals), and values (as provided by load_from_yaml) and does the abs_set logic you do below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, have done this rebased onto the last passing commit for add-detector-logic

jsouter and others added 5 commits October 20, 2023 14:33
device_save_loader linting
Add load_from_yaml to device_save_loader

update test_device_save_loader.py

Co-authored-by: Oliver Silvester <[email protected]>
Require sequence of dicts in save_to_yaml
rebase on newer add-detector-logic commits
@rosesyrett rosesyrett deleted the branch bluesky:add-detector-logic October 20, 2023 15:29
@rosesyrett rosesyrett closed this Oct 20, 2023
@coretl coretl mentioned this pull request 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.

4 participants