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

resume procedure endpoint #253

Merged
merged 16 commits into from
Feb 12, 2025
Merged

resume procedure endpoint #253

merged 16 commits into from
Feb 12, 2025

Conversation

imaitland
Copy link
Collaborator

feature - when a calibration procedure state has been saved (persisted to fs), it can be 'resumed' by passing the "resume" flag to the /start endpoint.

@imaitland imaitland requested a review from amitschang February 6, 2025 18:55
@imaitland imaitland requested a review from a team as a code owner February 6, 2025 18:55
@@ -135,7 +140,7 @@ def dispatch_calibrator_action(request: Request, hardware_name: str = Path(...),

payload = action.get("payload", {})

return {**calibration_procedure.dispatch(action_to_dispatch, payload), "started": True}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Refactored "started" atrribute to be common state for all procedures. It's needed as the procedure controls (UI) are distinct for an uninitialized (not started) and initialized (started) procedure.

Copy link

codecov bot commented Feb 6, 2025

Codecov Report

Attention: Patch coverage is 97.77778% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
evolver/calibration/action.py 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@@ -44,7 +44,7 @@ class TestCalibration:
def test_get_calibration_status(self):
_, client = setup_evolver_with_calibrator(NoOpCalibrator)

response = client.post("/hardware/test/calibrator/procedure/start")
response = client.post("/hardware/test/calibrator/procedure/start", params={"resume": False})
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Now the user must be explicit about their intention to attempt to resume a procedure or start a new / fresh one.


raw_dispatch_response = dispatch_action(client, "test", "read_vial_0_raw_output")

assert raw_dispatch_response.status_code == 200
assert raw_dispatch_response.json() == {
"0": {"raw": [1.23], "reference": []},
"completed_actions": ["read_vial_0_raw_output"],
"history": [{"completed_actions": [], "history": []}],
"history": [{"completed_actions": [], "history": [], "started": True}],
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

only downside to adding "started" to common procedure state is it appears in history.

class CalibrationData(Transformer.Config):
"""Stores calibration data, including the procedure_state from the calibration_procedure."""

procedure_state: CalibrationStateModel = Field(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

CalibrationStateModel is now common model shared across the Procedure, Actions and CalibrationData

Copy link
Member

Choose a reason for hiding this comment

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

I like the common model, but given that we now expect the measure data to be under CalibrationData.procedure_state.measured, I'm not sure what else would ever need to go in this structure at the base level, so is there even a need to have the nesting? In other words, do we need both CalibrationData and CalibrationStateModel or are they really the same thing?

@@ -75,29 +55,31 @@ def undo(self):
"""
Undo the last action that was dispatched in the calibration procedure.
"""
if len(self.state["history"]) > 0:
self.state = self.state["history"].pop()
if len(self.state.history) > 0:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

since state remains a pydantic model now across the system, we can get typed property access.

Copy link
Member

@amitschang amitschang left a comment

Choose a reason for hiding this comment

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

overall looks good. Just minor comments and maybe opportunity to simplify

class CalibrationData(Transformer.Config):
"""Stores calibration data, including the procedure_state from the calibration_procedure."""

procedure_state: CalibrationStateModel = Field(
Copy link
Member

Choose a reason for hiding this comment

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

I like the common model, but given that we now expect the measure data to be under CalibrationData.procedure_state.measured, I'm not sure what else would ever need to go in this structure at the base level, so is there even a need to have the nesting? In other words, do we need both CalibrationData and CalibrationStateModel or are they really the same thing?

@@ -54,7 +33,8 @@ def __init__(self, hardware, state=None, *args, **kwargs):
"""
super().__init__(*args, **kwargs)
self.actions = []
self.state = CalibrationStateModel(**(state or {})).model_dump()
self.state = CalibrationStateModel(**(state or {}))
Copy link
Member

Choose a reason for hiding this comment

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

It's not wrong, but since we are working with an object, might it be cleaner to

CalibrationStateModel.model_validate(state or {})

And I didn't really catch earlier, but since the init takes state as a parameter we could default state={} there, and can pass just state to the above - unless of course we specifically need to support None being passed in.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep will fix

Comment on lines 18 to 19
class CalibrationData(Calibrator.CalibrationData):
measured: Dict[int, Dict[str, List[float]]] = {} # {vial_index: {"reference": [], "raw": []}}
procedure_state: CalibrationStateModel = {}
Copy link
Member

Choose a reason for hiding this comment

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

in this case we actually don't even need to overload anymore, so this whole class can (and should) be removed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep will fix

@imaitland imaitland requested a review from amitschang February 11, 2025 21:43
@@ -212,6 +275,22 @@ def test_dispatch_temperature_calibration_calculate_fit_action():
assert output_transformer_response.json()["0"]["parameters"] == [0.6149999999999997, 0.024599999999999997]


def get_stable_state_subset(state):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Having to do this reveals an area for improvement in the history data structure. Since history is part of the state persisted to the history, the entire history is actually already available in the history prop of the last added item to the history stack...

Soooo...
To fix, update the history stack data-structure update methods (push and pop) to only need the last state in the stack, and update the undo method, to essentially walk the history attr of the single state that's now in the history stack.

See issue: #256

amitschang
amitschang previously approved these changes Feb 12, 2025
Copy link
Member

@amitschang amitschang left a comment

Choose a reason for hiding this comment

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

Thanks! I think it can go, but a couple comments for your perusal

Comment on lines 165 to 168
if self.calibration_file:
self.calibration_data = CalibrationStateModel.load(file_path=self.calibration_file)
else:
self.calibration_data = CalibrationStateModel()
Copy link
Member

Choose a reason for hiding this comment

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

I think this is not quite right. Since this is init and we are passed the calibration_file to input, we shouldn't have the self.calibration_file unless set here, so the above condition (if calibration_file) should be sufficient? Meaning here is just to create a default calibration state

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah makes sense. Fixed.

Comment on lines 18 to 22
if not state.measured:
state.measured = {}
if self.vial_idx not in state.measured:
state.measured[self.vial_idx] = {"reference": [], "raw": []}
state.measured[self.vial_idx]["reference"].append(payload.temperature)
Copy link
Member

Choose a reason for hiding this comment

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

totally fine, but in case you like: collections.defaultdict and/or setdefault is useful here:

state.measured = state.measured or defaultdict(lambda: {"referece": [], "raw": []})
state.measured[self.vial_idx]["reference"].append(payload.temperature)

where all elements are same kind, no need to check and initialize yourself.

@imaitland imaitland merged commit 89e9ee7 into main Feb 12, 2025
8 checks passed
@imaitland imaitland deleted the iainland/resume-procedure-2 branch February 12, 2025 17:13
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.

2 participants