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

chore(release): Merge chore_release-8.2.0 into edge #16879

Merged
merged 27 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6ed5aaf
app(fix): Fix liquids always required during protocol setup (#16762)
mjhuff Nov 12, 2024
9e951da
fix(api): use encoder position instead of homing gantry when placing …
vegano1 Nov 13, 2024
653fb49
fix(app, shared-data, components): add calibration not req text for p…
smb2268 Nov 13, 2024
21bf747
fix(app): Bubble pipette command errors during drop tip wizard (#16793)
mjhuff Nov 13, 2024
f2adaab
fix(app): Fix run setup buttons (#16796)
mjhuff Nov 13, 2024
20c98e6
fix(app): enable about plate reader button when run is in progress (#…
smb2268 Nov 13, 2024
91b40ae
fix(api): Skip updating position estimators for axes that are not pre…
SyntaxColoring Nov 14, 2024
a0fe00f
fix(app): add error handling for failed maintenance run creation (#16…
mjhuff Nov 14, 2024
6d62bec
fix(robot-server): update data_files_table with uploaded source (#16813)
TamarZanzouri Nov 14, 2024
0ae0414
fix(api): add supported wavelengths to runtime error when initializin…
vegano1 Nov 14, 2024
7cbee8e
fix(app, components) fix csv name wrapping issue (#16785)
koji Nov 14, 2024
59814e6
fix(app): support special cased slot name copy (#16823)
mjhuff Nov 14, 2024
9bf09d8
fix(components) fix long unit wrapping issue (#16836)
koji Nov 14, 2024
972c592
fix(app): add affordances for tip detection failures (#16828)
mjhuff Nov 14, 2024
df80263
fix(app, robot-server): support `retryLocation` when retrying `dropTi…
mjhuff Nov 15, 2024
d49f990
fix(api): update the plate reader parsing of the serial + version to …
vegano1 Nov 15, 2024
62b1e9d
fix(shared-data): deck riser and auto sealing lid labware definition …
CaseyBatten Nov 15, 2024
c90aaea
fix(api): add stopped state so a stop request doesn't mean the grippe…
ryanthecoder Nov 15, 2024
6d5b3a2
fix(app): fix post run tip detection after error recovery (#16860)
mjhuff Nov 15, 2024
29e03ae
fix(app,robot-server): Account for failed commands not having a pipet…
SyntaxColoring Nov 18, 2024
c94a64c
fix(api): update error message to title case (#16851)
TamarZanzouri Nov 18, 2024
57ea4ae
fix(app): fix timestamp used for protocol completion (#16855)
mjhuff Nov 18, 2024
91bab8c
docs(api): Absorbance Plate Reader in Python API docs (#16668)
ecormany Nov 18, 2024
2df177f
chore(release): Merge branch 'chore_release-8.2.0' into edge
SyntaxColoring Nov 18, 2024
9c5fdb5
chore(release): Update command schema for merge.
SyntaxColoring Nov 18, 2024
8e84bb4
chore(release): Merge fixups.
SyntaxColoring Nov 18, 2024
2c326c1
chore(release): Merge branch 'edge' into merge_release_820_into_edge
SyntaxColoring Nov 22, 2024
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
5 changes: 5 additions & 0 deletions api-client/src/runs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export interface Runs {
export interface RunCurrentStateData {
estopEngaged: boolean
activeNozzleLayouts: Record<string, NozzleLayoutValues> // keyed by pipetteId
tipStates: Record<string, TipStates> // keyed by pipetteId
placeLabwareState?: PlaceLabwareState
}

Expand Down Expand Up @@ -218,3 +219,7 @@ export interface PlaceLabwareState {
location: OnDeckLabwareLocation
shouldPlaceDown: boolean
}

export interface TipStates {
hasTip: boolean
}
1 change: 0 additions & 1 deletion api/docs/v2/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,6 @@
("py:class", r".*protocol_api\.config.*"),
("py:class", r".*opentrons_shared_data.*"),
("py:class", r".*protocol_api._parameters.Parameters.*"),
("py:class", r".*AbsorbanceReaderContext"),
("py:class", r".*RobotContext"), # shh it's a secret (for now)
("py:class", r'.*AbstractLabware|APIVersion|LabwareLike|LoadedCoreMap|ModuleTypes|NoneType|OffDeckType|ProtocolCore|WellCore'), # laundry list of not fully qualified things
]
147 changes: 147 additions & 0 deletions api/docs/v2/modules/absorbance_plate_reader.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
:og:description: How to use the Absorbance Plate Reader Module in a Python protocol.

.. _absorbance-plate-reader-module:

******************************
Absorbance Plate Reader Module
******************************

The Absorbance Plate Reader Module is an on-deck microplate spectrophotometer that works with the Flex robot only. The module uses light absorbance to determine sample concentrations in 96-well plates.

The Absorbance Plate Reader is represented in code by an :py:class:`.AbsorbanceReaderContext` object, which has methods for moving the module lid with the Flex Gripper, initializing the module to read at a single wavelength or multiple wavelengths, and reading a plate. With the Python Protocol API, you can process plate reader data immediately in your protocol or export it to a CSV for post-run use.

This page explains the actions necessary for using the Absorbance Plate Reader. These combine to form the typical reader workflow:

1. Close the lid with no plate inside
2. Initialize the reader
3. Open the lid
4. Move a plate onto the module
5. Close the lid
6. Read the plate


Loading and Deck Slots
======================

The Absorbance Plate Reader can only be loaded in slots A3–D3. If you try to load it in any other slot, the API will raise an error. The module's caddy is designed such that the detection unit is in deck column 3 and the special staging area for the lid/illumination unit is in deck column 4. You can't load or move other labware on the Absorbance Plate Reader caddy in deck column 4, even while the lid is in the closed position (on top of the detection unit in deck column 3).

The examples in this section will use an Absorbance Plate Reader Module loaded as follows::

pr_mod = protocol.load_module(
module_name="absorbanceReaderV1",
location="D3"
)

.. versionadded:: 2.21

Lid Control
===========

Flex uses the gripper to move the lid between its two positions.

- :py:meth:`~.AbsorbanceReaderContext.open_lid()` moves the lid to the righthand side of the caddy, in deck column 4.
- :py:meth:`~.AbsorbanceReaderContext.close_lid()` moves the lid onto the detection unit, in deck column 3.

If you call ``open_lid()`` or ``close_lid()`` and the lid is already in the corresponding position, the method will succeed immediately. You can also check the position of the lid with :py:meth:`~.AbsorbanceReaderContext.is_lid_on()`.

You need to call ``close_lid()`` before initializing the reader, even if the reader was in the closed position at the start of the protocol.

.. warning::
Do not move the lid manually, during or outside of a protocol. The API does not allow manual lid movement because there is a risk of damaging the module.

.. _absorbance-initialization:

Initialization
==============

Initializing the reader prepares it to read a plate later in your protocol. The :py:meth:`.AbsorbanceReaderContext.initialize` method accepts parameters for the number of readings you want to take, the wavelengths to read, and whether you want to compare the reading to a reference wavelength. In the default hardware configuration, the supported wavelengths are 450 nm (blue), 562 nm (green), 600 nm (orange), and 650 nm (red).

The module uses these parameters immediately to perform the physical initialization. Additionally, the API preserves these values and uses them when you read the plate later in your protocol.

Let's take a look at examples of how to combine these parameters to prepare different types of readings. The simplest reading measures one wavelength, with no reference wavelength::

pr_mod.initialize(mode="single", wavelengths=[450])

.. versionadded:: 2.21

Now the reader is prepared to read at 450 nm. Note that the ``wavelengths`` parameter always takes a list of integer wavelengths, even when only reading a single wavelength.

This example can be extended by adding a reference wavelength::

pr_mod.initialize(
mode="single", wavelengths=[450], reference_wavelength=[562]
)

When configured this way, the module will read twice. In the :ref:`output data <plate-reader-data>`, the values read for ``reference_wavelength`` will be subtracted from the values read for the single member of ``wavelengths``. This is useful for normalization, or to correct for background interference in wavelength measurements.

The reader can also be initialized to take multiple measurements. When ``mode="multi"``, the ``wavelengths`` list can have up to six elements. This example will initialize the reader to read at three wavelengths::

pr_mod.initialize(mode="multi", wavelengths=[450, 562, 600])

You can't use a reference wavelength when performing multiple measurements.


Reading a Plate
===============

Use :py:meth:`.AbsorbanceReaderContext.read` to have the module read the plate, using the parameters that you specified during initialization::

pr_data = pr_mod.read()

.. versionadded:: 2.21

The ``read()`` method returns the results in a dictionary, which the above example saves to the variable ``pr_data``.

If you need to access this data after the conclusion of your protocol, add the ``export_filename`` parameter to instruct the API to output a CSV file, which is available in the Opentrons App by going to your Flex and viewing Recent Protocol Runs::

pr_data = pr_mod.read(export_filename="plate_data")

In the above example, the API both saves the data to a variable and outputs a CSV file. If you only need the data post-run, you can omit the variable assignment.

.. _plate-reader-data:

Using Plate Reader Data
=======================

There are two ways to use output data from the Absorbance Plate Reader:

- Within your protocol as a nested dictionary object.
- Outside of your protocol, as a tabular CSV file.

The two formats are structured differently, even though they contain the same measurement data.

Dictionary Data
---------------

The dictionary object returned by ``read()`` has two nested levels. The keys at the top level are the wavelengths you provided to ``initialize()``. The keys at the second level are string names of each of the 96 wells, ``"A1"`` through ``"H12"``. The values at the second level are the measured values for each wells. These values are floating point numbers, representing the optical density (OD) of the samples in each well. OD ranges from 0.0 (low sample concentration) to 4.0 (high sample concentration).

The nested dictionary structure allows you to access results by index later in your protocol. This example initializes a multiple read and then accesses different portions of the results::

# initializing and reading
pr_mod.initialize(mode="multi", wavelengths=[450, 600])
pr_mod.open_lid()
protocol.move_labware(plate, pr_mod, use_gripper=True)
pr_mod.close_lid()
pr_data = pr_mod.read()

# accessing results
pr_data[450]["A1"] # value for well A1 at 450 nm
pr_data[600]["H12"] # value for well H12 at 600 nm
pr_data[450] # dict of all wells at 450 nm

You can write additional code to transform this data in any way that you need. For example, you could use a list comprehension to create a list of only the 450 nm values for column 1, ordered by well from A1 to H1::

[pr_data[450][w.well_name] for w in plate.columns()[0]]

.. _absorbance-csv:

CSV data
--------

The CSV exported when specifying ``export_filename`` consists of tabular data followed by additional information. Each measurement produces 9 rows in the CSV file, representing the layout of the well plate that has been read. These rows form a table with numeric labels in the first row and alphabetic labels in the first column, as you would see on physical labware. Each "cell" of the table contains the measured OD value for the well (0.0–4.0) in the corresponding position on the plate.

Additional information, starting with one blank labware grid, is output at the end of the file. The last few lines of the file list the sample wavelengths, serial number of the module, and timestamps for when measurement started and finished.

Each output file for your protocol is available in the Opentrons App by going to your Flex and viewing Recent Protocol Runs. After downloading the file from your Flex, you can read it with any software that reads CSV files, and you can write additional code to parse and act upon its contents.

You can also select the output CSV as the value of a CSV runtime parameter in a subsequent protocol. When you :ref:`parse the CSV data <rtp-csv-data>`, make sure to set ``detect_dialect=False``, or the API will raise an error.
7 changes: 5 additions & 2 deletions api/docs/v2/modules/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Available Modules
The first parameter of :py:meth:`.ProtocolContext.load_module` is the module's *API load name*. The load name tells your robot which module you're going to use in a protocol. The table below lists the API load names for the currently available modules.

.. table::
:widths: 4 5 2
:widths: 4 4 2

+--------------------+-------------------------------+---------------------------+
| Module | API Load Name | Introduced in API Version |
Expand Down Expand Up @@ -95,6 +95,9 @@ The first parameter of :py:meth:`.ProtocolContext.load_module` is the module's
| Magnetic Block | ``magneticBlockV1`` | 2.15 |
| GEN1 | | |
+--------------------+-------------------------------+---------------------------+
| Absorbance Plate | ``absorbanceReaderV1`` | 2.21 |
| Reader Module | | |
+--------------------+-------------------------------+---------------------------+

Some modules were added to our Python API later than others, and others span multiple hardware generations. When writing a protocol that requires a module, make sure your ``requirements`` or ``metadata`` code block specifies an :ref:`API version <v2-versioning>` high enough to support all the module generations you want to use.

Expand Down Expand Up @@ -124,7 +127,7 @@ Any :ref:`custom labware <v2-custom-labware>` added to your Opentrons App is als
Module and Labware Compatibility
--------------------------------

It's your responsibility to ensure the labware and module combinations you load together work together. The Protocol API won't raise a warning or error if you load an unusual combination, like placing a tube rack on a Thermocycler. See `What labware can I use with my modules? <https://support.opentrons.com/s/article/What-labware-can-I-use-with-my-modules>`_ for more information about labware/module combinations.
It's your responsibility to ensure the labware and module combinations you load together work together. The API generally won't raise a warning or error if you load an unusual combination, like placing a tube rack on a Thermocycler. The API will raise an error if you try to load a labware on an unsupported adapter. When working with custom labware and module adapters, be sure to add stacking offsets for the adapter to your custom labware definition.


Additional Labware Parameters
Expand Down
4 changes: 3 additions & 1 deletion api/docs/v2/new_modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Hardware Modules

.. toctree::
modules/setup
modules/absorbance_plate_reader
modules/heater_shaker
modules/magnetic_block
modules/magnetic_module
Expand All @@ -17,13 +18,14 @@ Hardware Modules

Hardware modules are powered and unpowered deck-mounted peripherals. The Flex and OT-2 are aware of deck-mounted powered modules when they're attached via a USB connection and used in an uploaded protocol. The robots do not know about unpowered modules until you use one in a protocol and upload it to the Opentrons App.

Powered modules include the Heater-Shaker Module, Magnetic Module, Temperature Module, and Thermocycler Module. The 96-well Magnetic Block is an unpowered module.
Powered modules include the Absorbance Plate Reader Module, Heater-Shaker Module, Magnetic Module, Temperature Module, and Thermocycler Module. The 96-well Magnetic Block is an unpowered module.

Pages in this section of the documentation cover:

- :ref:`Setting up modules and their labware <module-setup>`.
- Working with the module contexts for each type of module.

- :ref:`Absorbance Plate Reader Module <absorbance-plate-reader-module>`
- :ref:`Heater-Shaker Module <heater-shaker-module>`
- :ref:`Magnetic Block <magnetic-block>`
- :ref:`Magnetic Module <magnetic-module>`
Expand Down
26 changes: 25 additions & 1 deletion api/docs/v2/new_protocol_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,29 +53,53 @@ Wells and Liquids
Modules
=======

Absorbance Plate Reader
-----------------------

.. autoclass:: opentrons.protocol_api.AbsorbanceReaderContext
:members:
:exclude-members: broker, geometry, load_labware_object, load_adapter, load_adapter_from_definition
:inherited-members:


Heater-Shaker
-------------

.. autoclass:: opentrons.protocol_api.HeaterShakerContext
:members:
:exclude-members: broker, geometry, load_labware_object
:inherited-members:

Magnetic Block
--------------

.. autoclass:: opentrons.protocol_api.MagneticBlockContext
:members:
:exclude-members: broker, geometry, load_labware_object
:inherited-members:

Magnetic Module
---------------

.. autoclass:: opentrons.protocol_api.MagneticModuleContext
:members:
:exclude-members: calibrate, broker, geometry, load_labware_object
:inherited-members:

Temperature Module
------------------

.. autoclass:: opentrons.protocol_api.TemperatureModuleContext
:members:
:exclude-members: start_set_temperature, await_temperature, broker, geometry, load_labware_object
:inherited-members:

Thermocycler
------------

.. autoclass:: opentrons.protocol_api.ThermocyclerContext
:members:
:exclude-members: total_step_count, current_cycle_index, total_cycle_count, hold_time, ramp_rate, current_step_index, broker, geometry, load_labware_object
:exclude-members: total_step_count, current_cycle_index, total_cycle_count, hold_time, ramp_rate, current_step_index, broker, geometry, load_labware_object, load_adapter, load_adapter_from_definition
:inherited-members:


Expand Down
9 changes: 5 additions & 4 deletions api/src/opentrons/drivers/absorbance_reader/async_byonoy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@


SN_PARSER = re.compile(r'ATTRS{serial}=="(?P<serial>.+?)"')
VERSION_PARSER = re.compile(r"Absorbance (?P<version>V\d+\.\d+\.\d+)")
# match semver V0.0.0 (old format) or one integer (latest format)
VERSION_PARSER = re.compile(r"(?P<version>(V\d+\.\d+\.\d+|^\d+$))")
SERIAL_PARSER = re.compile(r"(?P<serial>(OPT|BYO)[A-Z]{3}[0-9]+)")


Expand Down Expand Up @@ -156,10 +157,10 @@ async def get_device_information(self) -> Dict[str, str]:
func=partial(self._interface.get_device_information, handle),
)
self._raise_if_error(err.name, f"Error getting device information: {err}")
serial_match = SERIAL_PARSER.fullmatch(device_info.sn)
version_match = VERSION_PARSER.match(device_info.version)
serial_match = SERIAL_PARSER.match(device_info.sn)
version_match = VERSION_PARSER.search(device_info.version)
serial = serial_match["serial"].strip() if serial_match else "OPTMAA00000"
version = version_match["version"].lower() if version_match else "v0.0.0"
version = version_match["version"].lower() if version_match else "v0"
info = {
"serial": serial,
"version": version,
Expand Down
1 change: 1 addition & 0 deletions api/src/opentrons/hardware_control/backends/ot3utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ def update(
FirmwareGripperjawState.force_controlling_home: GripperJawState.HOMED_READY,
FirmwareGripperjawState.force_controlling: GripperJawState.GRIPPING,
FirmwareGripperjawState.position_controlling: GripperJawState.HOLDING,
FirmwareGripperjawState.stopped: GripperJawState.STOPPED,
}


Expand Down
12 changes: 7 additions & 5 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,11 +788,13 @@ async def _update_position_estimation(
Function to update motor estimation for a set of axes
"""
await self._backend.update_motor_status()
if axes:
checked_axes = [ax for ax in axes if ax in Axis]
else:
checked_axes = [ax for ax in Axis]
await self._backend.update_motor_estimation(checked_axes)

if axes is None:
axes = [ax for ax in Axis]

axes = [ax for ax in axes if self._backend.axis_is_present(ax)]

await self._backend.update_motor_estimation(axes)

# Global actions API
def pause(self, pause_type: PauseType) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async def update_axis_position_estimations(self, axes: Sequence[Axis]) -> None:
"""Update the specified axes' position estimators from their encoders.

This will allow these axes to make a non-home move even if they do not currently have
a position estimation (unless there is no tracked poition from the encoders, as would be
a position estimation (unless there is no tracked position from the encoders, as would be
true immediately after boot).

Axis encoders have less precision than their position estimators. Calling this function will
Expand All @@ -19,6 +19,8 @@ async def update_axis_position_estimations(self, axes: Sequence[Axis]) -> None:

This function updates only the requested axes. If other axes have bad position estimation,
moves that require those axes or attempts to get the position of those axes will still fail.
Axes that are not currently available (like a plunger for a pipette that is not connected)
will be ignored.
"""
...

Expand Down
2 changes: 2 additions & 0 deletions api/src/opentrons/hardware_control/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,8 @@ class GripperJawState(enum.Enum):
#: the gripper is actively force-control gripping something
HOLDING = enum.auto()
#: the gripper is in position-control mode
STOPPED = enum.auto()
#: the gripper has been homed before but is stopped now


class InstrumentProbeType(enum.Enum):
Expand Down
Loading
Loading