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

refactor(calibration-storage): Use pydantic models for data validation, add OT-3 pydantic models #11520

Merged
merged 39 commits into from
Oct 28, 2022

Conversation

Laura-Danielle
Copy link
Contributor

@Laura-Danielle Laura-Danielle commented Sep 29, 2022

Overview

Closes RLIQ-118. The objective of this PR is to add pydantic models to support better validation on calibration data in general. Please check out this confluence doc for more information on the pydantic schemas and this confluence doc for more information on what the file system looks like in general. There are a lot of loose ends around calibration data that need to be tied up in separate PRs of which I've documented in this epic.

In-memory caching for calibration data will be in a follow-up PR. Mike's comments from here have been addressed in this new branch aside from file access asynchrony which will be addressed in a separate ticket. Apologies for the commit wonky-ness. I couldn't get the branches to rebase properly.

Changelog

  • Added two modules under calibration_storage to separate OT-2 and OT-3 calibration types
  • Separated instrument calibration data based on OT-2 and OT-3 (pipette(s) will be handled in a follow-up PR which changes the data schema of pipettes to support OT-3 pipette functionality).
  • Moved the dataclass objects of the calibration data models (which were only used in the hardware controller) to the hardware controller and converted from the pydantic schemas in the hardware controller once the data was being loaded in.
  • Switched to using only pydantic models in the robot-server
  • Added more test coverage on the calibration storage module in general
  • Modified tests where necessary

Review requests

Please test this on a robot, let me know if you have any changes you want to the actual data shape as well.

Test Plan

Please view this confluence document for a more detailed test plan and check list.

  • Run through all OT-2 calibration flows
  • Check that all calibration data endpoints (calibration/pipette_offset and calibation/tip_length) still work as expected
  • Check that all robot calibrations are still being properly loaded and applied in the hardware controller
  • Check that downgrading the software keeps all calibrations that were calibrated with the new software

Risk assessment

High. This touches a lot of flakey code and needs a lot of QA eyes. See test plan above.

@Laura-Danielle Laura-Danielle requested review from a team as code owners September 29, 2022 20:09
@codecov
Copy link

codecov bot commented Sep 29, 2022

Codecov Report

Merging #11520 (db8c636) into edge (2dd6aac) will decrease coverage by 10.09%.
The diff coverage is n/a.

Impacted file tree graph

@@             Coverage Diff             @@
##             edge   #11520       +/-   ##
===========================================
- Coverage   74.58%   64.49%   -10.10%     
===========================================
  Files        2074     1416      -658     
  Lines       57827    24790    -33037     
  Branches     6104     5990      -114     
===========================================
- Hits        43133    15988    -27145     
+ Misses      13265     7381     -5884     
+ Partials     1429     1421        -8     
Flag Coverage Δ
api ?
g-code-testing ?
hardware ?
hardware-testing ∅ <ø> (∅)
notify-server 88.26% <ø> (ø)
ot3-gravimetric-test ?
robot-server ?
shared-data ?
update-server ?
usb-bridge ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...c/components/steplist/MultiSelectToolbar/index.tsx 86.79% <0.00%> (ø)
...rc/opentrons/calibration_storage/file_operators.py
api/src/opentrons/calibration_storage/helpers.py
api/src/opentrons/calibration_storage/types.py
api/src/opentrons/config/reset.py
api/src/opentrons/hardware_control/__init__.py
api/src/opentrons/hardware_control/api.py
api/src/opentrons/hardware_control/ot3api.py
...ardware_control/protocols/instrument_configurer.py
...rc/opentrons/hardware_control/robot_calibration.py
... and 645 more

Copy link
Member

@sfoster1 sfoster1 left a comment

Choose a reason for hiding this comment

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

Some smaller things in code review, but one bigger one: I don't really like the use of "Schema" in pydantic model names. They're schemas, but they're also instantiated with data and passed around as data container objects. It's weird to have a function that modifies the data of a schema, it's a mismatch of kind: if you have a function that operates on a schema, i expect the schema to be different afterwards - but here, it's just the data in an instance. We should either use these objects only to verify data but not store it, or call them Models or something

shared-data/LICENSE Outdated Show resolved Hide resolved
Copy link
Contributor

@mcous mcous left a comment

Choose a reason for hiding this comment

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

Many of the added tests assert on implementation details (private methods / fields) of the code under test, which is inappropriate and does not actually test that the public API works. Code and/or tests should structured such that behavior can be validated against public, used-in-production methods

@@ -52,7 +72,7 @@ def read_cal_file(

def save_to_file(
filepath: StrPath,
data: typing.Mapping[str, typing.Any],
data: typing.Union[BaseModel, typing.Dict[str, typing.Any], typing.Any],
Copy link
Contributor

Choose a reason for hiding this comment

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

That last typing.Any effectively removes any type constraints on this argument. Why is it needed?

Suggested change
data: typing.Union[BaseModel, typing.Dict[str, typing.Any], typing.Any],
data: typing.Union[BaseModel, typing.Dict[str, typing.Any]],

api/src/opentrons/calibration_storage/file_operators.py Outdated Show resolved Hide resolved
api/src/opentrons/calibration_storage/file_operators.py Outdated Show resolved Hide resolved
if file.name == "deck_calibration.json":
try:
return v1.DeckCalibrationSchema(**io.read_cal_file(file.path))
except (json.JSONDecodeError, ValidationError):
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this mean? Should it be logged?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure I'll throw in a log. It effectively means the data is bad.

api/tests/opentrons/calibration_storage/test_tip_length.py Outdated Show resolved Hide resolved
api/tests/opentrons/calibration_storage/test_tip_length.py Outdated Show resolved Hide resolved
@Laura-Danielle
Copy link
Contributor Author

I want to get this merged ASAP because it's blocking some other calibration work. Any other clean-ups requested (aside from anything found during testing) will be documented here and addressed after our 'feature freeze'.

…chemas for all calibration data

We are now using pydantic models to store and modify calibration data. Most OT-3 calibration data no
longer requires tips to calibrate with, so the schemas reflect this fact.

re #RLIQ-118
…are stored

We should really keep the dataclasses in hardware controller -- as that's the only place they are
used. While here, also separated out the concept of 'robot' calibration and 'instrument'
calibration.
…ble names

'get', 'delete' etc can be rather confusing when using the calibration_storage module. Instead, we
should name each file based on the calibration data they handle.
Copy link
Member

@sfoster1 sfoster1 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 to me! Would love to remove conditional imports at some point but not this PR.

)
from .ot2.pipette_offset import get_all_pipette_offset_calibrations

if config.feature_flags.enable_ot3_hardware_controller():
Copy link
Member

Choose a reason for hiding this comment

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

This is fine for now but can we add a todo to make this something that is not based on a feature flag and not conditionally imported at some point? I'd love for fewer decisions to get made during import, and having our code support both machines at runtime is an important thing for offline analysis and general sanity

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. If we do need to check a feature flag, it should be done at function run time, not import time. This global config singleton that needs to be initialized in order for imports to work is an antipattern in our codebase

If it's too much for this PR, that's fine, but I was imagining a lightweight facade in my earlier review:

def save_robot_deck_attitude(...) -> ...:
    if is_ot3:
        from .ot3 import deck_attitude as ot3_deck_attitude
        ot3_deck_attitude.save_robot_deck_attitude(...)
    else:
        ...

@Laura-Danielle Laura-Danielle force-pushed the RLIQ-118-add-schemas-file-name-changes branch from ffe4bef to 0a1c8f2 Compare October 27, 2022 18:58
Copy link
Contributor

@mcous mcous left a comment

Choose a reason for hiding this comment

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

I'm ok to move forward with this PR and address additional items in a follow up PR for speed, but in order of importance, I'm still concerned about these issues:

  1. Conditional imports/exports at the top level of the calibration_storage module should be avoided at all costs
  2. Tests for calibration_storage are written with if/else, which seems to suggest that a test suite run will only check OT-2 or OT-3, rather than OT-2 and OT-3
  3. File/directory reading/writing concerns still leak out of file_operators.py
  4. Since file_operators.py has test coverage, tests for other calibration_storage components could mock out file_operators for speed and useful design feedback, but they do not
    • Tests will be slower because there will be a lot of hitting the filesystem
    • We have redundant test coverage of file operations

)
from .ot2.pipette_offset import get_all_pipette_offset_calibrations

if config.feature_flags.enable_ot3_hardware_controller():
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. If we do need to check a feature flag, it should be done at function run time, not import time. This global config singleton that needs to be initialized in order for imports to work is an antipattern in our codebase

If it's too much for this PR, that's fine, but I was imagining a lightweight facade in my earlier review:

def save_robot_deck_attitude(...) -> ...:
    if is_ot3:
        from .ot3 import deck_attitude as ot3_deck_attitude
        ot3_deck_attitude.save_robot_deck_attitude(...)
    else:
        ...

api/src/opentrons/calibration_storage/ot2/deck_attitude.py Outdated Show resolved Hide resolved
"""
pipette_calibration_dir = Path(config.get_opentrons_path("pipette_calibration_dir"))
pipette_calibration_list = []
for filepath in pipette_calibration_dir.glob("**/*.json"):
Copy link
Contributor

Choose a reason for hiding this comment

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

Would recommend moving to a io.get_json_files_from_directory or something similar for testability

Comment on lines +103 to +107
with open(custom_tiprack_path, "rb") as f:
return typing.cast(
"LabwareDefinition",
json.loads(f.read().decode("utf-8")),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be going through io.read_json_file or similar?

api/src/opentrons/calibration_storage/ot3/deck_attitude.py Outdated Show resolved Hide resolved
Comment on lines +11 to +13
@pytest.fixture(autouse=True)
def reload_module(robot_model: "RobotModel") -> None:
importlib.reload(opentrons.calibration_storage)
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 a warning sign, is it due to all the conditional importing and re-exporting? An explicit facade would avoid this problem.

importlib.reload, in my experience, is not terribly reliable or intuitive

)

assert get_robot_deck_attitude() is None
if robot_model == "OT-3 Standard":
Copy link
Contributor

Choose a reason for hiding this comment

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

if/else in tests should almost always be avoided. Can this be parametrized, instead?



@pytest.fixture
def starting_calibration_data(
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we write these tests as isolated unit tests and mock out file_operators instead, given that file_operators has unit tests?

@Laura-Danielle
Copy link
Contributor Author

I'm ok to move forward with this PR and address additional items in a follow up PR for speed, but in order of importance, I'm still concerned about these issues:

  1. Conditional imports/exports at the top level of the calibration_storage module should be avoided at all costs

  2. Tests for calibration_storage are written with if/else, which seems to suggest that a test suite run will only check OT-2 or OT-3, rather than OT-2 and OT-3

  3. File/directory reading/writing concerns still leak out of file_operators.py

  4. Since file_operators.py has test coverage, tests for other calibration_storage components could mock out file_operators for speed and useful design feedback, but they do not

    • Tests will be slower because there will be a lot of hitting the filesystem
    • We have redundant test coverage of file operations

Will hopefully get to all of this next week!

@Laura-Danielle Laura-Danielle merged commit eb13afd into edge Oct 28, 2022
@Laura-Danielle Laura-Danielle deleted the RLIQ-118-add-schemas-file-name-changes branch October 28, 2022 19:30
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

4 participants