diff --git a/api/docs/v2/conf.py b/api/docs/v2/conf.py index 480cc2ed872..26723260e7b 100644 --- a/api/docs/v2/conf.py +++ b/api/docs/v2/conf.py @@ -99,7 +99,7 @@ # use rst_prolog to hold the subsitution # update the apiLevel value whenever a new minor version is released rst_prolog = f""" -.. |apiLevel| replace:: 2.17 +.. |apiLevel| replace:: 2.18 .. |release| replace:: {release} """ @@ -444,5 +444,6 @@ ("py:class", r".*protocol_api\.deck.*"), ("py:class", r".*protocol_api\.config.*"), ("py:class", r".*opentrons_shared_data.*"), + ("py:class", r".*protocol_api._parameters.Parameters.*"), ("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/deck_slots.rst b/api/docs/v2/deck_slots.rst index 2c38e70755f..6441ab0d562 100644 --- a/api/docs/v2/deck_slots.rst +++ b/api/docs/v2/deck_slots.rst @@ -149,9 +149,6 @@ Starting in API version 2.16, you must load trash bin fixtures in your protocol .. versionadded:: 2.16 -.. note:: - The :py:class:`.TrashBin` class doesn't have any callable methods, so you don't have to save the result of ``load_trash_bin()`` to a variable, especially if your protocol only loads a single trash container. Being able to reference the trash bin by name is useful when dealing with multiple trash containers. - Call ``load_trash_bin()`` multiple times to add more than one bin. See :ref:`pipette-trash-containers` for more information on using pipettes with multiple trash bins. .. _configure-waste-chute: diff --git a/api/docs/v2/index.rst b/api/docs/v2/index.rst index 5e29296241d..29fad41865b 100644 --- a/api/docs/v2/index.rst +++ b/api/docs/v2/index.rst @@ -17,6 +17,7 @@ Welcome new_atomic_commands new_complex_commands robot_position + runtime_parameters new_advanced_running new_examples adapting_ot2_flex diff --git a/api/docs/v2/new_advanced_running.rst b/api/docs/v2/new_advanced_running.rst index 5a867c0d172..c564455e391 100644 --- a/api/docs/v2/new_advanced_running.rst +++ b/api/docs/v2/new_advanced_running.rst @@ -65,7 +65,12 @@ Since a typical protocol only `defines` the ``run`` function but doesn't `call` Setting Labware Offsets ----------------------- -All positions relative to labware are adjusted automatically based on labware offset data. When you're running your code in Jupyter Notebook or with ``opentrons_execute``, you need to set your own offsets because you can't perform run setup and Labware Position Check in the Opentrons App or on the Flex touchscreen. For these applications, do the following to calculate and apply labware offsets: +All positions relative to labware are adjusted automatically based on labware offset data. When you're running your code in Jupyter Notebook or with ``opentrons_execute``, you need to set your own offsets because you can't perform run setup and Labware Position Check in the Opentrons App or on the Flex touchscreen. + +Creating a Dummy Protocol +^^^^^^^^^^^^^^^^^^^^^^^^^ + +For advanced control applications, do the following to calculate and apply labware offsets: 1. Create a "dummy" protocol that loads your labware and has each used pipette pick up a tip from a tip rack. 2. Import the dummy protocol to the Opentrons App. @@ -118,11 +123,50 @@ This automatically generated code uses generic names for the loaded labware. If Once you've executed this code in Jupyter Notebook, all subsequent positional calculations for this reservoir in slot 2 will be adjusted 0.1 mm to the right, 0.2 mm to the back, and 0.3 mm up. -Remember, you should only add ``set_offset()`` commands to protocols run outside of the Opentrons App. And you should follow the behavior of Labware Position Check, i.e., *do not* reuse offset measurements unless they apply to the *same labware* in the *same deck slot* on the *same robot*. +Keep in mind that ``set_offset()`` commands will override any labware offsets set by running Labware Position Check in the Opentrons App. And you should follow the behavior of Labware Position Check, i.e., *do not* reuse offset measurements unless they apply to the *same labware type* in the *same deck slot* on the *same robot*. .. warning:: - Improperly reusing offset data may cause your robot to move to an unexpected position or crash against labware, which can lead to incorrect protocol execution or damage your equipment. The same applies when running protocols with ``set_offset()`` commands in the Opentrons App. When in doubt: run Labware Position Check again and update your code! + Improperly reusing offset data may cause your robot to move to an unexpected position or crash against labware, which can lead to incorrect protocol execution or damage your equipment. When in doubt: run Labware Position Check again and update your code! + +.. _labware-offset-behavior: + +Labware Offset Behavior +^^^^^^^^^^^^^^^^^^^^^^^ + +How the API applies labware offsets varies depending on the API level of your protocol. This section describes the latest behavior. For details on how offsets work in earlier API versions, see the API reference entry for :py:meth:`.set_offset`. + +In the latest API version, offsets apply to labware type–location combinations. For example, if you use ``set_offset()`` on a tip rack, use all the tips, and replace the rack with a fresh one of the same type in the same location, the offsets will apply to the fresh tip rack:: + + tiprack = protocol.load_labware( + load_name="opentrons_flex_96_tiprack_1000ul", location="D3" + ) + tiprack2 = protocol.load_labware( + load_name="opentrons_flex_96_tiprack_1000ul", + location=protocol_api.OFF_DECK, + ) + tiprack.set_offset(x=0.1, y=0.1, z=0.1) + protocol.move_labware( + labware=tiprack, new_location=protocol_api.OFF_DECK + ) # tiprack has no offset while off-deck + protocol.move_labware( + labware=tiprack2, new_location="D3" + ) # tiprack2 now has offset 0.1, 0.1, 0.1 + +Because offsets apply to combinations of labware type and location, if you want an offset to apply to a piece of labware as it moves around the deck, call ``set_offset()`` again after each movement:: + + plate = protocol.load_labware( + load_name="corning_96_wellplate_360ul_flat", location="D2" + ) + plate.set_offset( + x=-0.1, y=-0.2, z=-0.3 + ) # plate now has offset -0.1, -0.2, -0.3 + protocol.move_labware( + labware=plate, new_location="D3" + ) # plate now has offset 0, 0, 0 + plate.set_offset( + x=-0.1, y=-0.2, z=-0.3 + ) # plate again has offset -0.1, -0.2, -0.3 Using Custom Labware -------------------- diff --git a/api/docs/v2/new_labware.rst b/api/docs/v2/new_labware.rst index 50428d4a232..a85512999c9 100644 --- a/api/docs/v2/new_labware.rst +++ b/api/docs/v2/new_labware.rst @@ -269,6 +269,8 @@ To use these optional methods, first create a liquid object with :py:meth:`.Prot Let's examine how these two methods work. The following examples demonstrate how to define colored water samples for a well plate and reservoir. +.. _defining-liquids: + Defining Liquids ================ @@ -291,6 +293,8 @@ This example uses ``define_liquid`` to create two liquid objects and instantiate The ``display_color`` parameter accepts a hex color code, which adds a color to that liquid's label when you import your protocol into the Opentrons App. The ``define_liquid`` method accepts standard 3-, 4-, 6-, and 8-character hex color codes. +.. _loading-liquids: + Labeling Wells and Reservoirs ============================= diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst index 3bd6ac38658..0fd8deb4afb 100644 --- a/api/docs/v2/new_protocol_api.rst +++ b/api/docs/v2/new_protocol_api.rst @@ -14,7 +14,7 @@ Protocols .. autoclass:: opentrons.protocol_api.ProtocolContext :members: - :exclude-members: location_cache, cleanup, clear_commands, params + :exclude-members: location_cache, cleanup, clear_commands Instruments =========== @@ -35,8 +35,10 @@ Labware signatures, since users should never construct these directly. .. autoclass:: opentrons.protocol_api.TrashBin() + :members: .. autoclass:: opentrons.protocol_api.WasteChute() + :members: Wells and Liquids ================= diff --git a/api/docs/v2/parameters/choosing.rst b/api/docs/v2/parameters/choosing.rst new file mode 100644 index 00000000000..2add49a0dd6 --- /dev/null +++ b/api/docs/v2/parameters/choosing.rst @@ -0,0 +1,53 @@ +:og:description: Advice on choosing effective parameters in Opentrons Python protocols. + +.. _good-rtps: + +************************ +Choosing Good Parameters +************************ + +The first decision you need to make when adding parameters to your protocol is "What should be parameterized?" Your goals in adding parameters should be the following: + +1. **Add flexibility.** Accommodate changes from run to run or from lab to lab. +2. **Work efficiently.** Don't burden run setup with too many choices or confusing options. +3. **Avoid errors.** Ensure that every combination of parameters produces an analyzable, runnable protocol. + +The trick to choosing good parameters is reasoning through the choices the protocol's users may make. If any of them lead to nonsensical outcomes or errors, adjust the parameters — or how your protocol :ref:`uses parameter values ` — to avoid those situations. + +Build on a Task +=============== + +Consider what scientific task is at the heart of your protocol, and build parameters that contribute to, rather than diverge from it. + +For example, it makes sense to add a parameter for number of samples to a DNA prep protocol that uses a particular reagent kit. But it wouldn't make sense to add a parameter for *which reagent kit* to use for DNA prep. That kind of parameter would affect so many aspects of the protocol that it would make more sense to maintain a separate protocol for each kit. + +Also consider how a small number of parameters can combine to produce many useful outputs. Take the serial dilution task from the :ref:`tutorial` as an example. We could add just three parameters to it: number of dilutions, dilution factor, and number of rows. Now that single protocol can produce a whole plate that gradually dilutes, a 2×4 grid that rapidly dilutes, and *thousands* of other combinations. + +Consider Contradictions +======================= + +Here's a common time-saving use of parameters: your protocol requires a 1-channel pipette and an 8-channel pipette, but it doesn't matter which mount they're attached to. Without parameters, you would have to assign the mounts in your protocol. Then if the robot is set up in the reverse configuration, you'd have to either physically swap the pipettes or modify your protocol. + +One way to get this information is to ask which mount the 1-channel pipette is on, and which mount the 8-channel pipette is on. But if a technician answers "left" to both questions — even by accident — the API will raise an error, because you can't load two pipettes on a single mount. It's no better to flip things around by asking which pipette is on the left mount, and which pipette is on the right mount. Now the technician can say that both mounts have a 1-channel pipette. This is even more dangerous, because it *might not* raise any errors in analysis. The protocol could run "successfully" on a robot with two 1-channel pipettes, but produce completely unintended results. + +The best way to avoid these contradictions is to collapse the two questions into one, with limited choices. Where are the pipettes mounted? Either the 1-channel is on the left and the 8-channel on the right, or the 8-channel is on the left and the 1-channel is on the right. This approach is best for several reasons: + +- It avoids analysis errors. +- It avoids potentially dangerous execution errors. +- It only requires answering one question instead of two. +- The :ref:`phrasing of the question and answer ` makes it clear that the protocol requires exactly one of each pipette type. + +Set Boundaries +============== + +Numerical parameters support minimum and maximum values, which you should set to avoid incorrect inputs that are outside of your protocol's possibile actions. + +Consider our earlier example of parameterizing serial dilution. Each of the three numerical parameters have logical upper and lower bounds, which we need to enforce to get sensible results. + +- *Number of dilutions* must be between 0 and 11 on a 96-well plate. And it may make sense to require at least 1 dilution. +- *Dilution factor* is a ratio, which we can express as a decimal number that must be between 0 and 1. +- *Number of rows* must be between 1 and 8 on a 96-well plate. + +What if you wanted to perform a dilution with 20 repetitions? It's possible with two 96-well plates, or with a 384-well plate. You could set the maximum for the number of dilutions to 24 and allow for these possibilities — either switching the plate type or loading an additional plate based on the provided value. + +But what if the technician wanted to do just 8 repetitions on a 384-well plate? That would require an additional parameter, an additional choice by the technician, and additional logic in your protocol code. It's up to you as the protocol author to decide if adding more parameters will make protocol setup overly difficult. Sometimes it's more efficient to work with two or three simple protocols rather than one that's long and complex. \ No newline at end of file diff --git a/api/docs/v2/parameters/defining.rst b/api/docs/v2/parameters/defining.rst new file mode 100644 index 00000000000..6b596ec8a0a --- /dev/null +++ b/api/docs/v2/parameters/defining.rst @@ -0,0 +1,181 @@ +:og:description: Define and set possible values for parameters in Opentrons Python protocols. + +.. _defining-rtp: + +******************* +Defining Parameters +******************* + +To use parameters, you need to define them in :ref:`a separate function ` within your protocol. Each parameter definition has two main purposes: to specify acceptable values, and to inform the protocol user what the parameter does. + +Depending on the :ref:`type of parameter `, you'll need to specify some or all of the following. + +.. list-table:: + :header-rows: 1 + + * - Attribute + - Details + * - ``variable_name`` + - + - A unique name for :ref:`referencing the parameter value ` elsewhere in the protocol. + - Must meet the usual requirements for `naming objects in Python `__. + * - ``display_name`` + - + - A label for the parameter shown in the Opentrons App or on the touchscreen. + - Maximum 30 characters. + * - ``description`` + - + - An optional longer explanation of what the parameter does, or how its values will affect the execution of the protocol. + - Maximum 100 characters. + * - ``default`` + - + - The value the parameter will have if the technician makes no changes to it during run setup. + * - ``minimum`` and ``maximum`` + - + - For numeric parameters only. + - Allows free entry of any value within the range (inclusive). + - Both values are required. + - Can't be used at the same time as ``choices``. + * - ``choices`` + - + - For numeric or string parameters. + - Provides a fixed list of values to choose from. + - Each choice has its own display name and value. + - Can't be used at the same time as ``minimum`` and ``maximum``. + * - ``units`` + - + - Optional, for numeric parameters with ``minimum`` and ``maximum`` only. + - Displays after the number during run setup. + - Does not affect the parameter's value or protocol execution. + - Maximum 10 characters. + + + +.. _add-parameters: + +The ``add_parameters()`` Function +================================= + +All parameter definitions are contained in a Python function, which must be named ``add_parameters`` and takes a single argument. Define ``add_parameters()`` before the ``run()`` function that contains protocol commands. + +The examples on this page assume the following definition, which uses the argument name ``parameters``. The type specification of the argument is optional. + +.. code-block:: + + def add_parameters(parameters: protocol_api.Parameters): + +Within this function definition, call methods on ``parameters`` to define parameters. The next section demonstrates how each type of parameter has its own method. + +.. _rtp-types: + +Types of Parameters +=================== + +The API supports four types of parameters: Boolean (:py:class:`bool`), integer (:py:class:`int`), floating point number (:py:class:`float`), and string (:py:class:`str`). It is not possible to mix types within a single parameter. + +Boolean Parameters +------------------ + +Boolean parameters are ``True`` or ``False`` only. + +.. code-block:: + + parameters.add_bool( + variable_name="dry_run", + display_name="Dry Run", + description="Skip incubation delays and shorten mix steps.", + default=False + ) + +During run setup, the technician can toggle between the two values. In the Opentrons App, Boolean parameters appear as a toggle switch. On the touchscreen, they appear as *On* or *Off*, for ``True`` and ``False`` respectively. + +.. versionadded:: 2.18 + +Integer Parameters +------------------ + +Integer parameters either accept a range of numbers or a list of numbers. You must specify one or the other; you can't create an open-ended prompt that accepts any integer. + +To specify a range, include ``minimum`` and ``maximum``. + +.. code-block:: + + parameters.add_int( + variable_name="volume", + display_name="Aspirate volume", + description="How much to aspirate from each sample.", + default=20, + minimum=10, + maximum=100, + unit="µL" + ) + +During run setup, the technician can enter any integer value from the minimum up to the maximum. Entering a value outside of the range will show an error. At that point, they can correct their custom value or restore the default value. + +To specify a list of numbers, include ``choices``. Each choice is a dictionary with entries for display name and value. The display names let you briefly explain the effect each choice will have. + +.. code-block:: + + parameters.add_int( + variable_name="volume", + display_name="Aspirate volume", + description="How much to aspirate from each sample.", + default=20, + choices=[ + {"display_name": "Low (10 µL)", "value": 10}, + {"display_name": "Medium (20 µL)", "value": 20}, + {"display_name": "High (50 µL)", "value": 50}, + ] + ) + +During run setup, the technician can choose from a menu of the provided choices. + +.. versionadded:: 2.18 + +Float Parameters +---------------- + +Float parameters either accept a range of numbers or a list of numbers. You must specify one or the other; you can't create an open-ended prompt that accepts any floating point number. + +Specifying a range or list is done exactly the same as in the integer examples above. The only difference is that all values must be floating point numbers. + +.. code-block:: + + parameters.add_float( + variable_name="volume", + display_name="Aspirate volume", + description="How much to aspirate from each sample.", + default=5.0, + choices=[ + {"display_name": "Low (2.5 µL)", "value": 2.5}, + {"display_name": "Medium (5 µL)", "value": 5.0}, + {"display_name": "High (10 µL)", "value": 10.0}, + ] + ) + +.. versionadded:: 2.18 + +String Parameters +----------------- + +String parameters only accept a list of values. You can't currently prompt for free text entry of a string value. + +To specify a list of strings, include ``choices``. Each choice is a dictionary with entries for display name and value. Only the display name will appear during run setup. + +A common use for string display names is to provide an easy-to-read version of an API load name. You can also use them to briefly explain the effect each choice will have. + +.. code-block:: + + parameters.add_str( + variable_name="pipette", + display_name="Pipette type", + choices=[ + {"display_name": "1-Channel 50 µL", "value": "flex_1channel_50"}, + {"display_name": "8-Channel 50 µL", "value": "flex_8channel_50"}, + ], + default="flex_1channel_50", + ) + +During run setup, the technician can choose from a menu of the provided choices. + +.. versionadded:: 2.18 diff --git a/api/docs/v2/parameters/style.rst b/api/docs/v2/parameters/style.rst new file mode 100644 index 00000000000..04e4ef1e36f --- /dev/null +++ b/api/docs/v2/parameters/style.rst @@ -0,0 +1,137 @@ +:og:description: Style and usage guidance for parameters in Opentrons Python protocols. + +.. _rtp-style: + +********************* +Parameter Style Guide +********************* + +It's important to write clear names and descriptions when you :ref:`define parameters ` in your protocols. Clarity improves the user experience for the technicians who run your protocols. They rely on your parameter names and descriptions to understand how the robot will function when running your protocol. + +Adopting the advice of this guide will help make your protocols clear, consistent, and ultimately easy to use. It also aligns them with protocols in the `Opentrons Protocol Library `_, which can help others access and replicate your science. + +General Guidance +================ + +**Parameter names are nouns.** Parameters should be discrete enough that you can describe them in a single word or short noun phrase. ``display_name`` is limited to 30 characters, and you can add more context in the description. + +Don't ask questions or put other sentence punctuation in parameter names. For example: + +.. list-table:: + + * - ✅ Dry run + - ❌ Dry run? + * - ✅ Sample count + - ❌ How many samples? + * - ✅ Number of samples + - ❌ Number of samples to process. + + +**Parameter descriptions explain actions.** In one or two clauses or sentences, state when and how the parameter value is used in the protocol. Don't merely restate the parameter name. + +Punctuate descriptions as sentences, even if they aren't complete sentences. For example: + +.. list-table:: + :header-rows: 1 + :widths: 1 3 + + * - Parameter name + - Parameter description + * - Dry run + - + - ✅ Skip incubation delays and shorten mix steps. + - ❌ Whether to do a dry run. + * - Aspirate volume + - + - ✅ How much to aspirate from each sample. + - ❌ Volume that the pipette will aspirate + * - Dilution factor + - + - ✅ Each step uses this ratio of total liquid to original solution. Express the ratio as a decimal. + - ❌ total/diluent ratio for the process + +Not every parameter requires a description! For example, in a protocol that uses only one pipette, it would be difficult to explain a parameter named "Pipette type" without repeating yourself. In a protocol that offers parameters for two different pipettes, it may be useful to summarize what steps each pipette performs. + +**Use sentence case for readability**. Sentence case means adding a capital letter to *only* the first word of the name and description. This gives your parameters a professional appearance. Keep proper names capitalized as they would be elsewhere in a sentence. For example: + +.. list-table:: + + * - ✅ Number of samples + - ❌ number of samples + * - ✅ Temperature Module slot + - ❌ Temperature module slot + * - ✅ Dilution factor + - ❌ Dilution Factor + +**Use numerals for all numbers.** In a scientific context, this includes single-digit numbers. Additionally, punctuate numbers according to the needs of your protocol's users. If you plan to share your protocol widely, consider using American English number punctuation (comma for thousands separator; period for decimal separator). + +**Order choices logically.** Place items within the ``choices`` attribute in the order that makes sense for your application. + +Numeric choices should either ascend or descend. Consider an offset parameter with choices. Sorting according to value is easy to use in either direction, but sorting by absolute value is difficult: + +.. list-table:: + + * - ✅ -3, -2, -1, 0, 1, 2, 3 + - ❌ 0, 1, -1, 2, -2, 3, -3 + * - ✅ 3, 2, 1, 0, -1, -2, -3 + - + +String choices may have an intrinsic ordering. If they don't, fall back to alphabetical order. + +.. list-table:: + :header-rows: 1 + + * - Parameter name + - Parameter description + * - Liquid color + - + - ✅ Red, Orange, Yellow, Green, Blue, Violet + - ❌ Blue, Green, Orange, Red, Violet, Yellow + * - Tube brand + - + - ✅ Eppendorf, Falcon, Generic, NEST + - ❌ Falcon, NEST, Eppendorf, Generic + +Type-Specific Guidance +====================== + +Booleans +-------- + +The ``True`` value of a Boolean corresponds to the word *On* and the ``False`` value corresponds to the word *Off*. + +**Avoid double negatives.** These are difficult to understand and may lead to a technician making an incorrect choice. Remember that negation can be part of a word's meaning! For example, it's difficult to reason about what will happen when a parameter named "Deactivate module" is set to "Off". + +**When in doubt, clarify in the description.** If you feel like you need to add extra clarity to your Boolean choices, use the phrase "When on" or "When off" at the beginning of your description. For example, a parameter named "Dry run" could have the description "When on, skip protocol delays and return tips instead of trashing them." + +Number Choices +-------------- + +**Don't repeat text in choices.** Rely on the name and description to indicate what the number refers to. It's OK to add units to the display names of numeric choices, because the ``unit`` attribute is ignored when you specify ``choices``. + +.. list-table:: + :header-rows: 1 + + * - Parameter name + - Parameter description + * - Number of columns + - + - ✅ 1, 2, 3 + - ❌ 1 column, 2 columns, 3 columns + * - Aspirate volume + - + - ✅ 10 µL, 20 µL, 50 µL + - ✅ Low (10 µL), Medium (20 µL), High (50 µL) + - ❌ Low volume, Medium volume, High volume + +**Use a range instead of choices when all values are acceptable.** It's faster and easier to enter a numeric value than to choose from a long list. For example, a "Number of columns" parameter that accepts any number 1 through 12 should specify a ``minimum`` and ``maximum``, rather than ``choices``. However, if the application requires that the parameter only accepts even numbers, you need to specify choices (2, 4, 6, 8, 10, 12). + +Strings +------- + +**Avoid strings that are synonymous with "yes" and "no".** When presenting exactly two string choices, consider their meaning. Can they be rephrased in terms of "yes/no", "true/false", or "on/off"? If no, then a string parameter is appropriate. If yes, it's better to use a Boolean, which appears in run setup as a toggle rather than a dropdown menu. + + - ✅ Blue, Red + - ✅ Left-to-right, Right-to-left + - ❌ Include, Exclude + - ❌ Yes, No diff --git a/api/docs/v2/parameters/use_case_dry_run.rst b/api/docs/v2/parameters/use_case_dry_run.rst new file mode 100644 index 00000000000..d23cd2aeb9c --- /dev/null +++ b/api/docs/v2/parameters/use_case_dry_run.rst @@ -0,0 +1,127 @@ +:og:description: How to set up and use a dry run parameter in an Opentrons Python protocol. + +.. _use-case-dry-run: + +**************************** +Parameter Use Case – Dry Run +**************************** + +When testing out a new protocol, it's common to perform a dry run to watch your robot go through all the steps without actually handling samples or reagents. This use case explores how to add a single Boolean parameter for whether you're performing a dry run. + +The code examples will show how this single value can control: + +- Skipping module actions and long delays. +- Reducing mix repetitions to save time. +- Returning tips (that never touched any liquid) to their racks. + +To keep things as simple as possible, this use case only focuses on setting up and using the value of the dry run parameter, which could be just one of many parameters in a complete protocol. + +Dry Run Definition +================== + +First, we need to set up the dry run parameter. We want to set up a simple yes/no choice for the technician running the protocol, so we'll use a Boolean parameter:: + + def add_parameters(parameters): + + parameters.add_bool( + variable_name="dry_run", + display_name="Dry Run", + description=( + "Skip delays," + " shorten mix steps," + " and return tips to their racks." + ), + default=False + ) + +This parameter is set to ``False`` by default, assuming that most runs will be live runs. In other words, during run setup the technician will have to change the parameter setting to perform a dry run. If they leave it as is, the robot will perform a live run. + +Additionally, since "dry run" can have different meanings in different contexts, it's important to include a ``description`` that indicates exactly what the parameter will control — in this case, three things. The following sections will show how to accomplish each of those when the dry run parameter is set to ``True``. + +Skipping Delays +=============== + +Many protocols have built-in delays, either for a module to work or to let a reaction happen passively. Lengthy delays just get in the way when verifying a protocol with a dry run. So wherever the protocol calls for a delay, we can check the value of ``protocol.params.dry_run`` and make the protocol behave accordingly. + +To start, let's consider a simple :py:meth:`.delay` command. We can wrap it in an ``if`` statement such that the delay will only execute when the run is *not* a dry run:: + + if protocol.params.dry_run is False: + protocol.delay(minutes=5) + +You can extend this approach to more complex situations, like module interactions. For example, in a protocol that moves a plate to the Thermocycler for an incubation, you'll want to perform all the movement steps — opening and closing the module lid, and moving the plate to and from the block — but skip the heating and cooling time. The simplest way to do this is, like in the delay example above, to wrap each skippable command:: + + protocol.move_labware(labware=plate, new_location=tc_mod, use_gripper=True) + if protocol.params.dry_run is False: + tc_mod.set_block_temperature(4) + tc_mod.set_lid_temperature(100) + tc_mod.close_lid() + pcr_profile = [ + {"temperature": 68, "hold_time_seconds": 180}, + {"temperature": 98, "hold_time_seconds": 180}, + ] + if protocol.params.dry_run is False: + tc_mod.execute_profile( + steps=pcr_profile, repetitions=1, block_max_volume=50 + ) + tc_mod.open_lid() + +Shortening Mix Steps +==================== + +Similar to delays, mix steps can take a long time because they are inherently repetitive actions. Mixing ten times takes ten times as long as mixing once! To save time, set a mix repetitions variable based on the value of ``protocol.params.dry_run`` and pass that to :py:meth:`.mix`:: + + if protocol.params.dry_run is True: + mix_reps = 1 + else: + mix_reps = 10 + pipette.mix(repetitions=mix_reps, volume=50, location=plate["A1"].bottom()) + +Note that this checks whether the dry run parameter is ``True``. If you prefer to set up all your ``if`` statements to check whether it's ``False``, you can reverse the logic:: + + if protocol.params.dry_run is False: + mix_reps = 10 + else: + mix_reps = 1 + +Returning Tips +============== + +Tips used in a dry run should be reusable — for another dry run, if nothing else. It doesn't make sense to dispose of them in a trash container, unless you specifically need to test movement to the trash. You can choose whether to use :py:meth:`.drop_tip` or :py:meth:`.return_tip` based on the value of ``protocol.params.dry_run``. If the protocol doesn't have too many tip drop actions, you can use an ``if`` statement each time:: + + if protocol.params.dry_run is True: + pipette.return_tip() + else: + pipette.drop_tip() + +However, repeating this block every time you handle tips could significantly clutter your code. Instead, you could define it as a function:: + + def return_or_drop(pipette): + if protocol.params.dry_run is True: + pipette.return_tip() + else: + pipette.drop_tip() + +Then call that function throughout your protocol:: + + pipette.pick_up_tip() + return_or_drop(pipette) + +.. note:: + + It's generally better to define a standalone function, rather than adding a method to the :py:class:`.InstrumentContext` class. This makes your custom, parameterized commands stand out from API methods in your code. + +Additionally, if your protocol uses enough tips that you have to replenish tip racks, you'll need separate behavior for dry runs and live runs. In a live run, once you've used all the tips, the rack is empty, because the tips are in the trash. In a dry run, once you've used all the tips in a rack, the rack is *full*, because you returned the tips. + +The API has methods to handle both of these situations. To continue using the same tip rack without physically replacing it, call :py:meth:`.reset_tipracks`. In the live run, move the empty tip rack off the deck and move a full one into place:: + + if protocol.params.dry_run is True: + pipette.reset_tipracks() + else: + protocol.move_labware( + labware=tips_1, new_location=chute, use_gripper=True + ) + protocol.move_labware( + labware=tips_2, new_location="C3", use_gripper=True + ) + +You can modify this code for similar cases. You may be moving tip racks by hand, rather than with the gripper. Or you could even mix the two, moving the used (but full) rack off-deck by hand — instead of dropping it down the chute, spilling all the tips — and have the gripper move a new rack into place. Ultimately, it's up to you to fine-tune your dry run behavior, and communicate it to your protocol's users with your parameter descriptions. diff --git a/api/docs/v2/parameters/use_case_sample_count.rst b/api/docs/v2/parameters/use_case_sample_count.rst new file mode 100644 index 00000000000..15933752592 --- /dev/null +++ b/api/docs/v2/parameters/use_case_sample_count.rst @@ -0,0 +1,273 @@ +:og:description: How to set up and use a sample count parameter in an Opentrons Python protocol. + +.. _use-case-sample-count: + +********************************* +Parameter Use Case – Sample Count +********************************* + +Choosing how many samples to process is important for efficient automation. This use case explores how a single parameter for sample count can have pervasive effects throughout a protocol. The examples are adapted from an actual parameterized protocol for DNA prep. The sample code will use 8-channel pipettes to process 8, 16, 24, or 32 samples. + +At first glance, it might seem like sample count would primarily affect liquid transfers to and from sample wells. But when using the Python API's full range of capabilities, it affects: + +- How many tip racks to load. +- The initial volume and placement of reagents. +- Pipetting to and from samples. +- If and when tip racks need to be replaced. + +To keep things as simple as possible, this use case only focuses on setting up and using the value of the sample count parameter, which is just one of several parameters present in the full protocol. + +From Samples to Columns +======================= + +First of all, we need to set up the sample count parameter so it's both easy for technicians to understand during protocol setup and easy for us to use in the protocol's ``run()`` function. + +We want to limit the number of samples to 8, 16, 24, or 32, so we'll use an integer parameter with choices:: + + def add_parameters(parameters): + + parameters.add_int( + variable_name="sample_count", + display_name="Sample count", + description="Number of input DNA samples.", + default=24, + choices=[ + {"display_name": "8", "value": 8}, + {"display_name": "16", "value": 16}, + {"display_name": "24", "value": 24}, + {"display_name": "32", "value": 32}, + ] + ) + +All of the possible values are multiples of 8, because the protocol will use an 8-channel pipette to process an entire column of samples at once. Considering how 8-channel pipettes access wells, it may be more useful to operate with a *column count* in code. We can set a ``column_count`` very early in the ``run()`` function by accessing the value of ``params.sample_count`` and dividing it by 8:: + + def run(protocol): + + column_count = protocol.params.sample_count // 8 + +Most examples below will use ``column_count``, rather than redoing (and retyping!) this calculation multiple times. + +Loading Tip Racks +================= + +Tip racks come first in most protocols. To ensure that the protocol runs to completion, we need to load enough tip racks to avoid running out of tips. + +We could load as many tip racks as are needed for our maximum number of samples, but that would be suboptimal. Run setup is faster when the technician doesn't have to load extra items onto the deck. So it's best to examine the protocol's steps and determine how many racks are needed for each value of ``sample_count``. + +In the case of this DNA prep protocol, we can create formulas for the number of 200 µL and 50 µL tip racks needed. The following factors go into these computations: + +- 50 µL tips + - 1 fixed action that picks up once per protocol. + - 7 variable actions that pick up once per sample column. +- 200 µL tips + - 2 fixed actions that pick up once per protocol. + - 11 variable actions that pick up once per sample column. + +Since each tip rack has 12 columns, divide the number of pickup actions by 12 to get the number of racks needed. And we always need to round up — performing 13 pickups requires 2 racks. The :py:func:`math.ceil` method rounds up to the nearest integer. We'll add ``from math import ceil`` at the top of the protocol and then calculate the number of tip racks as follows:: + + tip_rack_50_count = ceil((1 + 7 * column_count) / 12) + tip_rack_200_count = ceil((2 + 13 * column_count) / 12) + +Running the numbers shows that the maximum combined number of tip racks is 7. Now we have to decide where to load up to 7 racks, working around the modules and other labware on the deck. Assuming we're running this protocol on a Flex with staging area slots, they'll all fit! (If you don't have staging area slots, you can load labware off-deck instead.) We'll reserve these slots for the different size racks:: + + tip_rack_50_slots = ["B3", "C3", "B4"] + tip_rack_200_slots = ["A2", "B2", "A3", "A4"] + +Finally, we can combine this information to call :py:meth:`~.ProtocolContext.load_labware`. Depending on the number of racks needed, we'll slice that number of elements from the slot list and use a `list comprehension `__ to gather up the loaded tip racks. For the 50 µL tips, this would look like:: + + tip_racks_50 = [ + protocol.load_labware( + load_name="opentrons_flex_96_tiprack_50ul", + location=slot + ) + for slot in tip_rack_50_slots[:tip_rack_50_count] + ] + +Then we can associate those lists of tip racks directly with each pipette as we load them. All together, the start of our ``run()`` function looks like this:: + + # calculate column count from sample count + column_count = protocol.params.sample_count // 8 + + # calculate number of required tip racks + tip_rack_50_count = ceil((1 + 7 * column_count) / 12) + tip_rack_200_count = ceil((2 + 13 * column_count) / 12) + + # assign tip rack locations (maximal case) + tip_rack_50_slots = ["B3", "C3", "B4"] + tip_rack_200_slots = ["A2", "B2", "A3", "A4"] + + # create lists of loaded tip racks + # limit to number of needed racks for each type + tip_racks_50 = [ + protocol.load_labware( + load_name="opentrons_flex_96_tiprack_50ul", + location=slot + ) + for slot in tip_rack_50_slots[:tip_rack_50_count] + ] + tip_racks_200 = [ + protocol.load_labware( + load_name="opentrons_flex_96_tiprack_200ul", + location=slot + ) + for slot in tip_rack_200_slots[:tip_rack_200_count] + ] + + pipette_50 = protocol.load_instrument( + instrument_name="flex_8channel_50", + mount="right", + tip_racks=tip_racks_50 + ) + pipette_1000 = protocol.load_instrument( + instrument_name="flex_1channel_1000", + mount="left", + tip_racks=tip_racks_200 + ) + +This code will load as few as 3 tip racks and as many as 7, and associate them with the correct pipettes — all based on a single choice from a dropdown menu at run setup. + +Loading Liquids +=============== + +Next come the reagents, samples, and the labware that holds them. + +The required volume of each reagent is dependent on the sample count. While the full protocol defines more than ten liquids, we'll show three reagents plus the samples here. + +First, let's load a reservoir and :ref:`define ` the three example liquids. Definitions only specify the name, description, and display color, so our sample count parameter doesn't come into play yet:: + + # labware to hold reagents + reservoir = protocol.load_labware( + load_name="nest_12_reservoir_15ml", location="C2" + ) + + # reagent liquid definitions + ampure_liquid = protocol.define_liquid( + name="AMPure", description="AMPure Beads", display_color="#704848" + ) + tagstop_liquid = protocol.define_liquid( + name="TAGSTOP", description="Tagmentation Stop", display_color="#FF0000" + ) + twb_liquid = protocol.define_liquid( + name="TWB", description="Tagmentation Wash Buffer", display_color="#FFA000" + ) + +Now we'll bring sample count into consideration as we :ref:`load the liquids `. The application requires the following volumes for each column of samples: + +.. list-table:: + :header-rows: 1 + + * - Liquid + - | Volume + | (µL per column) + * - AMPure Beads + - 180 + * - Tagmentation Stop + - 10 + * - Tagmentation Wash Buffer + - 900 + +To calculate the total volume for each liquid, we'll multiply these numbers by ``column_count`` and by 1.1 (to ensure that the pipette can aspirate the required volume without drawing in air at the bottom of the well). This calculation can be done inline as the ``volume`` value of :py:meth:`.load_liquid`:: + + reservoir["A1"].load_liquid( + liquid=ampure_liquid, volume=180 * column_count * 1.1 + ) + reservoir["A2"].load_liquid( + liquid=tagstop_liquid, volume=10 * column_count * 1.1 + ) + reservoir["A4"].load_liquid( + liquid=twb_liquid, volume=900 * column_count * 1.1 + ) + +Now, for example, the volume of AMPure beads to load will vary from 198 µL for a single sample column up to 792 µL for four columns. + +.. tip:: + + Does telling a technician to load 792 µL of a liquid seem overly precise? Remember that you can perform any calculation you like to set the value of ``volume``! For example, you could round the AMPure volume up to the nearest 10 µL:: + + volume=ceil((180 * column_count * 1.1) / 10) * 10 + +Finally, it's good practice to label the wells where the samples reside. The sample plate starts out atop the Heater-Shaker Module: + +.. code-block:: + + hs_mod = protocol.load_module( + module_name="heaterShakerModuleV1", location="D1" + ) + hs_adapter = hs_mod.load_adapter(name="opentrons_96_pcr_adapter") + sample_plate = hs_adapter.load_labware( + name="opentrons_96_wellplate_200ul_pcr_full_skirt", + label="Sample Plate", + ) + +Now we can construct a ``for`` loop to label each sample well with ``load_liquid()``. The simplest way to do this is to combine our original *sample count* with the fact that the :py:meth:`.Labware.wells()` accessor returns wells top-to-bottom, left-to-right:: + + # define sample liquid + sample_liquid = protocol.define_liquid( + name="Samples", description=None, display_color="#52AAFF" + ) + + # load 40 µL in each sample well + for w in range(protocol.params.sample_count): + sample_plate.wells()[w].load_liquid(liquid=sample_liquid, volume=40) + +Processing Samples +================== + +When it comes time to process the samples, we'll return to working by column, since the protocol uses an 8-channel pipette. There are many pipetting stages in the full protocol, but this section will examine just the stage for adding the Tagmentation Stop liquid. The same techniques would apply to similar stages. + +For pipetting in the original sample locations, we'll command the 50 µL pipette to move to some or all of A1–A4 on the sample plate. Similar to when we loaded tip racks earlier, we can use ``column_count`` to slice a list containing these well names, and then iterate over that list with a ``for`` loop:: + + for w in ["A1", "A2", "A3", "A4"][:column_count]: + pipette_50.pick_up_tip() + pipette_50.aspirate(volume=13, location=reservoir["A2"].bottom()) + pipette_50.dispense(volume=3, location=reservoir["A2"].bottom()) + pipette_50.dispense(volume=10, location=sample_plate[w].bottom()) + pipette_50.move_to(location=sample_plate[w].bottom()) + pipette_50.mix(repetitions=10, volume=20) + pipette_50.blow_out(location=sample_plate[w].top(z=-2)) + pipette_50.drop_tip() + +Each time through the loop, the pipette will fill from the same well of the reservoir and then dispense (and mix and blow out) in a different column of the sample plate. + +Later steps of the protocol will move intermediate samples to the middle of the plate (columns 5–8) and final samples to the right side of the plate (columns 9–12). When moving directly from one set of columns to another, we have to track *both lists* with the ``for`` loop. The :py:func:`zip` function lets us pair up the lists of well names and step through them in parallel:: + + for initial, intermediate in zip( + ["A1", "A2", "A3", "A4"][:column_count], + ["A5", "A6", "A7", "A8"][:column_count], + ): + pipette_50.pick_up_tip() + pipette_50.aspirate(volume=13, location=sample_plate[initial]) + pipette_50.dispense(volume=13, location=sample_plate[intermediate]) + pipette_50.drop_tip() + +This will transfer from column 1 to 5, 2 to 6, and so on — depending on the number of samples chosen during run setup. + +Replenishing Tips +================= + +For the higher values of ``protocol.params.sample_count``, the protocol will load tip racks in the staging area slots (column 4). Since pipettes can't reach these slots, we need to move these tip racks into the working area (columns 1–3) before issuing a pipetting command that targets them, or the API will raise an error. + +A protocol without parameters will always run out of tips at the same time — just add a :py:meth:`.move_labware` command when that happens. But as we saw in the Processing Samples section above, our parameterized protocol will go through tips at a different rate depending on the sample count. + +In our simplified example, we know that when the sample count is 32, the first 200 µL tip rack will be exhausted after three stages of pipetting using the 1000 µL pipette. So, after that step, we could add:: + + if protocol.params.sample_count == 32: + protocol.move_labware( + labware=tip_racks_200[0], + new_location=chute, + use_gripper=True, + ) + protocol.move_labware( + labware=tip_racks_200[-1], + new_location="A2", + use_gripper=True, + ) + +This will replace the first 200 µL tip rack (in slot A2) with the last 200 µL tip rack (in the staging area). + +However, in the full protocol, sample count is not the only parameter that affects the rate of tip use. It would be unwieldy to calculate in advance all the permutations of when tip replenishment is necessary. Instead, before each stage of the protocol, we could use :py:obj:`.Well.has_tip()` to check whether the first tip rack is empty. If the *last well* of the rack is empty, we can assume that the entire rack is empty and needs to be replaced:: + + if tip_racks_200[0].wells()[-1].has_tip is False: + # same move_labware() steps as above + +For a protocol that uses tips at a faster rate than this one — such that it might exhaust a tip rack in a single ``for`` loop of pipetting steps — you may have to perform such checks even more frequently. You can even define a function that counts tips or performs ``has_tip`` checks in combination with picking up a tip, and use that instead of :py:meth:`.pick_up_tip` every time you pipette. The built-in capabilities of Python and the methods of the Python Protocol API give you the flexibility to add this kind of smart behavior to your protocols. diff --git a/api/docs/v2/parameters/using_values.rst b/api/docs/v2/parameters/using_values.rst new file mode 100644 index 00000000000..b0d3b1a4151 --- /dev/null +++ b/api/docs/v2/parameters/using_values.rst @@ -0,0 +1,94 @@ +:og:description: Access parameter values in Opentrons Python protocols. + +.. _using-rtp: + +**************** +Using Parameters +**************** + +Once you've :ref:`defined parameters `, their values are accessible anywhere within the ``run()`` function of your protocol. + +The ``params`` Object +===================== + +Protocols with parameters have a :py:obj:`.ProtocolContext.params` object, which contains the values of all parameters as set during run setup. Each attribute of ``params`` corresponds to the ``variable_name`` of a parameter. + +For example, consider a protocol that defines the following three parameters: + +- ``add_bool`` with ``variable_name="dry_run"`` +- ``add_int`` with ``variable_name="sample_count"`` +- ``add_float`` with ``variable_name="volume"`` + +Then ``params`` will gain three attributes: ``params.dry_run``, ``params.sample_count``, and ``params.volume``. You can use these attributes anywhere you want to access their values, including directly as arguments of methods. + +.. code-block:: + + if protocol.params.dry_run is False: + pipette.mix(repetitions=10, volume=protocol.params.volume) + +You can also save parameter values to variables with names of your choosing. + +Parameter Types +=============== + +Each attribute of ``params`` has the type corresponding to its parameter definition. Keep in mind the parameter's type when using its value in different contexts. + +Say you wanted to add a comment to the run log, stating how many samples the protocol will process. Since ``sample_count`` is an ``int``, you'll need to cast it to a ``str`` or the API will raise an error. + +.. code-block:: + + protocol.comment( + "Processing " + str(protocol.params.sample_count) + " samples." + ) + +Also be careful with ``int`` types when performing calculations: dividing an ``int`` by an ``int`` with the ``/`` operator always produces a ``float``, even if there is no remainder. The :ref:`sample count use case ` converts a sample count to a column count by dividing by 8 — but it uses the ``//`` integer division operator, so the result can be used for creating ranges, slicing lists, and as ``int`` argument values without having to cast it in those contexts. + +Limitations +=========== + +Since ``params`` is only available within the ``run()`` function, there are certain aspects of a protocol that parameter values can't affect. These include, but are not limited to the following: + + +.. list-table:: + :header-rows: 1 + + * - Information + - Location + * - ``import`` statements + - At the beginning of the protocol. + * - Robot type (Flex or OT-2) + - In the ``requirements`` dictionary. + * - API version + - In the ``requirements`` or ``metadata`` dictionary. + * - Protocol name + - In the ``metadata`` dictionary. + * - Protocol description + - In the ``metadata`` dictionary. + * - Protocol author + - In the ``metadata`` dictionary. + * - Other runtime parameters + - In the ``add_parameters()`` function. + * - Non-nested function definitions + - Anywhere outside of ``run()``. + +Additionally, keep in mind that updated parameter values are applied by reanalyzing the protocol. This means you can't depend on updated values for any action that takes place *prior to reanalysis*. + +An example of such an action is applying labware offset data. Say you have a parameter that changes the type of well plate you load in a particular slot:: + + # within add_parameters() + parameters.add_str( + variable_name="plate_type", + display_name="Well plate type", + choices=[ + {"display_name": "Corning", "value": "corning_96_wellplate_360ul_flat"}, + {"display_name": "NEST", "value": "nest_96_wellplate_200ul_flat"}, + ], + default="corning_96_wellplate_360ul_flat", + ) + + # within run() + plate = protocol.load_labware( + load_name="protocol.params.plate_type", location="D2" + ) + +When performing run setup, you're prompted to apply offsets before selecting parameter values. This is your only opportunity to apply offsets, so they're applied for the default parameter values — in this case, the Corning plate. If you then change the "Well plate type" parameter to the NEST plate, the NEST plate will have default offset values (0.0 on all axes). You can fix this by running Labware Position Check, since it takes place after reanalysis, or by using :py:meth:`.Labware.set_offset` in your protocol. diff --git a/api/docs/v2/pipettes/partial_tip_pickup.rst b/api/docs/v2/pipettes/partial_tip_pickup.rst index a1e78fed570..a2ca1e614a3 100644 --- a/api/docs/v2/pipettes/partial_tip_pickup.rst +++ b/api/docs/v2/pipettes/partial_tip_pickup.rst @@ -23,7 +23,7 @@ For greater convenience, also import the individual layout constants that you pl from opentrons.protocol_api import COLUMN, ALL -Then when you call ``configure_nozzle_layout`` later in your protocol, you can set ``style=COLUMN``. +Then when you call ``configure_nozzle_layout`` later in your protocol, you can set ``style=COLUMN``. Here is the start of a protocol that performs both imports, loads a 96-channel pipette, and sets it to pick up a single column of tips. @@ -106,6 +106,10 @@ When switching between full and partial pickup, you may want to organize your ti partial_tip_racks = [tips_1, tips_2] full_tip_racks = [tips_3, tips_4] +.. Tip:: + + It's also good practice to keep separate lists of tip racks when using multiple partial tip pickup configurations (i.e., using both column 1 and column 12 in the same protocol). This improves positional accuracy when picking up tips. Additionally, use Labware Position Check in the Opentrons App to ensure that the partial configuration is well-aligned to the rack. + Now, when you configure the nozzle layout, you can reference the appropriate list as the value of ``tip_racks``:: pipette.configure_nozzle_layout( @@ -120,7 +124,7 @@ Now, when you configure the nozzle layout, you can reference the appropriate lis tip_racks=full_tip_racks ) pipette.pick_up_tip() # picks up full rack in C1 - + This keeps tip tracking consistent across each type of pickup. And it reduces the risk of errors due to the incorrect presence or absence of a tip rack adapter. @@ -135,12 +139,7 @@ The API will raise errors for potential labware crashes when using a column nozz - Simulate your protocol and compare the run preview to your expectations of where the pipette will travel. - Perform a dry run with only tip racks on the deck. Have the Emergency Stop Pendant handy in case you see an impending crash. -For column pickup, Opentrons recommends using the nozzles in column 12 of the pipette. - -Using Column 12 ---------------- - -The examples in this section use a 96-channel pipette configured to pick up tips with column 12:: +For column pickup, Opentrons recommends using the nozzles in column 12 of the pipette:: pipette.configure_nozzle_layout( style=COLUMN, @@ -164,9 +163,6 @@ You would get a similar error trying to aspirate from or dispense into a well pl When using column 12 for partial tip pickup and pipetting, generally organize your deck with the shortest labware on the left side of the deck, and the tallest labware on the right side. -Using Column 1 --------------- - If your application can't accommodate a deck layout that works well with column 12, you can configure the 96-channel pipette to pick up tips with column 1:: pipette.configure_nozzle_layout( @@ -174,16 +170,6 @@ If your application can't accommodate a deck layout that works well with column start="A1", ) -The major drawback of this configuration, compared to using column 12, is that tip tracking is not available with column 1. You must always specify a ``location`` parameter for :py:meth:`.pick_up_tip`. This *requires careful tip tracking* so you don't place the pipette over more than a single column of unused tips at once. You can write some additional code to manage valid tip pickup locations, like this:: - - tip_rack = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "C1") - pipette.configure_nozzle_layout(style=COLUMN, start="A1") - row_a = tip_rack.rows()[0] - pipette.pick_up_tip(row_a.pop()) # pick up A12-H12 - pipette.drop_tip() - pipette.pick_up_tip(row_a.pop()) # pick up A11-H11 - pipette.drop_tip() - -This code first constructs a list of all the wells in row A of the tip rack. Then, when picking up a tip, instead of referencing one of those wells directly, the ``location`` is set to ``row_a.pop()``. This uses the `built-in pop method `_ to get the last item from the list and remove it from the list. If you keep using this approach to pick up tips, you'll get an error once the tip rack is empty — not from the API, but from Python itself, since you're trying to ``pop`` an item from an empty list. +.. note:: -Additionally, you can't access the rightmost columns in labware in column 3, since they are beyond the movement limit of the pipette. The exact number of inaccessible columns varies by labware type. Any well that is within 29 mm of the right edge of the slot may be inaccessible in a column 1 configuration. Call ``configure_nozzle_layout()`` again to switch to a column 12 layout if you need to pipette in that area. + When using a column 1 layout, the pipette can't reach the rightmost portion of labware in slots A3–D3. Any well that is within 29 mm of the right edge of the slot may be inaccessible. Use a column 12 layout if you need to pipette in that area. \ No newline at end of file diff --git a/api/docs/v2/robot_position.rst b/api/docs/v2/robot_position.rst index 8b2ed762e71..a0e5e7579f3 100644 --- a/api/docs/v2/robot_position.rst +++ b/api/docs/v2/robot_position.rst @@ -21,6 +21,8 @@ Top, Bottom, and Center Every well on every piece of labware has three addressable positions: top, bottom, and center. The position is determined by the labware definition and what the labware is loaded on top of. You can use these positions as-is or calculate other positions relative to them. +.. _well-top: + Top ^^^^ @@ -116,6 +118,31 @@ All positions relative to labware are adjusted automatically based on labware of You should only adjust labware offsets in your Python code if you plan to run your protocol in Jupyter Notebook or from the command line. See :ref:`using_lpc` in the Advanced Control article for information. +.. _position-relative-trash: + +Position Relative to Trash Containers +===================================== + +Movement to :py:class:`.TrashBin` or :py:class:`.WasteChute` objects is based on the horizontal *center* of the pipette. This is different than movement to labware, which is based on the primary channel (the back channel on 8-channel pipettes, and the back-left channel on 96-channel pipettes in default configuration). Using the center of the pipette ensures that all attached tips are over the trash container for blowing out, dropping tips, or other disposal operations. + +.. note:: + In API version 2.15 and earlier, trash containers are :py:class:`.Labware` objects that have a single well. See :py:obj:`.fixed_trash` and :ref:`position-relative-labware` above. + +You can adjust the position of the pipette center with the :py:meth:`.TrashBin.top` and :py:meth:`.WasteChute.top` methods. These methods allow adjustments along the x-, y-, and z-axes. In contrast, ``Well.top()``, :ref:`covered above `, only allows z-axis adjustment. With no adjustments, the "top" position is centered on the x- and y-axes and is just below the opening of the trash container. + +.. code-block:: python + + trash = protocol.load_trash_bin("A3") + + trash # pipette center just below trash top center + trash.top() # same position + trash.top(z=10) # 10 mm higher + trash.top(y=10) # 10 mm towards back, default height + +.. versionadded:: 2.18 + +Another difference between the trash container ``top()`` methods and ``Well.top()`` is that they return an object of the same type, not a :py:class:`.Location`. This helps prevent performing undesired actions in trash containers. For example, you can :py:meth:`.aspirate` at a location or from a well, but not from a trash container. On the other hand, you can :py:meth:`.blow_out` at a location, well, trash bin, or waste chute. + .. _protocol-api-deck-coords: Position Relative to the Deck diff --git a/api/docs/v2/runtime_parameters.rst b/api/docs/v2/runtime_parameters.rst new file mode 100644 index 00000000000..71689eedb50 --- /dev/null +++ b/api/docs/v2/runtime_parameters.rst @@ -0,0 +1,29 @@ +:og:description: Define and customize parameters in Opentrons Python protocols. + +.. _runtime-parameters: + +****************** +Runtime Parameters +****************** + +.. toctree:: + parameters/choosing + parameters/defining + parameters/using_values + parameters/use_case_sample_count + parameters/use_case_dry_run + parameters/style + +Runtime parameters let you define user-customizable variables in your Python protocols. This gives you greater flexibility and puts extra control in the hands of the technician running the protocol — without forcing them to switch between lots of protocol files or write code themselves. + +This section begins with the fundamentals of runtime parameters: + +- Preliminary advice on how to :ref:`choose good parameters `, before you start writing code. +- The syntax for :ref:`defining parameters ` with boolean, numeric, and string values. +- How to :ref:`use parameter values ` in your protocol, building logic and API calls that implement the technician's choices. + +It continues with a selection of use cases and some overall style guidance. When adding parameters, you are in charge of the user experience when it comes time to set up the protocol! These pages outline best practices for making your protocols reliable and easy to use. + +- :ref:`Use case – sample count `: Change behavior throughout a protocol based on how many samples you plan to process. Setting sample count exactly saves time, tips, and reagents. +- :ref:`Use case – dry run `: Test your protocol, rather than perform a live run, just by flipping a toggle. +- :ref:`Style and usage `: When you're a protocol author, you write code. When you're a parameter author, you write words. Follow this advice to make things as clear as possible for the technicians who will run your protocol. diff --git a/api/docs/v2/versioning.rst b/api/docs/v2/versioning.rst index 5819bee4b47..b2391fc7041 100644 --- a/api/docs/v2/versioning.rst +++ b/api/docs/v2/versioning.rst @@ -68,7 +68,7 @@ The maximum supported API version for your robot is listed in the Opentrons App If you upload a protocol that specifies a higher API level than the maximum supported, your robot won't be able to analyze or run your protocol. You can increase the maximum supported version by updating your robot software and Opentrons App. -Opentrons robots running the latest software (7.2.0) support the following version ranges: +Opentrons robots running the latest software (7.3.0) support the following version ranges: * **Flex:** version 2.15–|apiLevel|. * **OT-2:** versions 2.0–|apiLevel|. @@ -84,6 +84,8 @@ This table lists the correspondence between Protocol API versions and robot soft +-------------+------------------------------+ | API Version | Introduced in Robot Software | +=============+==============================+ +| 2.18 | 7.3.0 | ++-------------+------------------------------+ | 2.17 | 7.2.0 | +-------------+------------------------------+ | 2.16 | 7.1.0 | @@ -128,6 +130,14 @@ This table lists the correspondence between Protocol API versions and robot soft Changes in API Versions ======================= +Version 2.18 +------------ + +- Define customizable parameters with the new ``add_parameters()`` function, and access their values on the :py:obj:`.ProtocolContext.params` object during a protocol run. See :ref:`runtime-parameters` and related pages for more information. +- Move the pipette to positions relative to the top of a trash container. See :ref:`position-relative-trash`. The default behavior of :py:meth:`.drop_tip` also accounts for this new possibility. +- :py:meth:`.set_offset` has been restored to the API with new behavior that applies to labware type–location pairs. +- Automatic tip tracking is now available for all nozzle configurations. + Version 2.17 ------------ diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 68e39888405..89f19f6c7ec 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -312,27 +312,34 @@ def dispense( # noqa: C901 :type volume: int or float :param location: Tells the robot where to dispense liquid held in the pipette. - The location can be a :py:class:`.Well` or a - :py:class:`.Location`. + The location can be a :py:class:`.Well`, :py:class:`.Location`, + :py:class:`.TrashBin`, or :py:class:`.WasteChute`. - - If the location is a ``Well``, the pipette will dispense + - If a ``Well``, the pipette will dispense at or above the bottom center of the well. The distance (in mm) from the well bottom is specified by :py:obj:`well_bottom_clearance.dispense `. - - If the location is a ``Location`` (e.g., the result of - :py:meth:`.Well.top` or :py:meth:`.Well.bottom`), the robot - will dispense into that specified position. + - If a ``Location`` (e.g., the result of + :py:meth:`.Well.top` or :py:meth:`.Well.bottom`), the pipette + will dispense at that specified position. - - If the ``location`` is unspecified, the robot will - dispense into its current position. + - If a trash container, the pipette will dispense at a location + relative to its center and the trash container's top center. + See :ref:`position-relative-trash` for details. + + - If unspecified, the pipette will + dispense at its current position. If only a ``location`` is passed (e.g., ``pipette.dispense(location=plate['A1'])``), all of the liquid aspirated into the pipette will be dispensed (the amount is accessible through :py:attr:`current_volume`). + .. versionchanged:: 2.16 + Accepts ``TrashBin`` and ``WasteChute`` values. + :param rate: How quickly a pipette dispenses liquid. The speed in µL/s is calculated as ``rate`` multiplied by :py:attr:`flow_rate.dispense `. If not specified, defaults to 1.0. See @@ -549,7 +556,11 @@ def blow_out( :ref:`blow-out`. :param location: The blowout location. If no location is specified, the pipette - will blow out from its current position. + will blow out from its current position. + + .. versionchanged:: 2.16 + Accepts ``TrashBin`` and ``WasteChute`` values. + :type location: :py:class:`.Well` or :py:class:`.Location` or ``None`` :raises RuntimeError: If no location is specified and the location cache is @@ -1010,11 +1021,6 @@ def drop_tip( If no location is passed (e.g. ``pipette.drop_tip()``), the pipette will drop the attached tip into its :py:attr:`trash_container`. - Starting with API version 2.15, if the trash container is the default fixed - trash, the API will instruct the pipette to drop tips in different locations - within the trash container. Varying the tip drop location helps prevent tips - from piling up in a single location. - The location in which to drop the tip can be manually specified with the ``location`` argument. The ``location`` argument can be specified in several ways: @@ -1033,8 +1039,21 @@ def drop_tip( the ``WasteChute`` object. For example, ``pipette.drop_tip(location=waste_chute)``. + In API versions 2.15 to 2.17, if ``location`` is a ``TrashBin`` or not + specified, the API will instruct the pipette to drop tips in different locations + within the bin. Varying the tip drop location helps prevent tips + from piling up in a single location. + + Starting with API version 2.18, the API will only vary the tip drop location if + ``location`` is not specified. Specifying a ``TrashBin`` as the ``location`` + behaves the same as specifying :py:meth:`.TrashBin.top`, which is a fixed position. + :param location: - The location to drop the tip. + Where to drop the tip. + + .. versionchanged:: 2.16 + Accepts ``TrashBin`` and ``WasteChute`` values. + :type location: :py:class:`~.types.Location` or :py:class:`.Well` or ``None`` :param home_after: @@ -1481,7 +1500,11 @@ def move_to( See :ref:`move-to` for examples. - :param location: The location to move to. + :param location: Where to move to. + + .. versionchanged:: 2.16 + Accepts ``TrashBin`` and ``WasteChute`` values. + :type location: :py:class:`~.types.Location` :param force_direct: If ``True``, move directly to the destination without arc motion. @@ -1936,6 +1959,10 @@ def configure_nozzle_layout( should be of the same format used when identifying wells by name. Required unless setting ``style=ALL``. + .. note:: + If possible, don't use both ``start="A1"`` and ``start="A12"`` to pick up + tips *from the same rack*. Doing so can affect positional accuracy. + :type start: str or ``None`` :param tip_racks: Behaves the same as setting the ``tip_racks`` parameter of :py:meth:`.load_instrument`. If not specified, the new configuration resets diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 3b7ae943208..be6cc442782 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -577,22 +577,39 @@ def set_offset(self, x: float, y: float, z: float) -> None: """Set the labware's position offset. The offset is an x, y, z vector in deck coordinates - (see :ref:`protocol-api-deck-coords`) that the motion system - will add to any movement targeting this labware instance. + (see :ref:`protocol-api-deck-coords`). - The offset *will not apply* to any other labware instances, - even if those labware are of the same type. + How the motion system applies the offset depends on the API level of the protocol. - This method is *only* for use with mechanisms like - :obj:`opentrons.execute.get_protocol_api`, which lack an interactive way - to adjust labware offsets. (See :ref:`advanced-control`.) + .. list-table:: + :header-rows: 1 - .. warning:: + * - API level + - Offset behavior + * - 2.12–2.13 + - Offsets only apply to the exact :py:class:`.Labware` instance. + * - 2.14–2.17 + - ``set_offset()`` is not available, and the API raises an error. + * - 2.18 and newer + - + - Offsets apply to any labware of the same type, in the same on-deck location. + - Offsets can't be set on labware that is currently off-deck. + - Offsets do not follow a labware instance when using :py:meth:`.move_labware`. + + .. note:: - If you're uploading a protocol via the Opentrons App, don't use this method, - because it will produce undefined behavior. - Instead, use Labware Position Check in the app or on the touchscreen. + Setting offsets with this method will override any labware offsets set + by running Labware Position Check in the Opentrons App. + + This method is designed for use with mechanisms like + :obj:`opentrons.execute.get_protocol_api`, which lack an interactive way + to adjust labware offsets. (See :ref:`advanced-control`.) + + .. versionchanged:: 2.14 + Temporarily removed. + .. versionchanged:: 2.18 + Restored, and now applies to labware type–location pairs. """ if ( self._api_version >= ENGINE_CORE_API_VERSION diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index feb8f56d91c..07c4bdfff5d 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -224,6 +224,15 @@ def bundled_data(self) -> Dict[str, bytes]: @property @requires_version(2, 18) def params(self) -> Parameters: + """ + The values of runtime parameters, as set during run setup. + + Each attribute of this object corresponds to the ``variable_name`` of a parameter. + See :ref:`using-rtp` for details. + + Parameter values can only be set during run setup. If you try to alter the value + of any attribute of ``params``, the API will raise an error. + """ return self._params def cleanup(self) -> None: