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

feat(api): Support V2 pipette configs in the OT-2 and Protocol Engine #13104

Merged
merged 32 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1f2cbc8
add quirks and mount configs to v2 pipette configurations
Laura-Danielle Jun 23, 2023
a224613
support mutable configs
Laura-Danielle Jun 23, 2023
8f997cc
add support in the routers
Laura-Danielle Jun 29, 2023
a44b33b
add quirks to schema
Laura-Danielle Jun 29, 2023
7a67e68
fix tavern tests
Laura-Danielle Jun 30, 2023
cd42f7d
add pieptte name back in
Laura-Danielle Jul 7, 2023
cdc34ec
feat(api): support V2 pipette schemas for the OT2 and Protocol Engine
Laura-Danielle Jul 6, 2023
70b2dbe
update flow rates to be dicts, support dict tip overlaps
Laura-Danielle Jul 6, 2023
f91f2e4
changes to match new schema
Laura-Danielle Jul 12, 2023
bcc99bd
schema fixes and accidental typos
Laura-Danielle Jul 12, 2023
a91dc30
test fix-ups
Laura-Danielle Jul 12, 2023
d58a7b6
update default idle current
Laura-Danielle Jul 14, 2023
a85f108
update a few helpers and correct some config mistakes
Laura-Danielle Jul 14, 2023
f01b9af
modify update pipette dict to support new configs
Laura-Danielle Jul 14, 2023
1b612f1
json formatting
Laura-Danielle Jul 14, 2023
f585b56
fixup from rebase
Laura-Danielle Jul 14, 2023
dae2269
fixup update key func in pip config
Laura-Danielle Jul 14, 2023
fae2363
fix up linter
Laura-Danielle Jul 14, 2023
3bc8db6
accidental fixture duplicate from rebase
Laura-Danielle Jul 17, 2023
3d499a4
channels inherit from int, do not type tipracks as LabwareUri
Laura-Danielle Jul 17, 2023
dd1e660
fixup channels name conversion
Laura-Danielle Jul 17, 2023
07b8d6c
fix linter and mistake with quirks union
Laura-Danielle Jul 17, 2023
dfa403b
fixup shared-data copy error
Laura-Danielle Jul 18, 2023
d21d9f9
move definition types to pipette/types
Laura-Danielle Jul 18, 2023
68d42df
change lookup dictionary to use pipette types
Laura-Danielle Jul 18, 2023
63eafcf
better schema comment for flowrate
Laura-Danielle Jul 18, 2023
e653868
change max_volume to model
Laura-Danielle Jul 19, 2023
bcbbdba
add test for configuration updater
Laura-Danielle Jul 19, 2023
c3ec3e0
support multiple versions of a pip function, remove tiprack uri keyed…
Laura-Danielle Jul 20, 2023
ea2d88a
fix backwards compatability from channels change
Laura-Danielle Jul 20, 2023
2b19e8e
formatting, linting, missed changes to pipetting functions
Laura-Danielle Jul 20, 2023
7919afd
fixup linter and hardware test functions
Laura-Danielle Jul 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
466 changes: 0 additions & 466 deletions api/src/opentrons/config/pipette_config.py

This file was deleted.

20 changes: 13 additions & 7 deletions api/src/opentrons/hardware_control/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
cast,
)

from opentrons_shared_data.pipette import name_config
from opentrons_shared_data.pipette import (
pipette_load_name_conversions as pipette_load_name,
)
from opentrons_shared_data.pipette.dev_types import PipetteName
from opentrons_shared_data.robot.dev_types import RobotType
from opentrons import types as top_types
Expand Down Expand Up @@ -409,11 +411,10 @@ async def cache_instruments(
self._log.info("Updating instrument model cache")
checked_require = require or {}
for mount, name in checked_require.items():
if name not in name_config():
if not pipette_load_name.supported_pipette(name):
raise RuntimeError(f"{name} is not a valid pipette name")
async with self._motion_lock:
found = await self._backend.get_attached_instruments(checked_require)

for mount, instrument_data in found.items():
config = instrument_data.get("config")
req_instr = checked_require.get(mount, None)
Expand All @@ -428,7 +429,10 @@ async def cache_instruments(
)
self._attached_instruments[mount] = p
if req_instr and p:
p.act_as(req_instr)
converted_name = pipette_load_name.convert_to_pipette_name_type(
req_instr
)
p.act_as(converted_name)

if may_skip:
self._log.info(f"Skipping configuration on {mount.name}")
Expand Down Expand Up @@ -557,14 +561,16 @@ async def _do_plunger_home(
await stack.enter_async_context(self._motion_lock)
with self._backend.save_current():
self._backend.set_active_current(
{checked_axis: instr.config.plunger_current}
{checked_axis: instr.plunger_motor_current.run}
Copy link
Member

Choose a reason for hiding this comment

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

honestly wondering if, just for the sake of this pr, we mapped at least some of the new values to the old names. most of the time we wouldn't use them, but it would prevent us from needing a million changes that have no semantic meaning but just need different letters in sequence

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in the config itself? Otherwise if we mapped it in the Pipette class there would still be a million changes. Plus mapping in a Pydantic model will get confusing since it's supposed to server as a schema.

)
await self._backend.home([ot2_axis_to_string(checked_axis)])
# either we were passed False for our acquire_lock and we
# should pass it on, or we acquired the lock above and
# shouldn't do it again
target_pos = target_position_from_plunger(
checked_mount, instr.config.bottom, self._current_position
checked_mount,
instr.plunger_positions.bottom,
self._current_position,
)
await self._move(
target_pos,
Expand Down Expand Up @@ -929,7 +935,7 @@ async def prepare_for_aspirate(
speed = self.plunger_speed(
instrument, instrument.blow_out_flow_rate, "aspirate"
)
bottom = instrument.config.bottom
bottom = instrument.plunger_positions.bottom
target = target_position_from_plunger(mount, bottom, self._current_position)
await self._move(
target,
Expand Down
29 changes: 23 additions & 6 deletions api/src/opentrons/hardware_control/backends/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
TYPE_CHECKING,
Union,
Sequence,
cast,
)
from typing_extensions import Final

Expand All @@ -21,10 +22,15 @@
except (OSError, ModuleNotFoundError):
aionotify = None

from opentrons_shared_data.pipette import (
pipette_load_name_conversions as pipette_load_name,
mutable_configurations,
)
from opentrons_shared_data.pipette.dev_types import PipetteName

from opentrons.drivers.smoothie_drivers import SmoothieDriver
from opentrons.drivers.rpi_drivers import build_gpio_chardev
import opentrons.config
from opentrons.config import pipette_config
from opentrons.config.types import RobotConfig
from opentrons.types import Mount

Expand All @@ -33,7 +39,7 @@
from ..util import ot2_axis_to_string

if TYPE_CHECKING:
from opentrons_shared_data.pipette.dev_types import PipetteModel, PipetteName
from opentrons_shared_data.pipette.dev_types import PipetteModel
from ..dev_types import (
AttachedPipette,
AttachedInstruments,
Expand Down Expand Up @@ -180,22 +186,33 @@ async def _query_mount(
] = await self._smoothie_driver.read_pipette_model( # type: ignore
mount.name.lower()
)
if found_model and found_model not in pipette_config.config_models:
if found_model and not pipette_load_name.supported_pipette(found_model):
# TODO: Consider how to handle this error - it bubbles up now
# and will cause problems at higher levels
MODULE_LOG.error(f"Bad model on {mount.name}: {found_model}")
found_model = None
found_id = await self._smoothie_driver.read_pipette_id(mount.name.lower())

if found_model:
config = pipette_config.load(found_model, found_id)
path_to_overrides = opentrons.config.get_opentrons_path(
"pipette_config_overrides_dir"
)
converted_found_model = pipette_load_name.convert_pipette_model(found_model)
converted_found_name = pipette_load_name.convert_to_pipette_name_type(
found_model
)
config = mutable_configurations.load_with_mutable_configurations(
converted_found_model, path_to_overrides, found_id
)
if expected:
acceptable = [config.name] + config.back_compat_names
acceptable = [
cast(PipetteName, str(converted_found_name))
] + config.pipette_backcompat_names
if expected not in acceptable:
raise RuntimeError(
f"mount {mount}: instrument"
f" {expected} was requested"
f" but {config.model} is present"
f" but {converted_found_model} is present"
)
return {"config": config, "id": found_id}
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@

if TYPE_CHECKING:
from ..dev_types import (
OT3AttachedPipette,
AttachedPipette,
AttachedGripper,
OT3AttachedInstruments,
)
Expand Down Expand Up @@ -691,7 +691,7 @@ def _combine_serial_number(pipette_info: ohc_tool_types.PipetteInformation) -> s
@staticmethod
def _build_attached_pip(
attached: ohc_tool_types.PipetteInformation, mount: OT3Mount
) -> OT3AttachedPipette:
) -> AttachedPipette:
if attached.name == FirmwarePipetteName.unknown:
raise InvalidPipetteName(name=attached.name_int, mount=mount)
try:
Expand Down
4 changes: 2 additions & 2 deletions api/src/opentrons/hardware_control/backends/ot3simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
InstrumentHardwareConfigs,
PipetteSpec,
GripperSpec,
OT3AttachedPipette,
AttachedPipette,
AttachedGripper,
OT3AttachedInstruments,
)
Expand Down Expand Up @@ -407,7 +407,7 @@ def _attached_pipette_to_mount(
mount: OT3Mount,
init_instr: PipetteSpec,
expected_instr: Optional[PipetteName],
) -> OT3AttachedPipette:
) -> AttachedPipette:
found_model = init_instr["model"]

# TODO (lc 12-05-2022) When the time comes, we should think about supporting
Expand Down
87 changes: 60 additions & 27 deletions api/src/opentrons/hardware_control/backends/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import copy
import logging
from threading import Event
from typing import Dict, Optional, List, Tuple, TYPE_CHECKING, Sequence, Iterator
from typing import Dict, Optional, List, Tuple, TYPE_CHECKING, Sequence, Iterator, cast
from contextlib import contextmanager

from opentrons_shared_data.pipette import dummy_model_for_name
from opentrons_shared_data.pipette import (
pipette_load_name_conversions as pipette_load_name,
mutable_configurations,
pipette_definition,
)

from opentrons import types
from opentrons.config.pipette_config import config_models, config_names, configs, load
from opentrons.config.types import RobotConfig
from opentrons.config import get_opentrons_path
from opentrons.drivers.smoothie_drivers import SimulatingDriver

from opentrons.drivers.rpi_drivers.gpio_simulator import SimulatingGPIOCharDev
Expand All @@ -22,7 +26,7 @@
from ..util import ot2_axis_to_string

if TYPE_CHECKING:
from opentrons_shared_data.pipette.dev_types import PipetteName
from opentrons_shared_data.pipette.dev_types import PipetteName, PipetteModel
from ..dev_types import (
AttachedPipette,
AttachedInstruments,
Expand Down Expand Up @@ -119,25 +123,31 @@ def __init__(
self._gpio_chardev = gpio_chardev

def _sanitize_attached_instrument(
passed_ai: Optional[Dict[str, Optional[str]]] = None
passed_ai: Optional[PipetteSpec] = None,
) -> PipetteSpec:
if not passed_ai or not passed_ai.get("model"):
return {"model": None, "id": None}
if passed_ai["model"] in config_models:
return passed_ai # type: ignore
if passed_ai["model"] in config_names:
return {
"model": dummy_model_for_name(passed_ai["model"]), # type: ignore
"id": passed_ai.get("id"),
}
if pipette_load_name.supported_pipette(
cast("PipetteName", passed_ai["model"])
):
if pipette_load_name.is_model(passed_ai["model"]):
return passed_ai
else:
get_pip_model = pipette_load_name.convert_pipette_name(
cast("PipetteName", passed_ai["model"])
)
return {
"model": PipetteModel(str(get_pip_model)),
"id": passed_ai.get("id"),
}
raise KeyError(
"If you specify attached_instruments, the model "
"should be pipette names or pipette models, but "
f'{passed_ai["model"]} is not'
)

self._attached_instruments = {
m: _sanitize_attached_instrument(attached_instruments.get(m))
m: _sanitize_attached_instrument(attached_instruments.get(m)) # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

what's this suppressing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just moved the #type: ignore from the internal function to have that be slightly better typed. It's suppressing a TypeDict type.

for m in types.Mount.ot2_mounts()
}
self._stubbed_attached_modules = attached_modules
Expand Down Expand Up @@ -213,18 +223,33 @@ async def fast_home(self, axis: Sequence[str], margin: float) -> Dict[str, float
return self._position

def _attached_to_mount(
self, mount: types.Mount, expected_instr: Optional[PipetteName]
self, mount: types.Mount, expected_instr: Optional["PipetteName"]
) -> AttachedPipette:
init_instr = self._attached_instruments.get(mount, {"model": None, "id": None})
found_model = init_instr["model"]
back_compat: List["PipetteName"] = []

path_to_overrides = get_opentrons_path("pipette_config_overrides_dir")
found_model_configs: Optional[pipette_definition.PipetteConfigurations] = None

converted_found_name: Optional[pipette_definition.PipetteNameType] = None
if found_model:
back_compat = configs[found_model].get("backCompatNames", [])
converted_found_model = pipette_load_name.convert_pipette_model(found_model)
converted_found_name = pipette_load_name.convert_to_pipette_name_type(
found_model
)
found_model_configs = (
mutable_configurations.load_with_mutable_configurations(
converted_found_model, path_to_overrides, init_instr["id"]
)
)
back_compat = found_model_configs.pipette_backcompat_names
if (
expected_instr
and found_model
and found_model_configs
and (
configs[found_model]["name"] != expected_instr
str(converted_found_name) != expected_instr
and expected_instr not in back_compat
)
):
Expand All @@ -235,34 +260,42 @@ def _attached_to_mount(
)
)
else:
converted_expected_model = pipette_load_name.convert_pipette_name(
expected_instr
)
return {
"config": load(dummy_model_for_name(expected_instr)),
"config": mutable_configurations.load_with_mutable_configurations(
converted_expected_model, path_to_overrides
),
"id": None,
}
elif found_model and expected_instr:
elif found_model and found_model_configs:
# Instrument detected matches instrument expected (note:
# "instrument detected" means passed as an argument to the
# constructor of this class)
return {
"config": load(found_model, init_instr["id"]),
"id": init_instr["id"],
}
elif found_model:
# constructor of this class) OR,
# Instrument detected and no expected instrument specified
return {
"config": load(found_model, init_instr["id"]),
"config": found_model_configs,
"id": init_instr["id"],
}
elif expected_instr:
# Expected instrument specified and no instrument detected
return {"config": load(dummy_model_for_name(expected_instr)), "id": None}
converted_expected_model = pipette_load_name.convert_pipette_name(
expected_instr
)
return {
"config": mutable_configurations.load_with_mutable_configurations(
converted_expected_model, path_to_overrides
),
"id": None,
}
else:
# No instrument detected or expected
return {"config": None, "id": None}

@ensure_yield
async def get_attached_instruments(
self, expected: Dict[types.Mount, PipetteName]
self, expected: Dict[types.Mount, "PipetteName"]
) -> AttachedInstruments:
"""Update the internal cache of attached instruments.

Expand Down
8 changes: 1 addition & 7 deletions api/src/opentrons/hardware_control/dev_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

from opentrons.drivers.types import MoveSplit
from opentrons.types import Mount
from opentrons.config.pipette_config import PipetteConfig
from opentrons.hardware_control.types import GripperJawState


Expand All @@ -40,11 +39,6 @@ class GripperSpec(InstrumentSpec):


class AttachedPipette(TypedDict):
config: Optional[PipetteConfig]
id: Optional[str]


class OT3AttachedPipette(TypedDict):
config: Optional[PipetteConfigurations]
id: Optional[str]

Expand All @@ -56,7 +50,7 @@ class AttachedGripper(TypedDict):

AttachedInstruments = Dict[Mount, AttachedPipette]

OT3AttachedInstruments = Union[OT3AttachedPipette, AttachedGripper]
OT3AttachedInstruments = Union[AttachedPipette, AttachedGripper]

EIGHT_CHANNELS = Literal[8]
ONE_CHANNEL = Literal[1]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any, Optional, Generic, TypeVar
from typing import Any, Optional, Generic, TypeVar, Dict

from opentrons.types import Point
from opentrons.hardware_control.types import CriticalPoint
Expand Down Expand Up @@ -27,7 +27,7 @@ def reload_configurations(self) -> None:
...

@abstractmethod
def update_config_item(self, elem_name: str, elem_val: Any) -> None:
def update_config_item(self, elements: Dict[str, Any]) -> None:
"""Update instrument config item."""
...

Expand Down
Loading
Loading