From 91bab8cd199f0da5db02f0fc68baf1f4e8ec71e1 Mon Sep 17 00:00:00 2001 From: Ed Cormany Date: Mon, 18 Nov 2024 13:38:38 -0500 Subject: [PATCH] docs(api): Absorbance Plate Reader in Python API docs (#16668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Overview Adds a docs page and publishes API Reference entries for the Absorbance Plate Reader. Addresses RTC-488, RTC-508. ## Test Plan and Hands on Testing [Sandbox](http://sandbox.docs.opentrons.com/docs-plate-reader/v2/modules/absorbance_plate_reader.html) ## Changelog - Adds a new page, Absorbance Plate Reader Module, under Hardware Modules - Links to new page from Hardware Modules landing page - Adds Absorbance Plate Reader to Module Setup page - Adds H3s for modules in API Reference for easier navigation - Edits to `AbsorbanceReaderContext` docstrings in `module_contexts.py` - Cleaned up malformed note syntax _for Thermocycler_ in `module_contexts.py` - Removed nitpick rule from `conf.py` now that `AbsorbanceReaderContext` is published - 🆕 Instruct authors not to detect CSV dialect when using plate reader output as RTP. ## Review requests - Check that all descriptions are true to how the plate reader behaves. - Double check that my code simulates — I have done this once. - Do we need more information in any area, especially for CSV files? - Should we provide a sample? If so, where to put it? - ✅ Should we give more concrete guidance on parsing the CSV other than "you know how to write code, yeah"? - 🆕 Do we need to list the four `wavelengths` values that actually function on a default HW config? ## Risk assessment v low, docs only --- api/docs/v2/conf.py | 1 - .../v2/modules/absorbance_plate_reader.rst | 147 ++++++++++++++++++ api/docs/v2/modules/setup.rst | 7 +- api/docs/v2/new_modules.rst | 4 +- api/docs/v2/new_protocol_api.rst | 26 +++- .../opentrons/protocol_api/module_contexts.py | 82 ++++++---- .../parameters/csv_parameter_interface.py | 4 +- 7 files changed, 239 insertions(+), 32 deletions(-) create mode 100644 api/docs/v2/modules/absorbance_plate_reader.rst diff --git a/api/docs/v2/conf.py b/api/docs/v2/conf.py index 5ab0fdaad76..708d800ce3e 100644 --- a/api/docs/v2/conf.py +++ b/api/docs/v2/conf.py @@ -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 ] diff --git a/api/docs/v2/modules/absorbance_plate_reader.rst b/api/docs/v2/modules/absorbance_plate_reader.rst new file mode 100644 index 00000000000..9f96d5e90d3 --- /dev/null +++ b/api/docs/v2/modules/absorbance_plate_reader.rst @@ -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 `, 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 `, make sure to set ``detect_dialect=False``, or the API will raise an error. \ No newline at end of file diff --git a/api/docs/v2/modules/setup.rst b/api/docs/v2/modules/setup.rst index c6badd82954..a0cbe18bf0e 100644 --- a/api/docs/v2/modules/setup.rst +++ b/api/docs/v2/modules/setup.rst @@ -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 | @@ -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 ` high enough to support all the module generations you want to use. @@ -124,7 +127,7 @@ Any :ref:`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? `_ 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 diff --git a/api/docs/v2/new_modules.rst b/api/docs/v2/new_modules.rst index 956a2bc7989..594ceca3867 100644 --- a/api/docs/v2/new_modules.rst +++ b/api/docs/v2/new_modules.rst @@ -8,6 +8,7 @@ Hardware Modules .. toctree:: modules/setup + modules/absorbance_plate_reader modules/heater_shaker modules/magnetic_block modules/magnetic_module @@ -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 `. - Working with the module contexts for each type of module. + - :ref:`Absorbance Plate Reader Module ` - :ref:`Heater-Shaker Module ` - :ref:`Magnetic Block ` - :ref:`Magnetic Module ` diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst index a71ad5cf4a2..2ce4c39e3cc 100644 --- a/api/docs/v2/new_protocol_api.rst +++ b/api/docs/v2/new_protocol_api.rst @@ -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: diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index 7beab69c53f..8890981e32a 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -581,7 +581,7 @@ def set_block_temperature( individual well of the loaded labware, in µL. If not specified, the default is 25 µL. - .. note: + .. note:: If ``hold_time_minutes`` and ``hold_time_seconds`` are not specified, the Thermocycler will proceed to the next command @@ -605,7 +605,7 @@ def set_lid_temperature(self, temperature: float) -> None: :param temperature: A value between 37 and 110, representing the target temperature in °C. - .. note: + .. note:: The Thermocycler will proceed to the next command immediately after ``temperature`` has been reached. @@ -635,13 +635,13 @@ def execute_profile( individual well of the loaded labware, in µL. If not specified, the default is 25 µL. - .. note: + .. note:: Unlike with :py:meth:`set_block_temperature`, either or both of ``hold_time_minutes`` and ``hold_time_seconds`` must be defined and for each step. - .. note: + .. note:: Before API Version 2.21, Thermocycler profiles run with this command would be listed in the app as having a number of repetitions equal to @@ -991,7 +991,7 @@ class MagneticBlockContext(ModuleContext): class AbsorbanceReaderContext(ModuleContext): - """An object representing a connected Absorbance Reader Module. + """An object representing a connected Absorbance Plate Reader Module. It should not be instantiated directly; instead, it should be created through :py:meth:`.ProtocolContext.load_module`. @@ -1009,17 +1009,21 @@ def serial_number(self) -> str: @requires_version(2, 21) def close_lid(self) -> None: - """Close the lid of the Absorbance Reader.""" + """Use the Flex Gripper to close the lid of the Absorbance Plate Reader. + + You must call this method before initializing the reader, even if the reader was + in the closed position at the start of the protocol. + """ self._core.close_lid() @requires_version(2, 21) def open_lid(self) -> None: - """Open the lid of the Absorbance Reader.""" + """Use the Flex Gripper to open the lid of the Absorbance Plate Reader.""" self._core.open_lid() @requires_version(2, 21) def is_lid_on(self) -> bool: - """Return ``True`` if the Absorbance Reader's lid is currently closed.""" + """Return ``True`` if the Absorbance Plate Reader's lid is currently closed.""" return self._core.is_lid_on() @requires_version(2, 21) @@ -1029,19 +1033,28 @@ def initialize( wavelengths: List[int], reference_wavelength: Optional[int] = None, ) -> None: - """Take a zero reading on the Absorbance Plate Reader Module. + """Prepare the Absorbance Plate Reader to read a plate. + + See :ref:`absorbance-initialization` for examples. :param mode: Either ``"single"`` or ``"multi"``. - - In single measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses - one sample wavelength and an optional reference wavelength. - - In multiple measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses - a list of up to six sample wavelengths. - :param wavelengths: A list of wavelengths, in mm, to measure. - - Must contain only one item when initializing a single measurement. - - Must contain one to six items when initializing a multiple measurement. - :param reference_wavelength: An optional reference wavelength, in mm. Cannot be - used with multiple measurements. + - In single measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses + one sample wavelength and an optional reference wavelength. + - In multiple measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses + a list of up to six sample wavelengths. + :param wavelengths: A list of wavelengths, in nm, to measure. + + - In the default hardware configuration, each wavelength must be one of + ``450`` (blue), ``562`` (green), ``600`` (orange), or ``650`` (red). In + custom hardware configurations, the module may accept other integers + between 350 and 1000. + - The list must contain only one item when initializing a single measurement. + - The list can contain one to six items when initializing a multiple measurement. + :param reference_wavelength: An optional reference wavelength, in nm. If provided, + :py:meth:`.AbsorbanceReaderContext.read` will read at the reference + wavelength and then subtract the reference wavelength values from the + measurement wavelength values. Can only be used with single measurements. """ self._core.initialize( mode, wavelengths, reference_wavelength=reference_wavelength @@ -1051,16 +1064,33 @@ def initialize( def read( self, export_filename: Optional[str] = None ) -> Dict[int, Dict[str, float]]: - """Initiate read on the Absorbance Reader. + """Read a plate on the Absorbance Plate Reader. + + This method always returns a dictionary of measurement data. It optionally will + save a CSV file of the results to the Flex filesystem, which you can access from + the Recent Protocol Runs screen in the Opentrons App. These files are `only` saved + if you specify ``export_filename``. + + In simulation, the values for each well key in the dictionary are set to zero, and + no files are written. + + .. note:: + + Avoid divide-by-zero errors when simulating and using the results of this + method later in the protocol. If you divide by any of the measurement + values, use :py:meth:`.ProtocolContext.is_simulating` to use alternate dummy + data or skip the division step. - Returns a dictionary of wavelengths to dictionary of values ordered by well name. + :param export_filename: An optional file basename. If provided, this method + will write a CSV file for each measurement in the read operation. File + names will use the value of this parameter, the measurement wavelength + supplied in :py:meth:`~.AbsorbanceReaderContext.initialize`, and a + ``.csv`` extension. For example, when reading at wavelengths 450 and 562 + with ``export_filename="my_data"``, there will be two output files: + ``my_data_450.csv`` and ``my_data_562.csv``. - :param export_filename: Optional, if a filename is provided a CSV file will be saved - as a result of the read action containing measurement data. The filename will - be modified to include the wavelength used during measurement. If multiple - measurements are taken, then a file will be generated for each wavelength provided. + See :ref:`absorbance-csv` for information on working with these CSV files. - Example: If `export_filename="my_data"` and wavelengths 450 and 531 are used during - measurement, the output files will be "my_data_450.csv" and "my_data_531.csv". + :returns: A dictionary of wavelengths to dictionary of values ordered by well name. """ return self._core.read(filename=export_filename) diff --git a/api/src/opentrons/protocols/parameters/csv_parameter_interface.py b/api/src/opentrons/protocols/parameters/csv_parameter_interface.py index 6da9a0f7aaf..ff460b48f21 100644 --- a/api/src/opentrons/protocols/parameters/csv_parameter_interface.py +++ b/api/src/opentrons/protocols/parameters/csv_parameter_interface.py @@ -60,7 +60,9 @@ def parse_as_csv( as appropriate. :param detect_dialect: If ``True``, examine the file and try to assign it a - :py:class:`csv.Dialect` to improve parsing behavior. + :py:class:`csv.Dialect` to improve parsing behavior. Set this to ``False`` + when using the file output of :py:meth:`.AbsorbanceReaderContext.read` as + a runtime parameter. :param kwargs: For advanced CSV handling, you can pass any of the `formatting parameters `_ accepted by :py:func:`csv.reader` from the Python standard library.