Skip to content

Commit

Permalink
more chatterboxes (#242)
Browse files Browse the repository at this point in the history
  • Loading branch information
rickwierenga authored Sep 11, 2024
1 parent 0f6146a commit 5826c73
Show file tree
Hide file tree
Showing 26 changed files with 458 additions and 228 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `PlateCarrierSite` can now take `ResourceStack` as a child, as long as the children are `Plate`s (https://github.com/PyLabRobot/pylabrobot/pull/226)
- `Resource.get_size_{x,y,z}` now return the size of the resource in local space, not absolute space (https://github.com/PyLabRobot/pylabrobot/pull/235)
- `Resource.center` now returns the center of the resource in local space, not absolute space (https://github.com/PyLabRobot/pylabrobot/pull/235)
- Rename `ChatterBoxBackend` to `LiquidHandlerChatterboxBackend` (https://github.com/PyLabRobot/pylabrobot/pull/242)
- Move `LiquidHandlerChatterboxBackend` from `liquid_handling.backends.chatterbox_backend` to `liquid_handling.backends.chatterbox` (https://github.com/PyLabRobot/pylabrobot/pull/242)

### Added

Expand Down Expand Up @@ -61,6 +63,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `HTF_L_ULTRAWIDE`, `ultrawide_high_volume_tip_with_filter` (https://github.com/PyLabRobot/pylabrobot/pull/229/)
- `get_absolute_size_x`, `get_absolute_size_y`, `get_absolute_size_z` for `Resource` (https://github.com/PyLabRobot/pylabrobot/pull/235)
- `Cytation5Backend` for plate reading on BioTek Cytation 5 (https://github.com/PyLabRobot/pylabrobot/pull/238)
- More chatterboxes (https://github.com/PyLabRobot/pylabrobot/pull/242)
- `FanChatterboxBackend`
- `PlateReaderChatterboxBackend`
- `PowderDispenserChatterboxBackend`
- `PumpChatterboxBackend`
- `PumpArrayChatterboxBackend`
- `ScaleChatterboxBackend`
- `ShakerChatterboxBackend`
- `TemperatureControllerChatterboxBackend`

### Deprecated

Expand Down
1 change: 1 addition & 0 deletions docs/pylabrobot.heating_shaking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ Backends
:nosignatures:
:recursive:

chatterbox.HeaterShakerChatterboxBackend
inheco.InhecoThermoShake
2 changes: 1 addition & 1 deletion docs/pylabrobot.liquid_handling.backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ Testing
:nosignatures:
:recursive:

backends.chatterbox_backend.ChatterBoxBackend
backends.chatterbox.LiquidHandlerChatterboxBackend
1 change: 1 addition & 0 deletions docs/pylabrobot.only_fans.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ Backends
:recursive:

backend.FanBackend
chatterbox.FanChatterboxBackend
hamilton_hepa_fan.HamiltonHepaFan
1 change: 1 addition & 0 deletions docs/pylabrobot.plate_reading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ Backends
:nosignatures:
:recursive:

chatterbox.PlateReaderChatterboxBackend
clario_star.CLARIOStar
1 change: 1 addition & 0 deletions docs/pylabrobot.pumps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ Backends
:nosignatures:
:recursive:

chatterbox.PumpChatterboxBackend
cole_parmer.masterflex.Masterflex
1 change: 1 addition & 0 deletions docs/pylabrobot.scales.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ Backends
:nosignatures:
:recursive:

chatterbox.ScaleChatterboxBackend
mettler_toledo.MettlerToledoWXS205SDU
1 change: 1 addition & 0 deletions docs/pylabrobot.shaking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ Backends
:recursive:

backend.ShakerBackend
chatterbox.ShakerChatterboxBackend
1 change: 1 addition & 0 deletions docs/pylabrobot.temperature_controlling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ Backends
:nosignatures:
:recursive:

chatterbox.TemperatureControllerChatterboxBackend
opentrons_backend.OpentronsTemperatureModuleBackend
1 change: 1 addition & 0 deletions docs/pylabrobot.tilting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ Backends
:nosignatures:
:recursive:

chatterbox.TilterChatterboxBackend
tilter_backend.TilterBackend
hamilton_backend.HamiltonTiltModuleBackend
10 changes: 5 additions & 5 deletions docs/using-the-visualizer.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"\n",
"The Visualizer is a tool that allows you to visualize the a Resource (like LiquidHandler) including its state to easily see what is going on, for example when executing a protocol on a robot or when developing a new protocol.\n",
"\n",
"When using a backend that does not require access to a physical robot, such as the {class}`~pylabrobot.liquid_handling.backends.chatterbox_backend.ChatterBoxBackend`, the Visualizer can be used to simulate a robot's behavior. Of course, you may also use the Visualizer when working with a real robot to see what is happening in the PLR resource and state trackers."
"When using a backend that does not require access to a physical robot, such as the {class}`~pylabrobot.liquid_handling.backends.chatterbox_backend.ChatterboxBackend`, the Visualizer can be used to simulate a robot's behavior. Of course, you may also use the Visualizer when working with a real robot to see what is happening in the PLR resource and state trackers."
]
},
{
Expand All @@ -20,7 +20,7 @@
"source": [
"## Setting up a connection with the robot\n",
"\n",
"As described in the [basic liquid handling tutorial](basic), we will use the {class}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler` class to control the robot. This time, however, instead of using the Hamilton {class}`~pylabrobot.liquid_handling.backends.hamilton.STAR.STAR` backend, we are using the software-only {class}`~pylabrobot.liquid_handling.backends.chatterbox_backend.ChatterBoxBackend` backend. This means that liquid handling will work exactly the same, but commands are simply printed out to the console instead of being sent to a physical robot. We are still using the same deck."
"As described in the [basic liquid handling tutorial](basic), we will use the {class}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler` class to control the robot. This time, however, instead of using the Hamilton {class}`~pylabrobot.liquid_handling.backends.hamilton.STAR.STAR` backend, we are using the software-only {class}`~pylabrobot.liquid_handling.backends.chatterbox_backend.ChatterboxBackend` backend. This means that liquid handling will work exactly the same, but commands are simply printed out to the console instead of being sent to a physical robot. We are still using the same deck."
]
},
{
Expand All @@ -31,7 +31,7 @@
"outputs": [],
"source": [
"from pylabrobot.liquid_handling import LiquidHandler\n",
"from pylabrobot.liquid_handling.backends import ChatterBoxBackend\n",
"from pylabrobot.liquid_handling.backends import ChatterboxBackend\n",
"from pylabrobot.visualizer.visualizer import Visualizer"
]
},
Expand All @@ -52,7 +52,7 @@
"metadata": {},
"outputs": [],
"source": [
"lh = LiquidHandler(backend=ChatterBoxBackend(), deck=STARLetDeck())"
"lh = LiquidHandler(backend=ChatterboxBackend(), deck=STARLetDeck())"
]
},
{
Expand Down Expand Up @@ -384,7 +384,7 @@
"source": [
"### Picking up tips\n",
"\n",
"Note that since we are using the {class}`~pylabrobot.liquid_handling.backends.chatterbox_backend.ChatterBoxBackend`, we just print out \"Picking up tips\" instead of actually performing an operation. The visualizer will show the tips being picked up."
"Note that since we are using the {class}`~pylabrobot.liquid_handling.backends.chatterbox_backend.ChatterboxBackend`, we just print out \"Picking up tips\" instead of actually performing an operation. The visualizer will show the tips being picked up."
]
},
{
Expand Down
4 changes: 2 additions & 2 deletions docs/using-trackers.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
],
"source": [
"from pylabrobot.liquid_handling import LiquidHandler\n",
"from pylabrobot.liquid_handling.backends.chatterbox_backend import ChatterBoxBackend\n",
"from pylabrobot.liquid_handling.backends.chatterbox_backend import ChatterboxBackend\n",
"from pylabrobot.resources import (\n",
" TIP_CAR_480_A00,\n",
" HTF_L,\n",
Expand All @@ -39,7 +39,7 @@
")\n",
"from pylabrobot.resources.hamilton import STARLetDeck\n",
"\n",
"lh = LiquidHandler(backend=ChatterBoxBackend(num_channels=8), deck=STARLetDeck())\n",
"lh = LiquidHandler(backend=ChatterboxBackend(num_channels=8), deck=STARLetDeck())\n",
"await lh.setup()"
]
},
Expand Down
7 changes: 7 additions & 0 deletions pylabrobot/heating_shaking/chatterbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pylabrobot.shaking.chatterbox import ShakerChatterboxBackend
from pylabrobot.temperature_controlling.chatterbox import TemperatureControllerChatterboxBackend


class HeaterShakerChatterboxBackend(
ShakerChatterboxBackend, TemperatureControllerChatterboxBackend):
pass
1 change: 1 addition & 0 deletions pylabrobot/liquid_handling/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .backend import LiquidHandlerBackend
from .chatterbox import LiquidHandlerChatterboxBackend
from .chatterbox_backend import ChatterBoxBackend
from .serializing_backend import SerializingBackend, SerializingSavingBackend # many rely on this
from .websocket import WebSocketBackend
Expand Down
218 changes: 218 additions & 0 deletions pylabrobot/liquid_handling/backends/chatterbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# pylint: disable=unused-argument, inconsistent-quotes

from typing import List, Union

from pylabrobot.liquid_handling.backends.backend import LiquidHandlerBackend
from pylabrobot.resources import Resource
from pylabrobot.liquid_handling.standard import (
Pickup,
PickupTipRack,
Drop,
DropTipRack,
Aspiration,
AspirationPlate,
AspirationContainer,
Dispense,
DispensePlate,
DispenseContainer,
Move
)


class LiquidHandlerChatterboxBackend(LiquidHandlerBackend):
""" Chatter box backend for device-free testing. Prints out all operations. """

_pip_length = 5
_vol_length = 8
_resource_length = 20
_offset_length = 16
_flow_rate_length = 10
_blowout_length = 10
_lld_z_length = 10
_kwargs_length = 15
_tip_type_length = 12
_max_volume_length = 16
_fitting_depth_length = 20
_tip_length_length = 16
# _pickup_method_length = 20
_filter_length = 10

def __init__(self, num_channels: int = 8):
""" Initialize a chatter box backend. """
super().__init__()
self._num_channels = num_channels

async def setup(self):
await super().setup()
print("Setting up the liquid handler.")

async def stop(self):
print("Stopping the liquid handler.")

def serialize(self) -> dict:
return {**super().serialize(), "num_channels": self.num_channels}

@property
def num_channels(self) -> int:
return self._num_channels

async def assigned_resource_callback(self, resource: Resource):
print(f"Resource {resource.name} was assigned to the liquid handler.")

async def unassigned_resource_callback(self, name: str):
print(f"Resource {name} was unassigned from the liquid handler.")

async def pick_up_tips(self, ops: List[Pickup], use_channels: List[int], **backend_kwargs):
print("Picking up tips:")
header = (
f"{'pip#':<{LiquidHandlerChatterboxBackend._pip_length}} "
f"{'resource':<{LiquidHandlerChatterboxBackend._resource_length}} "
f"{'offset':<{LiquidHandlerChatterboxBackend._offset_length}} "
f"{'tip type':<{LiquidHandlerChatterboxBackend._tip_type_length}} "
f"{'max volume (µL)':<{LiquidHandlerChatterboxBackend._max_volume_length}} "
f"{'fitting depth (mm)':<{LiquidHandlerChatterboxBackend._fitting_depth_length}} "
f"{'tip length (mm)':<{LiquidHandlerChatterboxBackend._tip_length_length}} "
# f"{'pickup method':<{ChatterboxBackend._pickup_method_length}} "
f"{'filter':<{LiquidHandlerChatterboxBackend._filter_length}}"
)
print(header)

for op, channel in zip(ops, use_channels):
offset = f"{round(op.offset.x, 1)},{round(op.offset.y, 1)},{round(op.offset.z, 1)}"
row = (
f" p{channel}: "
f"{op.resource.name[-30:]:<{LiquidHandlerChatterboxBackend._resource_length}} "
f"{offset:<{LiquidHandlerChatterboxBackend._offset_length}} "
f"{op.tip.__class__.__name__:<{LiquidHandlerChatterboxBackend._tip_type_length}} "
f"{op.tip.maximal_volume:<{LiquidHandlerChatterboxBackend._max_volume_length}} "
f"{op.tip.fitting_depth:<{LiquidHandlerChatterboxBackend._fitting_depth_length}} "
f"{op.tip.total_tip_length:<{LiquidHandlerChatterboxBackend._tip_length_length}} "
# f"{str(op.tip.pickup_method)[-20:]:<{ChatterboxBackend._pickup_method_length}} "
f"{'Yes' if op.tip.has_filter else 'No':<{LiquidHandlerChatterboxBackend._filter_length}}"
)
print(row)

async def drop_tips(self, ops: List[Drop], use_channels: List[int], **backend_kwargs):
print("Dropping tips:")
header = (
f"{'pip#':<{LiquidHandlerChatterboxBackend._pip_length}} "
f"{'resource':<{LiquidHandlerChatterboxBackend._resource_length}} "
f"{'offset':<{LiquidHandlerChatterboxBackend._offset_length}} "
f"{'tip type':<{LiquidHandlerChatterboxBackend._tip_type_length}} "
f"{'max volume (µL)':<{LiquidHandlerChatterboxBackend._max_volume_length}} "
f"{'fitting depth (mm)':<{LiquidHandlerChatterboxBackend._fitting_depth_length}} "
f"{'tip length (mm)':<{LiquidHandlerChatterboxBackend._tip_length_length}} "
# f"{'pickup method':<{ChatterboxBackend._pickup_method_length}} "
f"{'filter':<{LiquidHandlerChatterboxBackend._filter_length}}"
)
print(header)

for op, channel in zip(ops, use_channels):
offset = f"{round(op.offset.x, 1)},{round(op.offset.y, 1)},{round(op.offset.z, 1)}"
row = (
f" p{channel}: "
f"{op.resource.name[-30:]:<{LiquidHandlerChatterboxBackend._resource_length}} "
f"{offset:<{LiquidHandlerChatterboxBackend._offset_length}} "
f"{op.tip.__class__.__name__:<{LiquidHandlerChatterboxBackend._tip_type_length}} "
f"{op.tip.maximal_volume:<{LiquidHandlerChatterboxBackend._max_volume_length}} "
f"{op.tip.fitting_depth:<{LiquidHandlerChatterboxBackend._fitting_depth_length}} "
f"{op.tip.total_tip_length:<{LiquidHandlerChatterboxBackend._tip_length_length}} "
# f"{str(op.tip.pickup_method)[-20:]:<{ChatterboxBackend._pickup_method_length}} "
f"{'Yes' if op.tip.has_filter else 'No':<{LiquidHandlerChatterboxBackend._filter_length}}"
)
print(row)

async def aspirate(self, ops: List[Aspiration], use_channels: List[int], **backend_kwargs):
print("Aspirating:")
header = (
f"{'pip#':<{LiquidHandlerChatterboxBackend._pip_length}} "
f"{'vol(ul)':<{LiquidHandlerChatterboxBackend._vol_length}} "
f"{'resource':<{LiquidHandlerChatterboxBackend._resource_length}} "
f"{'offset':<{LiquidHandlerChatterboxBackend._offset_length}} "
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:]
print(header)

for o, p in zip(ops, use_channels):
offset = f"{round(o.offset.x, 1)},{round(o.offset.y, 1)},{round(o.offset.z, 1)}"
row = (
f" p{p}: "
f"{o.volume:<{LiquidHandlerChatterboxBackend._vol_length}} "
f"{o.resource.name[-20:]:<{LiquidHandlerChatterboxBackend._resource_length}} "
f"{offset:<{LiquidHandlerChatterboxBackend._offset_length}} "
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):
value = "".join("T" if v else "F" for v in value)
if isinstance(value, list):
value = "".join(map(str, value))
row += f" {value:<15}"
print(row)

async def dispense(self, ops: List[Dispense], use_channels: List[int], **backend_kwargs):
print("Dispensing:")
header = (
f"{'pip#':<{LiquidHandlerChatterboxBackend._pip_length}} "
f"{'vol(ul)':<{LiquidHandlerChatterboxBackend._vol_length}} "
f"{'resource':<{LiquidHandlerChatterboxBackend._resource_length}} "
f"{'offset':<{LiquidHandlerChatterboxBackend._offset_length}} "
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:]
print(header)

for o, p in zip(ops, use_channels):
offset = f"{round(o.offset.x, 1)},{round(o.offset.y, 1)},{round(o.offset.z, 1)}"
row = (
f" p{p}: "
f"{o.volume:<{LiquidHandlerChatterboxBackend._vol_length}} "
f"{o.resource.name[-20:]:<{LiquidHandlerChatterboxBackend._resource_length}} "
f"{offset:<{LiquidHandlerChatterboxBackend._offset_length}} "
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):
value = ''.join('T' if v else 'F' for v in value)
if isinstance(value, list):
value = "".join(map(str, value))
row += f" {value:<{LiquidHandlerChatterboxBackend._kwargs_length}}"
print(row)

async def pick_up_tips96(self, pickup: PickupTipRack, **backend_kwargs):
print(f"Picking up tips from {pickup.resource.name}.")

async def drop_tips96(self, drop: DropTipRack, **backend_kwargs):
print(f"Dropping tips to {drop.resource.name}.")

async def aspirate96(self, aspiration: Union[AspirationPlate, AspirationContainer]):
if isinstance(aspiration, AspirationPlate):
resource = aspiration.wells[0].parent
else:
resource = aspiration.container
print(f"Aspirating {aspiration.volume} from {resource}.")

async def dispense96(self, dispense: Union[DispensePlate, DispenseContainer]):
if isinstance(dispense, DispensePlate):
resource = dispense.wells[0].parent
else:
resource = dispense.container
print(f"Dispensing {dispense.volume} to {resource}.")

async def move_resource(self, move: Move, **backend_kwargs):
print(f"Moving {move}.")
Loading

0 comments on commit 5826c73

Please sign in to comment.