diff --git a/docs/user_guide/machine-agnostic-features/using-the-visualizer.ipynb b/docs/user_guide/machine-agnostic-features/using-the-visualizer.ipynb index 2a9d3d27a4c..3706e8a1b29 100644 --- a/docs/user_guide/machine-agnostic-features/using-the-visualizer.ipynb +++ b/docs/user_guide/machine-agnostic-features/using-the-visualizer.ipynb @@ -74,11 +74,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Setting up the liquid handler.\n", - "Resource deck was assigned to the liquid handler.\n", - "Resource trash was assigned to the liquid handler.\n", - "Resource trash_core96 was assigned to the liquid handler.\n", - "Resource waste_block was assigned to the liquid handler.\n" + "Setting up the liquid handler.\n" ] } ], @@ -168,15 +164,7 @@ "execution_count": 8, "id": "140872be", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resource tip carrier was assigned to the liquid handler.\n" - ] - } - ], + "outputs": [], "source": [ "lh.deck.assign_child_resource(tip_car, rails=15)" ] @@ -199,15 +187,7 @@ "execution_count": 10, "id": "d618ec6a", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resource plate carrier was assigned to the liquid handler.\n" - ] - } - ], + "outputs": [], "source": [ "lh.deck.assign_child_resource(plt_car, rails=8)" ] @@ -312,8 +292,7 @@ "metadata": {}, "outputs": [], "source": [ - "plate_1_liquids = [[(None, 500)]]*96\n", - "plate_1.set_well_liquids(plate_1_liquids)" + "plate_1.set_well_volumes([500]*96)" ] }, { @@ -323,8 +302,7 @@ "metadata": {}, "outputs": [], "source": [ - "plate_2_liquids = [[(None, 100)], [(None, 500)]]*(96//2)\n", - "plate_2.set_well_liquids(plate_2_liquids)" + "plate_2.set_well_volumes([100, 500]*(96//2))" ] }, { @@ -400,10 +378,10 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tips_01_tipspot_0_0 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", - " p1: tips_01_tipspot_1_1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", - " p2: tips_01_tipspot_2_2 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", - " p3: tips_01_tipspot_3_3 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p0: tips_01_tipspot_A1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p1: tips_01_tipspot_B2 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p2: tips_01_tipspot_C3 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p3: tips_01_tipspot_D4 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -423,10 +401,10 @@ "text": [ "Dropping tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tips_01_tipspot_0_0 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", - " p1: tips_01_tipspot_1_1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", - " p2: tips_01_tipspot_2_2 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", - " p3: tips_01_tipspot_3_3 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p0: tips_01_tipspot_A1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p1: tips_01_tipspot_B2 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p2: tips_01_tipspot_C3 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p3: tips_01_tipspot_D4 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -454,7 +432,7 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tips_01_tipspot_0_0 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p0: tips_01_tipspot_A1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -474,7 +452,7 @@ "text": [ "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 200.0 plate_01_well_1_0 0,0,0 None None None \n" + " p0: 200.0 plate_01_well_A2 0,0,0 None None None \n" ] } ], @@ -494,7 +472,7 @@ "text": [ "Dispensing:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 200.0 plate_02_well_0_0 0,0,0 None None None \n" + " p0: 200.0 plate_02_well_A1 0,0,0 None None None \n" ] } ], @@ -514,7 +492,7 @@ "text": [ "Dropping tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tips_01_tipspot_0_0 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p0: tips_01_tipspot_A1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -656,7 +634,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.7" + "version": "3.9.23" }, "toc": { "base_numbering": 1, diff --git a/docs/user_guide/machine-agnostic-features/using-trackers.ipynb b/docs/user_guide/machine-agnostic-features/using-trackers.ipynb index 7a266850997..4736b756619 100644 --- a/docs/user_guide/machine-agnostic-features/using-trackers.ipynb +++ b/docs/user_guide/machine-agnostic-features/using-trackers.ipynb @@ -19,10 +19,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Setting up the liquid handler.\n", - "Resource deck was assigned to the liquid handler.\n", - "Resource trash was assigned to the liquid handler.\n", - "Resource trash_core96 was assigned to the liquid handler.\n" + "Setting up the liquid handler.\n" ] } ], @@ -219,15 +216,7 @@ "cell_type": "code", "execution_count": 11, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resource tip carrier was assigned to the liquid handler.\n" - ] - } - ], + "outputs": [], "source": [ "lh.deck.assign_child_resource(tip_carrier, rails=3)" ] @@ -272,10 +261,10 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tip rack_tipspot_0_0 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p0: tip rack_tipspot_A1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", "Dropping tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: empty tip rack_tipspot_0_0 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p0: empty tip rack_tipspot_A1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", "As expected: Tip spot does not have a tip.\n" ] } @@ -311,7 +300,7 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tip rack_tipspot_0_1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p0: tip rack_tipspot_B1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", "As expected: Tip spot already has a tip.\n" ] } @@ -336,7 +325,7 @@ "text": [ "Dropping tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: empty tip rack_tipspot_0_1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p0: empty tip rack_tipspot_B1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -395,7 +384,7 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tip rack_tipspot_0_2 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p0: tip rack_tipspot_C1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", "As expected: Channel has tip\n" ] } @@ -451,10 +440,10 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tip rack_tipspot_0_4 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p0: tip rack_tipspot_E1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p1: tip rack_tipspot_0_4 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p1: tip rack_tipspot_E1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -496,10 +485,10 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tip rack_tipspot_0_5 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p0: tip rack_tipspot_F1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p1: tip rack_tipspot_0_5 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p1: tip rack_tipspot_F1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -541,10 +530,10 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tip rack_tipspot_0_5 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p0: tip rack_tipspot_F1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p1: tip rack_tipspot_0_5 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p1: tip rack_tipspot_F1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -587,10 +576,10 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tip rack_tipspot_0_6 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", + " p0: tip rack_tipspot_G1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n", "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p1: tip rack_tipspot_0_6 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p1: tip rack_tipspot_G1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -678,15 +667,6 @@ "cell_type": "code", "execution_count": 29, "metadata": {}, - "outputs": [], - "source": [ - "from pylabrobot.resources.liquid import Liquid" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, "outputs": [ { "data": { @@ -694,29 +674,21 @@ "(10, 350)" ] }, - "execution_count": 30, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "plate.get_item(\"A1\").tracker.set_liquids([(Liquid.WATER, 10)])\n", + "plate.get_item(\"A1\").tracker.set_volume(10)\n", "plate.get_item(\"A1\").tracker.get_used_volume(), plate.get_item(\"A1\").tracker.get_free_volume()" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 30, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resource plate carrier was assigned to the liquid handler.\n" - ] - } - ], + "outputs": [], "source": [ "lh.deck.assign_child_resource(plt_carrier, rails=9)" ] @@ -731,7 +703,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 31, "metadata": {}, "outputs": [ { @@ -740,16 +712,16 @@ "text": [ "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 10.0 plate_well_0_0 0,0,0 None None None \n" + " p0: 10.0 plate_well_A1 0,0,0 None None None \n" ] }, { "data": { "text/plain": [ - "(0, 360)" + "(0.0, 360.0)" ] }, - "execution_count": 32, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -761,7 +733,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 32, "metadata": {}, "outputs": [ { @@ -770,16 +742,16 @@ "text": [ "Dispensing:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 10.0 plate_well_0_0 0,0,0 None None None \n" + " p0: 10.0 plate_well_A1 0,0,0 None None None \n" ] }, { "data": { "text/plain": [ - "(10, 350)" + "(10.0, 350.0)" ] }, - "execution_count": 33, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -799,7 +771,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ @@ -818,14 +790,14 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 34, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "As expected: Tracker only has 0uL\n" + "As expected: Not enough liquid in container: 100.0uL > 0.0uL.\n" ] } ], @@ -848,7 +820,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 35, "metadata": {}, "outputs": [ { @@ -857,7 +829,7 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tip rack_tipspot_1_0 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p0: tip rack_tipspot_A2 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -868,7 +840,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 36, "metadata": {}, "outputs": [ { @@ -877,42 +849,42 @@ "text": [ "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_0 0,0,0 None None None \n", + " p0: 100.0 plate_well_A1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_1 0,0,0 None None None \n", + " p0: 100.0 plate_well_B1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_2 0,0,0 None None None \n", + " p0: 100.0 plate_well_C1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_3 0,0,0 None None None \n", + " p0: 100.0 plate_well_D1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_4 0,0,0 None None None \n", + " p0: 100.0 plate_well_E1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_5 0,0,0 None None None \n", + " p0: 100.0 plate_well_F1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_6 0,0,0 None None None \n", + " p0: 100.0 plate_well_G1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_7 0,0,0 None None None \n", + " p0: 100.0 plate_well_H1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_1_0 0,0,0 None None None \n", + " p0: 100.0 plate_well_A2 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_1_1 0,0,0 None None None \n", - "As expected: Container has too little volume: 100uL > 65uL.\n" + " p0: 100.0 plate_well_B2 0,0,0 None None None \n", + "As expected: Not enough space in container: 100.0uL > 65.0uL.\n" ] } ], "source": [ "# fill the first two columns\n", "for i in range(16):\n", - " plate.get_item(i).tracker.set_liquids([(Liquid.WATER, 100)])\n", + " plate.get_item(i).tracker.set_volume(100)\n", "\n", "try:\n", " # aspirate from the first two columns - this is more liquid than the tip can hold\n", @@ -934,14 +906,14 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 37, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "As expected: Tracker only has 0uL\n" + "As expected: Not enough liquid in container: 100.0uL > 0.0uL.\n" ] } ], @@ -964,7 +936,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 38, "metadata": {}, "outputs": [ { @@ -973,7 +945,7 @@ "text": [ "Picking up tips:\n", "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", - " p0: tip rack_tipspot_1_1 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" + " p0: tip rack_tipspot_B2 0,0,0 HamiltonTip 1065 8 95.1 Yes \n" ] } ], @@ -984,7 +956,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 39, "metadata": {}, "outputs": [ { @@ -993,27 +965,27 @@ "text": [ "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_1 0,0,0 None None None \n", + " p0: 100.0 plate_well_B1 0,0,0 None None None \n", "Dispensing:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_0 0,0,0 None None None \n", + " p0: 100.0 plate_well_A1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_2 0,0,0 None None None \n", + " p0: 100.0 plate_well_C1 0,0,0 None None None \n", "Dispensing:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_0 0,0,0 None None None \n", + " p0: 100.0 plate_well_A1 0,0,0 None None None \n", "Aspirating:\n", "pip# vol(ul) resource offset flow rate blowout lld_z \n", - " p0: 100.0 plate_well_0_3 0,0,0 None None None \n", - "As expected: Container has too little volume: 100uL > 60uL.\n" + " p0: 100.0 plate_well_D1 0,0,0 None None None \n", + "As expected: Not enough space in container: 100.0uL > 60.0uL.\n" ] } ], "source": [ "# fill the first column\n", "for i in range(8):\n", - " plate.get_item(i).tracker.set_liquids([(Liquid.WATER, 100)])\n", + " plate.get_item(i).tracker.set_volume(100)\n", "\n", "try:\n", " # aspirate liquid from the first column into the first well\n", @@ -1041,7 +1013,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.6" + "version": "3.9.23" }, "orig_nbformat": 4 }, diff --git a/pylabrobot/liquid_handling/backends/chatterbox.py b/pylabrobot/liquid_handling/backends/chatterbox.py index f07a43fdf07..183eebaab60 100644 --- a/pylabrobot/liquid_handling/backends/chatterbox.py +++ b/pylabrobot/liquid_handling/backends/chatterbox.py @@ -133,7 +133,6 @@ async def aspirate( f"{'flow rate':<{LiquidHandlerChatterboxBackend._flow_rate_length}} " f"{'blowout':<{LiquidHandlerChatterboxBackend._blowout_length}} " f"{'lld_z':<{LiquidHandlerChatterboxBackend._lld_z_length}} " - # f"{'liquids':<20}" # TODO: add liquids ) for key in backend_kwargs: header += f"{key:<{LiquidHandlerChatterboxBackend._kwargs_length}} "[-16:] @@ -149,7 +148,6 @@ async def aspirate( f"{str(o.flow_rate):<{LiquidHandlerChatterboxBackend._flow_rate_length}} " f"{str(o.blow_out_air_volume):<{LiquidHandlerChatterboxBackend._blowout_length}} " f"{str(o.liquid_height):<{LiquidHandlerChatterboxBackend._lld_z_length}} " - # f"{o.liquids if o.liquids is not None else 'none'}" ) for key, value in backend_kwargs.items(): if isinstance(value, list) and all(isinstance(v, bool) for v in value): @@ -174,7 +172,6 @@ async def dispense( f"{'flow rate':<{LiquidHandlerChatterboxBackend._flow_rate_length}} " f"{'blowout':<{LiquidHandlerChatterboxBackend._blowout_length}} " f"{'lld_z':<{LiquidHandlerChatterboxBackend._lld_z_length}} " - # f"{'liquids':<20}" # TODO: add liquids ) for key in backend_kwargs: header += f"{key:<{LiquidHandlerChatterboxBackend._kwargs_length}} "[-16:] @@ -190,7 +187,6 @@ async def dispense( f"{str(o.flow_rate):<{LiquidHandlerChatterboxBackend._flow_rate_length}} " f"{str(o.blow_out_air_volume):<{LiquidHandlerChatterboxBackend._blowout_length}} " f"{str(o.liquid_height):<{LiquidHandlerChatterboxBackend._lld_z_length}} " - # f"{o.liquids if o.liquids is not None else 'none'}" ) for key, value in backend_kwargs.items(): if isinstance(value, list) and all(isinstance(v, bool) for v in value): diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 2eabdb7f7c8..9ccbc7b6f68 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1880,18 +1880,13 @@ async def aspirate( if hamilton_liquid_classes is None: hamilton_liquid_classes = [] for i, op in enumerate(ops): - liquid = Liquid.WATER # default to WATER - # [-1][0]: get last liquid in well, [0] is indexing into the tuple - if len(op.liquids) > 0 and op.liquids[-1][0] is not None: - liquid = op.liquids[-1][0] - hamilton_liquid_classes.append( get_star_liquid_class( tip_volume=op.tip.maximal_volume, is_core=False, is_tip=True, has_filter=op.tip.has_filter, - liquid=liquid, + liquid=Liquid.WATER, # default to WATER jet=jet[i], blow_out=blow_out[i], ) @@ -2271,18 +2266,13 @@ async def dispense( if hamilton_liquid_classes is None: hamilton_liquid_classes = [] for i, op in enumerate(ops): - liquid = Liquid.WATER # default to WATER - # [-1][0]: get last liquid in tip, [0] is indexing into the tuple - if len(op.liquids) > 0 and op.liquids[-1][0] is not None: - liquid = op.liquids[-1][0] - hamilton_liquid_classes.append( get_star_liquid_class( tip_volume=op.tip.maximal_volume, is_core=False, is_tip=True, has_filter=op.tip.has_filter, - liquid=liquid, + liquid=Liquid.WATER, # default to WATER jet=jet[i], blow_out=blow_out[i], ) @@ -2797,17 +2787,13 @@ async def aspirate96( liquid_height = position.z + (aspiration.liquid_height or 0) - liquid_to_be_aspirated = Liquid.WATER - if len(aspiration.liquids[0]) > 0 and aspiration.liquids[0][0][0] is not None: - # [channel][liquid][PyLabRobot.resources.liquid.Liquid] - liquid_to_be_aspirated = aspiration.liquids[0][0][0] hlc = hlc or get_star_liquid_class( tip_volume=tip.maximal_volume, is_core=True, is_tip=True, has_filter=tip.has_filter, # get last liquid in pipette, first to be dispensed - liquid=liquid_to_be_aspirated, + liquid=Liquid.WATER, # default to WATER jet=jet, blow_out=blow_out, # see comment in method docstring ) @@ -3076,17 +3062,13 @@ async def dispense96( liquid_height = position.z + (dispense.liquid_height or 0) - liquid_to_be_dispensed = Liquid.WATER # default to water. - if len(dispense.liquids[0]) > 0 and dispense.liquids[0][-1][0] is not None: - # [channel][liquid][PyLabRobot.resources.liquid.Liquid] - liquid_to_be_dispensed = dispense.liquids[0][-1][0] hlc = hlc or get_star_liquid_class( tip_volume=tip.maximal_volume, is_core=True, is_tip=True, has_filter=tip.has_filter, # get last liquid in pipette, first to be dispensed - liquid=liquid_to_be_dispensed, + liquid=Liquid.WATER, # default to WATER jet=jet, blow_out=blow_out, # see comment in method docstring ) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py index 49435b2ce9f..77a94f1b106 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py @@ -384,7 +384,7 @@ async def test_aspirate56(self): assert self.plate.lid is not None self.plate.lid.unassign() for well in self.plate.get_items(["A1", "B1"]): - well.tracker.set_liquids([(None, 100 * 1.072)]) # liquid class correction + well.tracker.set_volume(100 * 1.072) # liquid class correction await self.lh.aspirate(self.plate["A1", "B1"], vols=[100, 100], use_channels=[4, 5]) self.STAR._write_and_read_command.assert_has_calls( [ @@ -414,7 +414,7 @@ async def test_single_channel_aspiration(self): assert self.plate.lid is not None self.plate.lid.unassign() well = self.plate.get_item("A1") - well.tracker.set_liquids([(None, 100 * 1.072)]) # liquid class correction + well.tracker.set_volume(100 * 1.072) # liquid class correction await self.lh.aspirate([well], vols=[100]) self.STAR._write_and_read_command.assert_has_calls( [ @@ -435,7 +435,7 @@ async def test_single_channel_aspiration_liquid_height(self): assert self.plate.lid is not None self.plate.lid.unassign() well = self.plate.get_item("A1") - well.tracker.set_liquids([(None, 100 * 1.072)]) # liquid class correction + well.tracker.set_volume(100 * 1.072) # liquid class correction await self.lh.aspirate([well], vols=[100], liquid_height=[10]) # This passes the test, but is not the real command. @@ -459,7 +459,7 @@ async def test_multi_channel_aspiration(self): self.plate.lid.unassign() wells = self.plate.get_items("A1:B1") for well in wells: - well.tracker.set_liquids([(None, 100 * 1.072)]) # liquid class correction + well.tracker.set_volume(100 * 1.072) # liquid class correction await self.lh.aspirate(self.plate["A1:B1"], vols=[100] * 2) # This passes the test, but is not the real command. diff --git a/pylabrobot/liquid_handling/backends/hamilton/vantage_backend.py b/pylabrobot/liquid_handling/backends/hamilton/vantage_backend.py index fe59f71b80f..e14ef2965e5 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/vantage_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/vantage_backend.py @@ -638,17 +638,13 @@ async def aspirate( if hlcs is None: hlcs = [] for j, bo, op in zip(jet, blow_out, ops): - liquid = Liquid.WATER # default to WATER - # [-1][0]: get last liquid in well, [0] is indexing into the tuple - if len(op.liquids) > 0 and op.liquids[-1][0] is not None: - liquid = op.liquids[-1][0] hlcs.append( get_vantage_liquid_class( tip_volume=op.tip.maximal_volume, is_core=False, is_tip=True, has_filter=op.tip.has_filter, - liquid=liquid, + liquid=Liquid.WATER, # default to WATER jet=j, blow_out=bo, ) @@ -832,17 +828,13 @@ async def dispense( if hlcs is None: hlcs = [] for j, bo, op in zip(jet, blow_out, ops): - liquid = Liquid.WATER # default to WATER - # [-1][0]: get last liquid in tip, [0] is indexing into the tuple - if len(op.liquids) > 0 and op.liquids[-1][0] is not None: - liquid = op.liquids[-1][0] hlcs.append( get_vantage_liquid_class( tip_volume=op.tip.maximal_volume, is_core=False, is_tip=True, has_filter=op.tip.has_filter, - liquid=liquid, + liquid=Liquid.WATER, # default to WATER jet=j, blow_out=bo, ) @@ -1101,17 +1093,13 @@ async def aspirate96( liquid_height = position.z + (aspiration.liquid_height or 0) tip = next(tip for tip in aspiration.tips if tip is not None) - liquid_to_be_aspirated = Liquid.WATER # default to water - if len(aspiration.liquids[0]) > 0 and aspiration.liquids[0][-1][0] is not None: - # first part of tuple in last liquid of first well - liquid_to_be_aspirated = aspiration.liquids[0][-1][0] if hlc is None: hlc = get_vantage_liquid_class( tip_volume=tip.maximal_volume, is_core=True, is_tip=True, has_filter=tip.has_filter, - liquid=liquid_to_be_aspirated, + liquid=Liquid.WATER, # default to WATER jet=jet, blow_out=blow_out, ) @@ -1266,17 +1254,13 @@ async def dispense96( liquid_height = position.z + (dispense.liquid_height or 0) + 10 tip = next(tip for tip in dispense.tips if tip is not None) - liquid_to_be_dispensed = Liquid.WATER # default to WATER - if len(dispense.liquids[0]) > 0 and dispense.liquids[0][-1][0] is not None: - # first part of tuple in last liquid of first well - liquid_to_be_dispensed = dispense.liquids[0][-1][0] if hlc is None: hlc = get_vantage_liquid_class( tip_volume=tip.maximal_volume, is_core=True, is_tip=True, has_filter=tip.has_filter, - liquid=liquid_to_be_dispensed, + liquid=Liquid.WATER, # default to WATER jet=jet, blow_out=blow_out, # see method docstring ) diff --git a/pylabrobot/liquid_handling/backends/http_tests.py b/pylabrobot/liquid_handling/backends/http_tests.py index 6e618c0a210..09fad096a0f 100644 --- a/pylabrobot/liquid_handling/backends/http_tests.py +++ b/pylabrobot/liquid_handling/backends/http_tests.py @@ -132,7 +132,7 @@ async def test_aspirate(self): ) self.lh.update_head_state({0: self.tip_rack.get_tip("A1")}) well = self.plate.get_item("A1") - well.tracker.set_liquids([(None, 10)]) + well.tracker.set_volume(10) await self.lh.aspirate([well], [10]) @responses.activate @@ -145,7 +145,7 @@ async def test_dispense(self): status=200, ) self.lh.update_head_state({0: self.tip_rack.get_tip("A1")}) - self.lh.head[0].get_tip().tracker.add_liquid(None, 10) + self.lh.head[0].get_tip().tracker.add_liquid(10) with no_volume_tracking(): await self.lh.dispense(self.plate["A1"], [10]) diff --git a/pylabrobot/liquid_handling/backends/opentrons_backend_tests.py b/pylabrobot/liquid_handling/backends/opentrons_backend_tests.py index b69b4b6a544..1333e39e17a 100644 --- a/pylabrobot/liquid_handling/backends/opentrons_backend_tests.py +++ b/pylabrobot/liquid_handling/backends/opentrons_backend_tests.py @@ -156,7 +156,7 @@ def assert_parameters( mock_aspirate.side_effect = assert_parameters await self.test_tip_pick_up() - self.plate.get_well("A1").tracker.set_liquids([(None, 10)]) + self.plate.get_well("A1").tracker.set_volume(10) await self.lh.aspirate(self.plate["A1"], vols=[10]) @patch("ot_api.lh.dispense_in_place") diff --git a/pylabrobot/liquid_handling/backends/serializing_backend.py b/pylabrobot/liquid_handling/backends/serializing_backend.py index 669992521d6..4d9413b5d1d 100644 --- a/pylabrobot/liquid_handling/backends/serializing_backend.py +++ b/pylabrobot/liquid_handling/backends/serializing_backend.py @@ -95,7 +95,6 @@ async def aspirate(self, ops: List[SingleChannelAspiration], use_channels: List[ "flow_rate": serialize(op.flow_rate), "liquid_height": serialize(op.liquid_height), "blow_out_air_volume": serialize(op.blow_out_air_volume), - "liquids": serialize(op.liquids), "mix": serialize(op.mix), } for op in ops @@ -115,7 +114,6 @@ async def dispense(self, ops: List[SingleChannelDispense], use_channels: List[in "flow_rate": serialize(op.flow_rate), "liquid_height": serialize(op.liquid_height), "blow_out_air_volume": serialize(op.blow_out_air_volume), - "liquids": serialize(op.liquids), "mix": serialize(op.mix), } for op in ops @@ -153,7 +151,6 @@ async def aspirate96( "flow_rate": serialize(aspiration.flow_rate), "liquid_height": serialize(aspiration.liquid_height), "blow_out_air_volume": serialize(aspiration.blow_out_air_volume), - "liquids": serialize(aspiration.liquids), "tips": [serialize(tip) for tip in aspiration.tips], } } @@ -171,7 +168,6 @@ async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDisp "flow_rate": serialize(dispense.flow_rate), "liquid_height": serialize(dispense.liquid_height), "blow_out_air_volume": serialize(dispense.blow_out_air_volume), - "liquids": serialize(dispense.liquids), "tips": [serialize(tip) for tip in dispense.tips], } } diff --git a/pylabrobot/liquid_handling/backends/serializing_backend_tests.py b/pylabrobot/liquid_handling/backends/serializing_backend_tests.py index f095ad4baca..ca2fd4349f5 100644 --- a/pylabrobot/liquid_handling/backends/serializing_backend_tests.py +++ b/pylabrobot/liquid_handling/backends/serializing_backend_tests.py @@ -85,7 +85,7 @@ async def test_drop_tips(self): async def test_aspirate(self): well = self.plate.get_item("A1") - well.tracker.set_liquids([(None, 10)]) + well.tracker.set_volume(10) tip = self.tip_rack.get_tip(0) self.lh.update_head_state({0: tip}) self.backend.clear() @@ -104,7 +104,6 @@ async def test_aspirate(self): "flow_rate": None, "liquid_height": None, "blow_out_air_volume": None, - "liquids": [[None, 10]], "mix": None, } ], @@ -133,7 +132,6 @@ async def test_dispense(self): "flow_rate": None, "liquid_height": None, "blow_out_air_volume": None, - "liquids": [[None, 10]], "mix": None, } ], @@ -187,7 +185,6 @@ async def test_aspirate96(self): "flow_rate": None, "liquid_height": None, "blow_out_air_volume": None, - "liquids": [[[None, 10]]] * 96, # tuple, list of liquids per well, list of wells "tips": [serialize(tip) for tip in tips], } }, @@ -212,7 +209,6 @@ async def test_dispense96(self): "flow_rate": None, "liquid_height": None, "blow_out_air_volume": None, - "liquids": [[[None, 10.0]]] * 96, # tuple, list of liquids per well, list of wells "tips": [serialize(tip) for tip in tips], } }, diff --git a/pylabrobot/liquid_handling/backends/tecan/EVO_backend.py b/pylabrobot/liquid_handling/backends/tecan/EVO_backend.py index 635ac8271f1..6af3e842d25 100644 --- a/pylabrobot/liquid_handling/backends/tecan/EVO_backend.py +++ b/pylabrobot/liquid_handling/backends/tecan/EVO_backend.py @@ -364,7 +364,7 @@ async def aspirate( tecan_liquid_classes = [ get_liquid_class( target_volume=op.volume, - liquid_class=op.liquids[-1][0] or Liquid.WATER, + liquid_class=Liquid.WATER, tip_type=op.tip.tip_type, ) if isinstance(op.tip, TecanTip) @@ -453,7 +453,7 @@ async def dispense(self, ops: List[SingleChannelDispense], use_channels: List[in tecan_liquid_classes = [ get_liquid_class( target_volume=op.volume, - liquid_class=op.liquids[-1][0] or Liquid.WATER, + liquid_class=Liquid.WATER, tip_type=op.tip.tip_type, ) if isinstance(op.tip, TecanTip) diff --git a/pylabrobot/liquid_handling/backends/tecan/EVO_tests.py b/pylabrobot/liquid_handling/backends/tecan/EVO_tests.py index 691a8af0ef3..fa7d4817739 100644 --- a/pylabrobot/liquid_handling/backends/tecan/EVO_tests.py +++ b/pylabrobot/liquid_handling/backends/tecan/EVO_tests.py @@ -143,7 +143,6 @@ async def test_aspirate(self): flow_rate=100, liquid_height=10, blow_out_air_volume=0, - liquids=[(None, 100)], mix=None, ) await self.evo.aspirate([op], use_channels=[0]) @@ -285,7 +284,6 @@ async def test_dispense(self): flow_rate=100, liquid_height=10, blow_out_air_volume=0, - liquids=[(None, 100)], mix=None, ) await self.evo.dispense([op], use_channels=[0]) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 5269f6c19c4..8a4a5e00074 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -53,14 +53,11 @@ TipSpot, TipTracker, Trash, - VolumeTracker, Well, - does_cross_contamination_tracking, does_tip_tracking, does_volume_tracking, ) -from pylabrobot.resources.errors import CrossContaminationError, HasTipError -from pylabrobot.resources.liquid import Liquid +from pylabrobot.resources.errors import HasTipError from pylabrobot.resources.rotation import Rotation from pylabrobot.serializer import deserialize, serialize from pylabrobot.tilting.tilter import Tilter @@ -93,21 +90,6 @@ ] -def check_contaminated(liquid_history_tip, liquid_history_well): - """Helper function used to check if adding a liquid to the container - would result in cross contamination""" - return not liquid_history_tip.issubset(liquid_history_well) and len(liquid_history_tip) > 0 - - -def check_updatable(src_tracker: VolumeTracker, dest_tracker: VolumeTracker): - """Helper function used to check if it is possible to update the - liquid_history of src based on contents of dst""" - return ( - not src_tracker.is_cross_contamination_tracking_disabled - and not dest_tracker.is_cross_contamination_tracking_disabled - ) - - class BlowOutVolumeError(Exception): pass @@ -934,14 +916,6 @@ async def aspirate( # add user defined offsets to the computed centers offsets = [c + o for c, o in zip(center_offsets, offsets)] - # liquid(s) for each channel. If volume tracking is disabled, use None as the liquid. - liquids: List[List[Tuple[Optional[Liquid], float]]] = [] - for r, vol in zip(resources, vols): - if r.tracker.is_disabled or not does_volume_tracking(): - liquids.append([(None, vol)]) - else: - liquids.append(r.tracker.get_liquids(top_volume=vol)) - # create operations aspirations = [ SingleChannelAspiration( @@ -952,10 +926,9 @@ async def aspirate( liquid_height=lh, tip=t, blow_out_air_volume=bav, - liquids=lvs, mix=m, ) - for r, v, o, fr, lh, t, bav, lvs, m in zip( + for r, v, o, fr, lh, t, bav, m in zip( resources, vols, offsets, @@ -963,7 +936,6 @@ async def aspirate( liquid_height, tips, blow_out_air_volume, - liquids, mix or [None] * len(use_channels), # type: ignore ) ] @@ -973,20 +945,7 @@ async def aspirate( if does_volume_tracking(): if not op.resource.tracker.is_disabled: op.resource.tracker.remove_liquid(op.volume) - - # Cross contamination check - if does_cross_contamination_tracking(): - if check_contaminated( - op.tip.tracker.liquid_history, - op.resource.tracker.liquid_history, - ): - raise CrossContaminationError( - f"Attempting to aspirate {next(reversed(op.liquids))[0]} with a tip contaminated " - f"with {op.tip.tracker.liquid_history}." - ) - - for liquid, volume in reversed(op.liquids): - op.tip.tracker.add_liquid(liquid=liquid, volume=volume) + op.tip.tracker.add_liquid(volume=op.volume) extras = self._check_args( self.backend.aspirate, @@ -1168,13 +1127,6 @@ async def dispense( f"Length of {n} must match length of use_channels: {len(p)} != {len(use_channels)}" ) - # liquid(s) for each channel. If volume tracking is disabled, use None as the liquid. - if does_volume_tracking(): - channels = [self.head[channel] for channel in use_channels] - liquids = [c.get_tip().tracker.get_liquids(top_volume=vol) for c, vol in zip(channels, vols)] - else: - liquids = [[(None, vol)] for vol in vols] - # create operations dispenses = [ SingleChannelDispense( @@ -1184,11 +1136,10 @@ async def dispense( flow_rate=fr, liquid_height=lh, tip=t, - liquids=lvs, blow_out_air_volume=bav, mix=m, ) - for r, v, o, fr, lh, t, bav, lvs, m in zip( + for r, v, o, fr, lh, t, bav, m in zip( resources, vols, offsets, @@ -1196,7 +1147,6 @@ async def dispense( liquid_height, tips, blow_out_air_volume, - liquids, mix or [None] * len(use_channels), # type: ignore ) ] @@ -1205,12 +1155,7 @@ async def dispense( for op in dispenses: if does_volume_tracking(): if not op.resource.tracker.is_disabled: - # Update the liquid history of the tip to reflect new liquid - if check_updatable(op.tip.tracker, op.resource.tracker): - op.tip.tracker.liquid_history.update(op.resource.tracker.liquid_history) - - for liquid, volume in op.liquids: - op.resource.tracker.add_liquid(liquid=liquid, volume=volume) + op.resource.tracker.add_liquid(volume=op.volume) op.tip.tracker.remove_liquid(op.volume) # fix the backend kwargs @@ -1717,7 +1662,6 @@ async def aspirate96( del backend_kwargs[extra] tips = [channel.get_tip() if channel.has_tip else None for channel in self.head96.values()] - all_liquids: List[List[Tuple[Optional[Liquid], float]]] = [] aspiration: Union[MultiHeadAspirationPlate, MultiHeadAspirationContainer] # Convert everything to floats to handle exotic number types @@ -1745,18 +1689,9 @@ async def aspirate96( if tip is None: continue - # superfluous to have append in two places but the type checker is very angry and does not - # understand that Optional[Liquid] (remove_liquid) is the same as None from the first case - liquids: List[Tuple[Optional[Liquid], float]] - if container.tracker.is_disabled or not does_volume_tracking(): - liquids = [(None, volume)] - all_liquids.append(liquids) - else: - liquids = container.tracker.remove_liquid(volume=volume) # type: ignore - all_liquids.append(liquids) - - for liquid, vol in reversed(liquids): - tip.tracker.add_liquid(liquid=liquid, volume=vol) + if not container.tracker.is_disabled and does_volume_tracking(): + container.tracker.remove_liquid(volume=volume) + tip.tracker.add_liquid(volume=volume) aspiration = MultiHeadAspirationContainer( container=container, @@ -1766,7 +1701,6 @@ async def aspirate96( tips=tips, liquid_height=liquid_height, blow_out_air_volume=blow_out_air_volume, - liquids=cast(List[List[Tuple[Optional[Liquid], float]]], all_liquids), # stupid mix=mix, ) else: # multiple containers @@ -1783,18 +1717,9 @@ async def aspirate96( if tip is None: continue - # superfluous to have append in two places but the type checker is very angry and does not - # understand that Optional[Liquid] (remove_liquid) is the same as None from the first case - if well.tracker.is_disabled or not does_volume_tracking(): - liquids = [(None, volume)] - all_liquids.append(liquids) - else: - # tracker is enabled: update tracker liquid history - liquids = well.tracker.remove_liquid(volume=volume) # type: ignore - all_liquids.append(liquids) - - for liquid, vol in reversed(liquids): - tip.tracker.add_liquid(liquid=liquid, volume=vol) + if not well.tracker.is_disabled and does_volume_tracking(): + well.tracker.remove_liquid(volume=volume) + tip.tracker.add_liquid(volume=volume) aspiration = MultiHeadAspirationPlate( wells=cast(List[Well], containers), @@ -1804,7 +1729,6 @@ async def aspirate96( tips=tips, liquid_height=liquid_height, blow_out_air_volume=blow_out_air_volume, - liquids=cast(List[List[Tuple[Optional[Liquid], float]]], all_liquids), # stupid mix=mix, ) @@ -1881,7 +1805,6 @@ async def dispense96( del backend_kwargs[extra] tips = [channel.get_tip() if channel.has_tip else None for channel in self.head96.values()] - all_liquids: List[List[Tuple[Optional[Liquid], float]]] = [] dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer] # Convert everything to floats to handle exotic number types @@ -1909,13 +1832,10 @@ async def dispense96( if tip is None: continue - liquids = tip.tracker.remove_liquid(volume=volume) - reversed_liquids = list(reversed(liquids)) - all_liquids.append(reversed_liquids) + tip.tracker.remove_liquid(volume=volume) if not container.tracker.is_disabled and does_volume_tracking(): - for liquid, vol in reversed(reversed_liquids): - container.tracker.add_liquid(liquid=liquid, volume=vol) + container.tracker.add_liquid(volume=volume) dispense = MultiHeadDispenseContainer( container=container, @@ -1925,7 +1845,6 @@ async def dispense96( tips=tips, liquid_height=liquid_height, blow_out_air_volume=blow_out_air_volume, - liquids=cast(List[List[Tuple[Optional[Liquid], float]]], all_liquids), # stupid mix=mix, ) else: @@ -1942,18 +1861,11 @@ async def dispense96( if tip is None: continue - # even if the volume tracker is disabled, a liquid (None, volume) is added to the list - # during the aspiration command - if tip.tracker.is_disabled or not does_volume_tracking(): - liquids = [(None, volume)] - else: - liquids = tip.tracker.remove_liquid(volume=volume) - reversed_liquids = list(reversed(liquids)) - all_liquids.append(reversed_liquids) + if not tip.tracker.is_disabled and does_volume_tracking(): + tip.tracker.remove_liquid(volume=volume) if not well.tracker.is_disabled and does_volume_tracking(): - for liquid, vol in reversed_liquids: - well.tracker.add_liquid(liquid=liquid, volume=vol) + well.tracker.add_liquid(volume=volume) dispense = MultiHeadDispensePlate( wells=cast(List[Well], containers), @@ -1963,7 +1875,6 @@ async def dispense96( tips=tips, liquid_height=liquid_height, blow_out_air_volume=blow_out_air_volume, - liquids=all_liquids, mix=mix, ) diff --git a/pylabrobot/liquid_handling/liquid_handler_tests.py b/pylabrobot/liquid_handling/liquid_handler_tests.py index d824d82b971..59231737865 100644 --- a/pylabrobot/liquid_handling/liquid_handler_tests.py +++ b/pylabrobot/liquid_handling/liquid_handler_tests.py @@ -20,7 +20,6 @@ Cor_96_wellplate_360ul_Fb, Deck, Lid, - Liquid, Plate, ResourceNotFoundError, ResourceStack, @@ -31,7 +30,6 @@ ) from pylabrobot.resources.carrier import PlateHolder from pylabrobot.resources.errors import ( - CrossContaminationError, HasTipError, NoTipError, ) @@ -42,7 +40,6 @@ ) from pylabrobot.resources.utils import create_ordered_items_2d from pylabrobot.resources.volume_tracker import ( - set_cross_contamination_tracking, set_volume_tracking, ) from pylabrobot.resources.well import Well @@ -76,7 +73,6 @@ def _make_asp( flow_rate=None, liquid_height=None, blow_out_air_volume=None, - liquids=[(None, vol)], mix=None, ) @@ -95,7 +91,6 @@ def _make_disp( flow_rate=None, liquid_height=None, blow_out_air_volume=None, - liquids=[(None, vol)], mix=None, ) @@ -575,7 +570,7 @@ async def test_with_use_channels(self): async def test_offsets_asp_disp(self): well = self.plate.get_item("A1") - well.tracker.set_liquids([(None, 10)]) + self.plate.get_item("A1").tracker.set_volume(10) t = self.tip_rack.get_item("A1").get_tip() self.lh.update_head_state({0: t}) await self.lh.aspirate([well], vols=[10], offsets=[Coordinate(x=1, y=1, z=1)]) @@ -661,7 +656,6 @@ async def test_aspirate_dispense96(self): flow_rate=None, liquid_height=None, blow_out_air_volume=None, - liquids=[[(None, 10)] for _ in range(96)], mix=None, ) ) @@ -671,7 +665,7 @@ async def test_transfer(self): self.lh.update_head_state({0: t}) # Simple transfer - self.plate.get_item("A1").tracker.set_liquids([(None, 10)]) + self.plate.get_item("A1").tracker.set_volume(10) await self.lh.transfer(self.plate.get_well("A1"), self.plate["A2"], source_vol=10) self.assertEqual( @@ -699,7 +693,7 @@ async def test_transfer(self): self.backend.clear() # Transfer to multiple wells - self.plate.get_item("A1").tracker.set_liquids([(None, 80)]) + self.plate.get_item("A1").tracker.set_volume(80) await self.lh.transfer(self.plate.get_well("A1"), self.plate["A1:H1"], source_vol=80) self.assertEqual( self.get_first_command("aspirate"), @@ -736,7 +730,7 @@ async def test_transfer(self): self.backend.clear() # Transfer with ratios - self.plate.get_item("A1").tracker.set_liquids([(None, 60)]) + self.plate.get_item("A1").tracker.set_volume(60) await self.lh.transfer( self.plate.get_well("A1"), self.plate["B1:C1"], @@ -778,7 +772,7 @@ async def test_transfer(self): # Transfer with target_vols vols: List[float] = [3, 1, 4, 1, 5, 9, 6, 2] - self.plate.get_item("A1").tracker.set_liquids([(None, sum(vols))]) + self.plate.get_item("A1").tracker.set_volume(sum(vols)) await self.lh.transfer(self.plate.get_well("A1"), self.plate["A1:H1"], target_vols=vols) self.assertEqual( self.get_first_command("aspirate"), @@ -851,7 +845,6 @@ async def test_stamp(self): flow_rate=None, liquid_height=None, blow_out_air_volume=None, - liquids=[[(None, 10)]] * 96, mix=None, ) }, @@ -871,7 +864,6 @@ async def test_stamp(self): flow_rate=None, liquid_height=None, blow_out_air_volume=None, - liquids=[[(None, 10)]] * 96, mix=None, ) }, @@ -992,7 +984,6 @@ async def test_aspirate_with_lid(self): ) self.plate.assign_child_resource(lid) well = self.plate.get_item("A1") - well.tracker.set_liquids([(None, 10)]) t = self.tip_rack.get_item("A1").get_tip() self.lh.update_head_state({0: t}) with self.assertRaises(ValueError): @@ -1064,7 +1055,7 @@ async def test_save_state(self): set_volume_tracking(enabled=True) # set and save the state - self.plate.get_item("A2").tracker.set_liquids([(None, 10)]) + self.plate.get_item("A2").tracker.set_volume(10) state_filename = tempfile.mktemp() self.lh.deck.save_state_to_file(fn=state_filename) @@ -1079,9 +1070,9 @@ async def test_save_state(self): # assert that the state is the same well_a1 = lh2.deck.get_resource("plate").get_item("A1") # type: ignore - self.assertEqual(well_a1.tracker.liquids, []) + self.assertEqual(well_a1.tracker.volume, 0) well_a2 = lh2.deck.get_resource("plate").get_item("A2") # type: ignore - self.assertEqual(well_a2.tracker.liquids, [(None, 10)]) + self.assertEqual(well_a2.tracker.volume, 10) set_volume_tracking(enabled=False) @@ -1122,25 +1113,22 @@ async def asyncTearDown(self): async def test_dispense_with_volume_tracking(self): well = self.plate.get_item("A1") await self.lh.pick_up_tips(self.tip_rack["A1"]) - well.tracker.set_liquids([(None, 10)]) + well.tracker.set_volume(10) await self.lh.aspirate([well], vols=[10]) await self.lh.dispense([well], vols=[10]) - self.assertEqual(well.tracker.liquids, [(None, 10)]) + self.assertEqual(well.tracker.volume, 10) async def test_mix_volume_tracking(self): for i in range(8): - self.plate.get_item(i).set_liquids([(Liquid.SERUM, 55)]) + self.plate.get_item(i).set_volume(55) await self.lh.pick_up_tips(self.tip_rack[0:8]) - initial_liquids = [self.plate.get_item(i).tracker.liquids for i in range(8)] for _ in range(10): await self.lh.aspirate(self.plate[0:8], vols=[45] * 8) await self.lh.dispense(self.plate[0:8], vols=[45] * 8) - liquids_now = [self.plate.get_item(i).tracker.liquids for i in range(8)] - self.assertEqual(liquids_now, initial_liquids) async def test_channel_1_liquid_tracking(self): - self.plate.get_item("A1").tracker.set_liquids([(Liquid.WATER, 10)]) + self.plate.get_item("A1").tracker.set_volume(10) with self.lh.use_channels([1]): await self.lh.pick_up_tips(self.tip_rack["A1"]) await self.lh.aspirate([self.plate.get_item("A1")], vols=[10]) @@ -1154,7 +1142,7 @@ async def error_func(*args, **kwargs): raise ChannelizedError(errors={0: Exception("This is an error")}) self.backend.dispense = error_func # type: ignore - well.tracker.set_liquids([(None, 200)]) + well.tracker.set_volume(200) await self.lh.aspirate([well], vols=[200]) assert self.lh.head[0].get_tip().tracker.get_used_volume() == 200 @@ -1165,7 +1153,7 @@ async def error_func(*args, **kwargs): async def test_96_head_volume_tracking_multi_container(self): for item in self.plate.get_all_items(): - item.tracker.set_liquids([(Liquid.WATER, 10)]) + item.tracker.set_volume(10) await self.lh.pick_up_tips96(self.tip_rack) await self.lh.aspirate96(self.plate, volume=10) for i in range(96): @@ -1179,76 +1167,17 @@ async def test_96_head_volume_tracking_multi_container(self): async def test_96_head_volume_tracking_single_container(self): well = self.single_well_plate.get_item(0) - well.tracker.set_liquids([(Liquid.WATER, 10 * 96)]) + well.tracker.set_volume(10 * 96) await self.lh.pick_up_tips96(self.tip_rack) await self.lh.aspirate96(self.single_well_plate, volume=10) assert all(self.lh.head96[i].get_tip().tracker.get_used_volume() == 10 for i in range(96)) - assert all( - self.lh.head96[i].get_tip().tracker.liquids == [(Liquid.WATER, 10)] for i in range(96) - ) + assert all(self.lh.head96[i].get_tip().tracker.volume == 10 for i in range(96)) assert well.tracker.get_used_volume() == 0 await self.lh.dispense96(self.single_well_plate, volume=10) assert all(self.lh.head96[i].get_tip().tracker.get_used_volume() == 0 for i in range(96)) - assert all(self.lh.head96[i].get_tip().tracker.liquids == [] for i in range(96)) + assert all(self.lh.head96[i].get_tip().tracker.volume == 0 for i in range(96)) assert well.tracker.get_used_volume() == 10 * 96 await self.lh.return_tips96() - - -class TestLiquidHandlerCrossContaminationTracking(unittest.IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.backend = backends.SaverBackend(num_channels=8) - self.deck = STARLetDeck() - self.lh = LiquidHandler(backend=self.backend, deck=self.deck) - self.tip_rack = hamilton_96_tiprack_300uL_filter(name="tip_rack") - self.plate = Cor_96_wellplate_360ul_Fb(name="plate") - self.deck.assign_child_resource(self.tip_rack, location=Coordinate(0, 0, 0)) - self.deck.assign_child_resource(self.plate, location=Coordinate(100, 100, 0)) - await self.lh.setup() - set_volume_tracking(enabled=True) - set_cross_contamination_tracking(enabled=True) - - async def asyncTearDown(self): - set_volume_tracking(enabled=False) - set_cross_contamination_tracking(enabled=False) - - async def test_aspirate_with_contaminated_tip(self): - blood_well = self.plate.get_item("A1") - etoh_well = self.plate.get_item("A2") - dest_well = self.plate.get_item("A3") - await self.lh.pick_up_tips(self.tip_rack["A1"]) - blood_well.tracker.set_liquids([(Liquid.BLOOD, 10)]) - etoh_well.tracker.set_liquids([(Liquid.ETHANOL, 10)]) - await self.lh.aspirate([blood_well], vols=[10]) - await self.lh.dispense([dest_well], vols=[10]) - with self.assertRaises(CrossContaminationError): - await self.lh.aspirate([etoh_well], vols=[10]) - - async def test_aspirate_from_same_well_twice(self): - src_well = self.plate.get_item("A1") - dst_well = self.plate.get_item("A2") - await self.lh.pick_up_tips(self.tip_rack["A1"]) - src_well.tracker.set_liquids([(Liquid.BLOOD, 20)]) - await self.lh.aspirate([src_well], vols=[10]) - await self.lh.dispense([dst_well], vols=[10]) - self.assertEqual(dst_well.tracker.liquids, [(Liquid.BLOOD, 10)]) - await self.lh.aspirate([src_well], vols=[10]) - await self.lh.dispense([dst_well], vols=[10]) - self.assertEqual(dst_well.tracker.liquids, [(Liquid.BLOOD, 20)]) - - async def test_aspirate_from_well_with_partial_overlap(self): - pure_blood_well = self.plate.get_item("A1") - mix_well = self.plate.get_item("A2") - await self.lh.pick_up_tips(self.tip_rack["A1"]) - pure_blood_well.tracker.set_liquids([(Liquid.BLOOD, 20)]) - mix_well.tracker.set_liquids([(Liquid.ETHANOL, 20)]) - await self.lh.aspirate([pure_blood_well], vols=[10]) - await self.lh.dispense([mix_well], vols=[10]) - self.assertEqual( - mix_well.tracker.liquids, - [(Liquid.ETHANOL, 20), (Liquid.BLOOD, 10)], - ) # order matters - with self.assertRaises(CrossContaminationError): - await self.lh.aspirate([pure_blood_well], vols=[10]) diff --git a/pylabrobot/liquid_handling/standard.py b/pylabrobot/liquid_handling/standard.py index 419502d2cae..57f1100b2b1 100644 --- a/pylabrobot/liquid_handling/standard.py +++ b/pylabrobot/liquid_handling/standard.py @@ -4,10 +4,9 @@ import enum from dataclasses import dataclass -from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, List, Optional, Sequence, Union from pylabrobot.resources.coordinate import Coordinate -from pylabrobot.resources.liquid import Liquid from pylabrobot.resources.rotation import Rotation if TYPE_CHECKING: @@ -58,7 +57,6 @@ class SingleChannelAspiration: flow_rate: Optional[float] liquid_height: Optional[float] blow_out_air_volume: Optional[float] - liquids: List[Tuple[Optional[Liquid], float]] mix: Optional[Mix] @@ -71,7 +69,6 @@ class SingleChannelDispense: flow_rate: Optional[float] liquid_height: Optional[float] blow_out_air_volume: Optional[float] - liquids: List[Tuple[Optional[Liquid], float]] mix: Optional[Mix] @@ -91,7 +88,6 @@ class MultiHeadAspirationPlate: flow_rate: Optional[float] liquid_height: Optional[float] blow_out_air_volume: Optional[float] - liquids: List[List[Tuple[Optional[Liquid], float]]] mix: Optional[Mix] @@ -104,7 +100,6 @@ class MultiHeadDispensePlate: flow_rate: Optional[float] liquid_height: Optional[float] blow_out_air_volume: Optional[float] - liquids: List[List[Tuple[Optional[Liquid], float]]] mix: Optional[Mix] @@ -117,7 +112,6 @@ class MultiHeadAspirationContainer: flow_rate: Optional[float] liquid_height: Optional[float] blow_out_air_volume: Optional[float] - liquids: List[List[Tuple[Optional[Liquid], float]]] mix: Optional[Mix] @@ -130,7 +124,6 @@ class MultiHeadDispenseContainer: flow_rate: Optional[float] liquid_height: Optional[float] blow_out_air_volume: Optional[float] - liquids: List[List[Tuple[Optional[Liquid], float]]] mix: Optional[Mix] diff --git a/pylabrobot/resources/__init__.py b/pylabrobot/resources/__init__.py index a824b0c220d..a1df867073e 100644 --- a/pylabrobot/resources/__init__.py +++ b/pylabrobot/resources/__init__.py @@ -59,11 +59,8 @@ ) from .volume_tracker import ( VolumeTracker, - does_cross_contamination_tracking, does_volume_tracking, - no_cross_contamination_tracking, no_volume_tracking, - set_cross_contamination_tracking, set_volume_tracking, ) from .vwr import * diff --git a/pylabrobot/resources/errors.py b/pylabrobot/resources/errors.py index 0b72c7d5f11..9d39b4b97b1 100644 --- a/pylabrobot/resources/errors.py +++ b/pylabrobot/resources/errors.py @@ -1,3 +1,6 @@ +import warnings + + class ResourceNotFoundError(Exception): pass @@ -22,7 +25,13 @@ class CrossContaminationError(Exception): """Raised when attempting to aspirate from a well with a tip that has touched a different liquid.""" - + def __init__(self, *args, **kwargs): + warnings.warn( + "Cross contamination tracking is deprecated and will be removed in a future version. ", + DeprecationWarning, + stacklevel=2 + ) + super().__init__(*args, **kwargs) class ResourceDefinitionIncompleteError(Exception): """Raised when trying to access a resource that has not been defined or is not complete. diff --git a/pylabrobot/resources/plate.py b/pylabrobot/resources/plate.py index 15535ae2eff..f7c85799c66 100644 --- a/pylabrobot/resources/plate.py +++ b/pylabrobot/resources/plate.py @@ -13,10 +13,10 @@ cast, ) +from pylabrobot.resources.liquid import Liquid from pylabrobot.resources.resource_holder import get_child_location from .itemized_resource import ItemizedResource -from .liquid import Liquid from .resource import Coordinate, Resource if TYPE_CHECKING: @@ -168,31 +168,33 @@ def get_wells(self, identifier: Union[str, Sequence[int]]) -> List["Well"]: def has_lid(self) -> bool: return self.lid is not None - def set_well_liquids( + def set_well_volumes( self, - liquids: Union[ - List[List[Tuple[Optional["Liquid"], Union[int, float]]]], - List[Tuple[Optional["Liquid"], Union[int, float]]], - Tuple[Optional["Liquid"], Union[int, float]], - ], + volumes: List[float], ) -> None: - """Update the liquid in the volume tracker for each well in the plate. + """Fill all wells in the plate with a given volume. Args: - liquids: A list of liquids, one for each well in the plate. The list can be a list of lists, - where each inner list contains the liquids for each well in a column. If a single tuple is - given, the volume is assumed to be the same for all wells. Liquids are in uL. - - Raises: - ValueError: If the number of liquids does not match the number of wells in the plate. + volume: The volume to fill each well with, in uL. + """ - Example: - Set the volume of each well in a 96-well plate to 10 uL. + if not len(volumes) == self.num_items: + raise ValueError( + f"Length of volumes ({len(volumes)}) does not match number of wells ({self.num_items})." + ) - >>> plate = Plate("plate", 127.76, 85.48, 14.5, num_items_x=12, num_items_y=8) - >>> plate.set_well_liquids((Liquid.WATER, 10)) - """ + for well, volume in zip(self.get_all_items(), volumes): + well.set_volume(volume) + def set_well_liquids( + self, + liquids: Union[ + List[List[Tuple[Optional["Liquid"], Union[int, float]]]], + List[Tuple[Optional["Liquid"], Union[int, float]]], + Tuple[Optional["Liquid"], Union[int, float]], + ], + ): + """Deprecated: Use `set_well_volumes` instead.""" if isinstance(liquids, tuple): liquids = [liquids] * self.num_items elif isinstance(liquids, list) and all(isinstance(column, list) for column in liquids): @@ -201,15 +203,7 @@ def set_well_liquids( liquids = [list(column) for column in zip(*liquids)] # transpose the list of lists liquids = [volume for column in liquids for volume in column] # flatten the list of lists - if len(liquids) != self.num_items: - raise ValueError( - f"Number of liquids ({len(liquids)}) does not match number of wells " - f"({self.num_items}) in plate '{self.name}'." - ) - - for i, (liquid, volume) in enumerate(liquids): - well = self.get_well(i) - well.tracker.set_liquids([(liquid, volume)]) # type: ignore + self.set_well_volumes([volume for _, volume in liquids]) # type: ignore def disable_volume_trackers(self) -> None: """Disable volume tracking for all wells in the plate.""" diff --git a/pylabrobot/resources/tip_rack.py b/pylabrobot/resources/tip_rack.py index 2e3abbc3e87..9d638926253 100644 --- a/pylabrobot/resources/tip_rack.py +++ b/pylabrobot/resources/tip_rack.py @@ -6,10 +6,7 @@ from pylabrobot.resources.coordinate import Coordinate from pylabrobot.resources.tip import Tip, TipCreator -from pylabrobot.resources.tip_tracker import ( - TipTracker, - does_tip_tracking, -) +from pylabrobot.resources.tip_tracker import TipTracker, does_tip_tracking from pylabrobot.serializer import deserialize from .itemized_resource import ItemizedResource diff --git a/pylabrobot/resources/tube.py b/pylabrobot/resources/tube.py index 6f5fddbe578..4f11bca2d45 100644 --- a/pylabrobot/resources/tube.py +++ b/pylabrobot/resources/tube.py @@ -1,3 +1,4 @@ +import warnings from typing import Callable, List, Optional, Tuple from pylabrobot.resources.container import Container @@ -53,15 +54,29 @@ def __init__( def serialize(self) -> dict: return {**super().serialize(), "max_volume": self.max_volume} - def set_liquids(self, liquids: List[Tuple[Optional["Liquid"], float]]): - """Set the liquids in the tube. + def set_volume(self, volume: float): + """Set the volume in the tube. - (wraps :meth:`~.VolumeTracker.set_liquids`) + (wraps :meth:`~.VolumeTracker.set_volume`) Example: - Set the liquids in a tube to 10 uL of water: + Set the volume in a tube to 10 uL: + + >>> tube.set_volume(10) + """ + + self.tracker.set_volume(volume) - >>> tube.set_liquids([(Liquid.WATER, 10)]) + def set_liquids(self, liquids: List[Tuple[Optional["Liquid"], float]]): + """Set the liquids in the tube. + + Deprecated: use `set_volume` instead. This method will be removed in a future version. """ + warnings.warn( + "`set_liquids` is deprecated and will be removed in a future version. Use `set_volume` instead.", + DeprecationWarning, + stacklevel=2 + ) + self.tracker.set_liquids(liquids) diff --git a/pylabrobot/resources/volume_tracker.py b/pylabrobot/resources/volume_tracker.py index 113a1ae3cbc..d722274e905 100644 --- a/pylabrobot/resources/volume_tracker.py +++ b/pylabrobot/resources/volume_tracker.py @@ -1,24 +1,20 @@ import contextlib -import copy import sys -from typing import Callable, List, Optional, Tuple, cast +import warnings +from typing import Callable, List, Optional, Tuple from pylabrobot.resources.errors import ( TooLittleLiquidError, TooLittleVolumeError, ) from pylabrobot.resources.liquid import Liquid -from pylabrobot.serializer import deserialize, serialize this = sys.modules[__name__] this.volume_tracking_enabled = False # type: ignore -this.cross_contamination_tracking_enabled = False # type: ignore def set_volume_tracking(enabled: bool): this.volume_tracking_enabled = enabled # type: ignore - if not enabled: - this.cross_contamination_tracking_enabled = False # type: ignore def does_volume_tracking() -> bool: @@ -27,35 +23,10 @@ def does_volume_tracking() -> bool: @contextlib.contextmanager def no_volume_tracking(): - vt, ct = ( - this.volume_tracking_enabled, - this.cross_contamination_tracking_enabled, - ) # type: ignore + old_value = this.volume_tracking_enabled this.volume_tracking_enabled = False # type: ignore - this.cross_contamination_tracking_enabled = False # type: ignore yield - this.volume_tracking_enabled = vt # type: ignore - this.cross_contamination_tracking_enabled = ct # type: ignore - - -def set_cross_contamination_tracking(enabled: bool): - if enabled: - assert ( - this.volume_tracking_enabled - ), "Cross contamination tracking only possible if volume tracking active." - this.cross_contamination_tracking_enabled = enabled # type: ignore - - -def does_cross_contamination_tracking() -> bool: - return this.cross_contamination_tracking_enabled # type: ignore - - -@contextlib.contextmanager -def no_cross_contamination_tracking(): - old_value = this.cross_contamination_tracking_enabled - this.cross_contamination_tracking_enabled = False # type: ignore - yield - this.cross_contamination_tracking_enabled = old_value # type: ignore + this.volume_tracking_enabled = old_value # type: ignore VolumeTrackerCallback = Callable[[], None] @@ -69,19 +40,13 @@ def __init__( self, thing: str, max_volume: float, - liquids: Optional[List[Tuple[Optional[Liquid], float]]] = None, - pending_liquids: Optional[List[Tuple[Optional[Liquid], float]]] = None, - liquid_history: Optional[set] = None, + initial_volume: Optional[float] = None, ) -> None: self._is_disabled = False - self._is_cross_contamination_tracking_disabled = False - self.thing = thing self.max_volume = max_volume - self.liquids: List[Tuple[Optional[Liquid], float]] = liquids or [] - self.pending_liquids: List[Tuple[Optional[Liquid], float]] = pending_liquids or [] - - self.liquid_history = {liquid for liquid in (liquid_history or set()) if liquid is not None} + self.volume = initial_volume or 0 + self.pending_volume = initial_volume or 0 self._callback: Optional[VolumeTrackerCallback] = None @@ -89,126 +54,92 @@ def __init__( def is_disabled(self) -> bool: return self._is_disabled - @property - def is_cross_contamination_tracking_disabled(self) -> bool: - return self._is_cross_contamination_tracking_disabled - def disable(self) -> None: """Disable the volume tracker.""" self._is_disabled = True - def disable_cross_contamination_tracking(self) -> None: - """Disable the cross contamination tracker.""" - self._is_cross_contamination_tracking_disabled = True - def enable(self) -> None: """Enable the volume tracker.""" self._is_disabled = False - def enable_cross_contamination_tracking(self) -> None: - """Enable the cross contamination tracker.""" - self._is_cross_contamination_tracking_disabled = False - - def set_liquids(self, liquids: List[Tuple[Optional["Liquid"], float]]) -> None: - """Set the liquids in the container.""" - self.liquids = liquids - self.pending_liquids = liquids - - if not self.is_cross_contamination_tracking_disabled: - self.liquid_history.update([liquid[0] for liquid in liquids]) + def set_volume(self, volume: float) -> None: + """Set the volume in the container.""" + self.volume = volume + self.pending_volume = volume if self._callback is not None: self._callback() - def remove_liquid(self, volume: float) -> List[Tuple[Optional["Liquid"], float]]: - """Remove liquid from the container. Top to bottom.""" - - available_volume = self.get_used_volume() - if (volume - available_volume) > 1e-6: + def set_liquids(self, liquids: List[Tuple[Optional["Liquid"], float]]) -> None: + """Set the liquids in the container. + + Deprecated: + Use `set_volume` instead. This method will be removed in a future version. + """ + warnings.warn( + "`set_liquids` is deprecated and will be removed in a future version. " + "Use `set_volume` instead.", + DeprecationWarning, + stacklevel=2, + ) + self.set_volume(sum(volume for _, volume in liquids)) + + def remove_liquid(self, volume: float) -> None: + """Remove liquid from the container.""" + + if (volume - self.get_used_volume()) > 1e-6: raise TooLittleLiquidError( - f"Container {self.thing} has too little liquid: {volume}uL > {available_volume}uL." + f"Not enough liquid in container: {volume}uL > {self.get_used_volume()}uL." ) - removed_liquids = [] - removed_volume = 0.0 - while abs(removed_volume - volume) > 1e-6 and removed_volume < volume: - liquid, liquid_volume = self.pending_liquids.pop() - removed_volume += liquid_volume - - # If we have more liquid than we need, put the excess back. - if removed_volume > volume: - self.pending_liquids.append((liquid, removed_volume - volume)) - removed_liquids.append((liquid, liquid_volume - (removed_volume - volume))) - else: - removed_liquids.append((liquid, liquid_volume)) + self.pending_volume -= volume if self._callback is not None: self._callback() - return removed_liquids - - def add_liquid(self, liquid: Optional["Liquid"], volume: float) -> None: + def add_liquid(self, volume: float) -> None: """Add liquid to the container.""" if (volume - self.get_free_volume()) > 1e-6: raise TooLittleVolumeError( - f"Container {self.thing} has too little volume: {volume}uL > {self.get_free_volume()}uL." + f"Not enough space in container: {volume}uL > {self.get_free_volume()}uL." ) - # Update the liquid history tracker if needed - if not self.is_cross_contamination_tracking_disabled: - if liquid is not None: - self.liquid_history.add(liquid) - - # If the last liquid is the same as the one we want to add, just add the volume to it. - if len(self.pending_liquids) > 0: - last_pending_liquid_tuple = self.pending_liquids[-1] - if last_pending_liquid_tuple[0] == liquid: - self.pending_liquids[-1] = ( - liquid, - last_pending_liquid_tuple[1] + volume, - ) - else: - self.pending_liquids.append((liquid, volume)) - else: - self.pending_liquids.append((liquid, volume)) + self.pending_volume += volume if self._callback is not None: self._callback() def get_used_volume(self) -> float: """Get the used volume of the container. Note that this includes pending operations.""" - return sum(volume for _, volume in self.pending_liquids) + return self.pending_volume def get_free_volume(self) -> float: """Get the free volume of the container. Note that this includes pending operations.""" - return self.max_volume - self.get_used_volume() def get_liquids(self, top_volume: float) -> List[Tuple[Optional[Liquid], float]]: - """Get the liquids in the top `top_volume` uL""" - + """Get the liquids in the top `top_volume` uL. + + Deprecated: + This method is deprecated and will be removed in a future version. + The volume tracker no longer tracks individual liquids. + """ + warnings.warn( + "`get_liquids` is deprecated and will be removed in a future version. " + "The volume tracker no longer tracks individual liquids.", + DeprecationWarning, + stacklevel=2, + ) if (top_volume - self.get_used_volume()) > 1e-6: raise TooLittleLiquidError(f"Tracker only has {self.get_used_volume()}uL") - liquids = [] - for liquid, volume in reversed(self.liquids): - if top_volume == 0: - break - - if volume > top_volume: - liquids.append((liquid, top_volume)) - break - - top_volume -= volume - liquids.append((liquid, volume)) - return liquids + return [(None, top_volume)] def commit(self) -> None: """Commit the pending operations.""" assert not self.is_disabled, f"Volume tracker {self.thing} is disabled. Call `enable()`." - - self.liquids = copy.deepcopy(self.pending_liquids) + self.volume = self.pending_volume if self._callback is not None: self._callback() @@ -216,38 +147,23 @@ def commit(self) -> None: def rollback(self) -> None: """Rollback the pending operations.""" assert not self.is_disabled, "Volume tracker is disabled. Call `enable()`." - self.pending_liquids = copy.deepcopy(self.liquids) - - def clear_cross_contamination_history(self) -> None: - """Resets the liquid_history for cross contamination tracking. Use when there is a wash step.""" - self.liquid_history.clear() + self.pending_volume = self.volume def serialize(self) -> dict: """Serialize the volume tracker.""" - - if not self.is_cross_contamination_tracking_disabled: - return { - "liquids": [serialize(liquid) for liquid in self.liquids], - "pending_liquids": [serialize(liquid) for liquid in self.pending_liquids], - "liquid_history": [serialize(liquid) for liquid in self.liquid_history], - } - return { - "liquids": [serialize(liquid) for liquid in self.liquids], - "pending_liquids": [serialize(liquid) for liquid in self.pending_liquids], + "volume": self.volume, + "pending_volume": self.pending_volume, + "thing": self.thing, + "max_volume": self.max_volume, } def load_state(self, state: dict) -> None: """Load the state of the volume tracker.""" - - def load_liquid(data) -> Tuple[Optional["Liquid"], float]: - return cast(Tuple["Liquid", float], tuple(deserialize(data))) - - self.liquids = [load_liquid(liquid) for liquid in state["liquids"]] - self.pending_liquids = [load_liquid(liquid) for liquid in state["pending_liquids"]] - - if not self.is_cross_contamination_tracking_disabled: - self.liquid_history = set(state["liquid_history"]) + self.volume = state["volume"] + self.pending_volume = state["pending_volume"] + self.thing = state["thing"] + self.max_volume = state["max_volume"] def register_callback(self, callback: VolumeTrackerCallback) -> None: self._callback = callback diff --git a/pylabrobot/resources/volume_tracker_tests.py b/pylabrobot/resources/volume_tracker_tests.py index 87566c1d59e..b763a0aed44 100644 --- a/pylabrobot/resources/volume_tracker_tests.py +++ b/pylabrobot/resources/volume_tracker_tests.py @@ -4,7 +4,6 @@ TooLittleLiquidError, TooLittleVolumeError, ) -from pylabrobot.resources.liquid import Liquid from pylabrobot.resources.volume_tracker import VolumeTracker @@ -16,14 +15,14 @@ def test_init(self): self.assertEqual(tracker.get_free_volume(), 100) self.assertEqual(tracker.get_used_volume(), 0) - tracker.set_liquids([(None, 20)]) + tracker.set_volume(20) self.assertEqual(tracker.get_free_volume(), 80) self.assertEqual(tracker.get_used_volume(), 20) def test_add_liquid(self): tracker = VolumeTracker(thing="test", max_volume=100) - tracker.add_liquid(liquid=None, volume=20) + tracker.add_liquid(volume=20) self.assertEqual(tracker.get_used_volume(), 20) self.assertEqual(tracker.get_free_volume(), 80) @@ -32,11 +31,10 @@ def test_add_liquid(self): self.assertEqual(tracker.get_free_volume(), 80) with self.assertRaises(TooLittleVolumeError): - tracker.add_liquid(liquid=None, volume=100) + tracker.add_liquid(volume=100) def test_remove_liquid(self): - tracker = VolumeTracker(thing="test", max_volume=100) - tracker.add_liquid(liquid=None, volume=60) + tracker = VolumeTracker(thing="test", max_volume=100, initial_volume=60) tracker.commit() self.assertEqual(tracker.get_used_volume(), 60) @@ -46,21 +44,3 @@ def test_remove_liquid(self): with self.assertRaises(TooLittleLiquidError): tracker.remove_liquid(volume=100) - - def test_get_liquids(self): - tracker = VolumeTracker(thing="test", max_volume=200) - tracker.add_liquid(liquid=None, volume=60) - tracker.add_liquid(liquid=Liquid.WATER, volume=60) - tracker.commit() - - liquids = tracker.get_liquids(top_volume=100) - self.assertEqual(liquids, [(Liquid.WATER, 60), (None, 40)]) - - liquids = tracker.get_liquids(top_volume=50) - self.assertEqual(liquids, [(Liquid.WATER, 50)]) - - liquids = tracker.get_liquids(top_volume=60) - self.assertEqual(liquids, [(Liquid.WATER, 60)]) - - with self.assertRaises(TooLittleLiquidError): - tracker.get_liquids(top_volume=600) diff --git a/pylabrobot/resources/well.py b/pylabrobot/resources/well.py index aa1e02fd868..e71f34359a6 100644 --- a/pylabrobot/resources/well.py +++ b/pylabrobot/resources/well.py @@ -108,15 +108,23 @@ def serialize(self): "cross_section_type": self.cross_section_type.value, } - def set_liquids(self, liquids: List[Tuple[Optional["Liquid"], float]]): - """Set the liquids in the well. + def set_volume(self, volume: float): + """Set the volume of the well. - (wraps :meth:`~.VolumeTracker.set_liquids`) + (wraps :meth:`~.VolumeTracker.set_volume`) Example: - Set the liquids in a well to 10 uL of water: + Set the volume in a well to 10 uL: + + >>> well.set_volume(10) + """ + + self.tracker.set_volume(volume) + + def set_liquids(self, liquids: List[Tuple[Optional["Liquid"], float]]): + """Set the liquids in the well. - >>> well.set_liquids([(Liquid.WATER, 10)]) + Deprecated: Use `set_volume` instead. This method will be removed in a future version. """ self.tracker.set_liquids(liquids) diff --git a/pylabrobot/resources/well_tests.py b/pylabrobot/resources/well_tests.py index 69a9985e493..50fba05bd97 100644 --- a/pylabrobot/resources/well_tests.py +++ b/pylabrobot/resources/well_tests.py @@ -42,20 +42,6 @@ def test_serialize(self): self.assertEqual(Well.deserialize(well.serialize()), well) - def test_set_liquids(self): - well = Well( - name="well...", - size_x=1, - size_y=2, - size_z=3, - bottom_type=WellBottomType.FLAT, - max_volume=10, - model="model", - ) - well.set_liquids([(None, 10)]) - self.assertEqual(well.tracker.liquids, [(None, 10)]) - self.assertEqual(well.tracker.get_used_volume(), 10) - def test_get_index_in_plate(self): plate = Plate( "plate", diff --git a/pylabrobot/server/liquid_handling_api_tests.py b/pylabrobot/server/liquid_handling_api_tests.py index f42846ef614..a407f93f0df 100644 --- a/pylabrobot/server/liquid_handling_api_tests.py +++ b/pylabrobot/server/liquid_handling_api_tests.py @@ -30,7 +30,7 @@ def build_layout() -> HamiltonDeck: plt_car = PLT_CAR_L5AC_A00(name="plate_carrier") plt_car[0] = plate = Cor_96_wellplate_360ul_Fb(name="aspiration plate") - plate.get_item("A1").tracker.set_liquids([(None, 400)]) + plate.get_item("A1").tracker.set_volume(400) deck = STARLetDeck() deck.assign_child_resource(tip_car, rails=1) @@ -211,7 +211,6 @@ def test_aspirate(self): "y": 0, "z": 0, }, - "liquids": [[None, 10]], "flow_rate": None, "liquid_height": None, "blow_out_air_volume": 0, @@ -245,7 +244,6 @@ def test_dispense(self): "y": 0, "z": 0, }, - "liquids": [[None, 10]], "flow_rate": None, "liquid_height": None, "blow_out_air_volume": 0, diff --git a/pylabrobot/server/liquid_handling_server.py b/pylabrobot/server/liquid_handling_server.py index 4882f8ccfd7..821e77a568e 100644 --- a/pylabrobot/server/liquid_handling_server.py +++ b/pylabrobot/server/liquid_handling_server.py @@ -5,7 +5,7 @@ import json import os import threading -from typing import Any, Coroutine, List, Optional, Tuple, cast +from typing import Any, Coroutine, List, Optional, cast import werkzeug from flask import ( @@ -31,7 +31,7 @@ SingleChannelAspiration, SingleChannelDispense, ) -from pylabrobot.resources import Coordinate, Deck, Liquid, Tip +from pylabrobot.resources import Coordinate, Deck, Tip from pylabrobot.serializer import deserialize lh_api = Blueprint("liquid handling", __name__) @@ -231,10 +231,6 @@ async def aspirate(): flow_rate = sc["flow_rate"] liquid_height = sc["liquid_height"] blow_out_air_volume = sc["blow_out_air_volume"] - liquids = cast( - List[Tuple[Optional[Liquid], float]], - deserialize(sc["liquids"]), - ) mix = Mix(**sc["mix"]) if sc.get("mix") is not None else None aspirations.append( SingleChannelAspiration( @@ -245,7 +241,6 @@ async def aspirate(): flow_rate=flow_rate, liquid_height=liquid_height, blow_out_air_volume=blow_out_air_volume, - liquids=liquids, mix=mix, ) ) @@ -289,10 +284,6 @@ async def dispense(): flow_rate = sc["flow_rate"] liquid_height = sc["liquid_height"] blow_out_air_volume = sc["blow_out_air_volume"] - liquids = cast( - List[Tuple[Optional[Liquid], float]], - deserialize(sc["liquids"]), - ) mix = Mix(**sc["mix"]) if sc.get("mix") is not None else None dispenses.append( SingleChannelDispense( @@ -303,7 +294,6 @@ async def dispense(): flow_rate=flow_rate, liquid_height=liquid_height, blow_out_air_volume=blow_out_air_volume, - liquids=liquids, mix=mix, ) ) diff --git a/pylabrobot/visualizer/lib.js b/pylabrobot/visualizer/lib.js index 8041d4b11d3..8c463d8d5c2 100644 --- a/pylabrobot/visualizer/lib.js +++ b/pylabrobot/visualizer/lib.js @@ -710,7 +710,7 @@ class Container extends Resource { super(resourceData, parent); const { max_volume } = resourceData; this.maxVolume = max_volume; - this.liquids = resourceData.liquids || []; + this.volume = resourceData.volume || 0; } static colorForVolume(volume, maxVolume) { @@ -718,30 +718,22 @@ class Container extends Resource { } getVolume() { - return this.liquids.reduce((acc, liquid) => acc + liquid.volume, 0); + return this.volume; } - setLiquids(liquids) { - this.liquids = liquids; + setVolume(volume) { + this.volume = volume; this.update(); } setState(state) { - let liquids = []; - for (let i = 0; i < state.liquids.length; i++) { - const liquid = state.liquids[i]; - liquids.push({ - name: liquid[0], - volume: liquid[1], - }); - } - this.setLiquids(liquids); + this.setVolume(state.volume); } serializeState() { return { - liquids: this.liquids, - pending_liquids: this.liquids, + volume: this.volume, + pending_volume: this.volume, }; } diff --git a/pylabrobot/visualizer/visualizer_tests.py b/pylabrobot/visualizer/visualizer_tests.py index 7a27f9ec733..e97c88f1a7a 100644 --- a/pylabrobot/visualizer/visualizer_tests.py +++ b/pylabrobot/visualizer/visualizer_tests.py @@ -139,12 +139,12 @@ async def test_state_updated(self): """Test that the state_updated method sends the correct event.""" plate = Cor_96_wellplate_360ul_Fb(name="plate_01") self.r.assign_child_resource(plate, location=Coordinate(0, 0, 0)) - plate.set_well_liquids((None, 500)) + plate.set_well_volumes([500] * 96) time.sleep(0.1) self.vis.send_command.assert_called() # type: ignore[attr-defined] call_args = self.vis.send_command.call_args[1] # type: ignore[attr-defined] self.assertEqual(call_args["event"], "set_state") self.assertEqual( - call_args["data"]["plate_01_well_H12"]["liquids"], - [[None, 500]], + call_args["data"]["plate_01_well_H12"]["volume"], + 500, )