From b948ddab0cbf30baba2384f3d6117362f32ec737 Mon Sep 17 00:00:00 2001
From: fly1d <309400591+fly1d@users.noreply.github.com>
Date: Fri, 11 Sep 2026 11:05:39 +0800
Subject: [PATCH] Add TipSpot guide
Document TipSpot access, tip state configuration, and custom rack definitions in an executable notebook.
Assisted-by: OpenAI Codex
---
docs/resources/index.md | 3 +-
.../itemized-resource/tiprack/tip-spot.ipynb | 180 ++++++++++++++++++
.../itemized-resource/tiprack/tiprack.rst | 10 +-
3 files changed, 191 insertions(+), 2 deletions(-)
create mode 100644 docs/resources/itemized-resource/tiprack/tip-spot.ipynb
diff --git a/docs/resources/index.md b/docs/resources/index.md
index 847610a431f..a7e343617c7 100644
--- a/docs/resources/index.md
+++ b/docs/resources/index.md
@@ -88,7 +88,7 @@ PLR's `Resource` subclasses in the inheritance tree are:
| ├── ItemizedResource |
| │ ├── Plate |
- | │ ├── TipRack |
+ | │ ├── TipRack |
| │ └── ContainerRack |
| │ └── TubeRack |
@@ -97,6 +97,7 @@ PLR's `Resource` subclasses in the inheritance tree are:
| │ └── PlateHolder |
+ | ├── TipSpot |
| ├── Lid |
| ├── PlateAdapter |
diff --git a/docs/resources/itemized-resource/tiprack/tip-spot.ipynb b/docs/resources/itemized-resource/tiprack/tip-spot.ipynb
new file mode 100644
index 00000000000..01d5d078823
--- /dev/null
+++ b/docs/resources/itemized-resource/tiprack/tip-spot.ipynb
@@ -0,0 +1,180 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "tipspot-intro",
+ "metadata": {},
+ "source": [
+ "# TipSpot\n",
+ "\n",
+ "A {class}`~pylabrobot.resources.tip_rack.TipSpot` represents one position in a\n",
+ "{class}`~pylabrobot.resources.TipRack`. The spot is a resource with its own location and\n",
+ "dimensions, while the disposable {class}`~pylabrobot.resources.tip.Tip` that it may contain is a\n",
+ "separate object. Each spot owns a {class}`~pylabrobot.resources.tip_tracker.TipTracker` that records\n",
+ "whether a tip is present."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "accessing-spots",
+ "metadata": {},
+ "source": [
+ "## Accessing tip spots\n",
+ "\n",
+ "Tip spots are normally obtained from a tip rack rather than created directly. Use\n",
+ "{meth}`~pylabrobot.resources.ItemizedResource.get_item` for one spot and\n",
+ "{meth}`~pylabrobot.resources.ItemizedResource.get_items` for a range:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "access-example",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pylabrobot.resources import hamilton_96_tiprack_300uL_filter\n",
+ "\n",
+ "tip_rack = hamilton_96_tiprack_300uL_filter(name=\"tips\")\n",
+ "\n",
+ "a1 = tip_rack.get_item(\"A1\")\n",
+ "first_column = tip_rack.get_items(\"A1:H1\")\n",
+ "\n",
+ "assert [spot.get_identifier() for spot in first_column] == [f\"{row}1\" for row in \"ABCDEFGH\"]\n",
+ "assert a1.get_identifier() == \"A1\"\n",
+ "assert a1.has_tip()\n",
+ "tip = a1.get_tip()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "identifiers",
+ "metadata": {},
+ "source": [
+ "The identifiers use transposed spreadsheet notation: rows are letters and columns are numbers.\n",
+ "For example, `A1:H1` selects the first column from top to bottom. A spot's location is relative\n",
+ "to its rack, so moving the rack does not change the spot's local coordinates."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "initial-state",
+ "metadata": {},
+ "source": [
+ "## Setting the initial tip state\n",
+ "\n",
+ "Predefined racks are full by default. Pass `with_tips=False` to start with an empty rack, or use\n",
+ "the rack-level methods to describe a partially filled rack:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "state-example",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "tip_rack = hamilton_96_tiprack_300uL_filter(name=\"tips\", with_tips=False)\n",
+ "tip_rack.set_tip_state({\"A1\": True, \"B1\": True})\n",
+ "\n",
+ "assert tip_rack.get_item(\"A1\").has_tip()\n",
+ "assert not tip_rack.get_item(\"C1\").has_tip()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "tracking-guide",
+ "metadata": {},
+ "source": [
+ "Use {meth}`~pylabrobot.resources.TipRack.fill` and\n",
+ "{meth}`~pylabrobot.resources.TipRack.empty` to update every spot. See the\n",
+ "{doc}`tip-tracking guide ` for how tip state\n",
+ "is validated and updated during liquid-handling operations."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "defining-spots",
+ "metadata": {},
+ "source": [
+ "## Defining tip spots\n",
+ "\n",
+ "A custom tip rack definition supplies an ordered grid of `TipSpot` objects. The `make_tip`\n",
+ "callable receives the unique name that the spot assigns to each tip it creates:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "definition-example",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pylabrobot.resources import Tip, TipRack, TipSpot, create_ordered_items_2d\n",
+ "\n",
+ "\n",
+ "def make_tip(name: str) -> Tip:\n",
+ " return Tip(\n",
+ " name=name,\n",
+ " has_filter=False,\n",
+ " total_tip_length=50.0,\n",
+ " maximal_volume=300.0,\n",
+ " fitting_depth=8.0,\n",
+ " )\n",
+ "\n",
+ "\n",
+ "name = \"custom_tip_rack\"\n",
+ "tip_rack = TipRack(\n",
+ " name=name,\n",
+ " size_x=20.0,\n",
+ " size_y=20.0,\n",
+ " size_z=10.0,\n",
+ " ordered_items=create_ordered_items_2d(\n",
+ " TipSpot,\n",
+ " num_items_x=2,\n",
+ " num_items_y=2,\n",
+ " dx=1.0,\n",
+ " dy=1.0,\n",
+ " dz=0.0,\n",
+ " item_dx=9.0,\n",
+ " item_dy=9.0,\n",
+ " size_x=8.0,\n",
+ " size_y=8.0,\n",
+ " make_tip=make_tip,\n",
+ " name_prefix=name,\n",
+ " ),\n",
+ ")\n",
+ "\n",
+ "assert tip_rack.get_item(\"A1\").get_tip().name == \"custom_tip_rack_tipspot_A1#0\"\n",
+ "assert tip_rack.get_item(\"A1\").location.x == 1.0\n",
+ "assert tip_rack.get_item(\"A1\").location.y == 10.0\n",
+ "assert tip_rack.get_item(\"B1\").location.y == 1.0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "grid-coordinates",
+ "metadata": {},
+ "source": [
+ "The dimensions and offsets are in millimeters. `dx` sets the x coordinate of the leftmost\n",
+ "column, while `dy` sets the y coordinate of the frontmost row (the bottom row when the rack is\n",
+ "viewed as a grid). Row A is at the back of the rack, where y is largest. `dz` sets the z\n",
+ "coordinate for every spot, and `item_dx` and `item_dy` are the origin-to-origin spacing.\n",
+ "\n",
+ "See {class}`~pylabrobot.resources.tip_rack.TipSpot` for the complete API reference."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/resources/itemized-resource/tiprack/tiprack.rst b/docs/resources/itemized-resource/tiprack/tiprack.rst
index d4b9fb62be6..201828f58e4 100644
--- a/docs/resources/itemized-resource/tiprack/tiprack.rst
+++ b/docs/resources/itemized-resource/tiprack/tiprack.rst
@@ -1,6 +1,14 @@
TipRack
=======
-TODO: write a tutorial
+A ``TipRack`` is an :class:`~pylabrobot.resources.itemized_resource.ItemizedResource` whose items
+are :class:`~pylabrobot.resources.tip_rack.TipSpot` objects. See the :doc:`TipSpot guide
+` for details on accessing spots and defining a custom rack.
See :class:`~pylabrobot.resources.tip_rack.TipRack` for the API reference.
+
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+
+ tip-spot