diff --git a/pylabrobot/filters/RetroFilterV4/RetroFilterV4.md b/pylabrobot/filters/RetroFilterV4/RetroFilterV4.md new file mode 100644 index 0000000000..3fc317d200 --- /dev/null +++ b/pylabrobot/filters/RetroFilterV4/RetroFilterV4.md @@ -0,0 +1,23 @@ +# Retro Filter V4 + + + +Designed for flow cytometry prep. + +## Usage Instructions + +1. Lay **40–80 µm filter cloth** between the upper and lower portions. +2. Snap the filter assembly shut. +3. Lay scissors flush against the top-bottom interface. +4. **Cut the filter cloth close** to the printed parts to avoid any excess that could obstruct robotic handling. +5. The filter is ready for **single-use**. + +> **Tip**: For a reusable filtration assembly, purchase **80 µm steel mesh** from Component Supply. + + +### [OnShape doc](https://cad.onshape.com/documents/c9c3cf6b64034d54f966eda5/w/fed83636389b833df37c2dac/e/80426a2258abe186fc00e172?renderMode=0&uiState=6768d74c1ea3896154236237) + + +
+ +
diff --git a/pylabrobot/filters/RetroFilterV4/RetroFilterV4.png b/pylabrobot/filters/RetroFilterV4/RetroFilterV4.png new file mode 100644 index 0000000000..836974dbd6 Binary files /dev/null and b/pylabrobot/filters/RetroFilterV4/RetroFilterV4.png differ diff --git a/pylabrobot/filters/RetroFilterV4/RetroFilterV4_lower.stl b/pylabrobot/filters/RetroFilterV4/RetroFilterV4_lower.stl new file mode 100644 index 0000000000..1231bef333 Binary files /dev/null and b/pylabrobot/filters/RetroFilterV4/RetroFilterV4_lower.stl differ diff --git a/pylabrobot/filters/RetroFilterV4/RetroFilterV4_upper.stl b/pylabrobot/filters/RetroFilterV4/RetroFilterV4_upper.stl new file mode 100644 index 0000000000..72de058012 Binary files /dev/null and b/pylabrobot/filters/RetroFilterV4/RetroFilterV4_upper.stl differ diff --git a/pylabrobot/filters/__init__.py b/pylabrobot/filters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pylabrobot/filters/filter.py b/pylabrobot/filters/filter.py new file mode 100644 index 0000000000..296ea9d045 --- /dev/null +++ b/pylabrobot/filters/filter.py @@ -0,0 +1,78 @@ +from typing import Optional, Union + +from pylabrobot.liquid_handling.liquid_handler import LiquidHandler +from pylabrobot.resources.carrier import ResourceHolder +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.plate import Lid, Plate + + +class Filter(Lid): + """Filter for plates for use in filtering cells before flow cytometry.""" + + filter_dispense_offset = Coordinate( + 0, 0, 7 + ) # height to pipette through filter (required pressure on filter) + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + nesting_z_height: float, + category: str = "filter", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + nesting_z_height=nesting_z_height, + ) + + async def move_filter( + self, lh: LiquidHandler, to_dest: Union[Plate, ResourceHolder], arm: str = "core", **kwargs + ): + """move filter from CarrierSite to a Plate using core grippers (faster) or iSWAP (slower)""" + await lh.move_lid( + lid=self, + to=to_dest, + use_arm=arm, + pickup_distance_from_top=15, + core_grip_strength=20, + return_core_gripper=True, + **kwargs, + ) + + async def dispense_through_filter( + self, indices: list[int], volume: float, lh: LiquidHandler, **disp_kwargs + ): + assert isinstance(self.parent, Plate), "Filter must be placed on a plate to be pipetted." + + offsets = disp_kwargs.get("offsets", self.filter_dispense_offset) + if not isinstance(offsets, Coordinate): + raise ValueError("Offsets must be a Coordinate.") + + defaults = { + "offsets": [offsets + self.filter_dispense_offset] * len(indices) + if isinstance(offsets, Coordinate) + else [offsets] * len(indices), + "transport_air_volume": 5, + "swap_speed": 100, + "minimum_traverse_height_at_beginning_of_a_command": self.parent.get_absolute_location( + "c", "c", "t" + ).z + + 20, + "min_z_endpos": self.parent.get_absolute_location("c", "c", "t").z + 20, + } + + disp_params = {**defaults, **{k: v for k, v in disp_kwargs.items() if k in defaults}} + + await lh.dispense([self.parent[i][0] for i in indices], [volume] * len(indices), **disp_params) + + +def RetroFilterv4(name: str) -> Filter: + return Filter(name=name, size_x=129, size_y=88, size_z=19.7, nesting_z_height=2) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 14a5876c92..d371f2d521 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -28,6 +28,7 @@ Strictness, get_strictness, ) +from pylabrobot.filters.filter import Filter from pylabrobot.machines.machine import Machine, need_setup_finished from pylabrobot.plate_reading import PlateReader from pylabrobot.resources import ( @@ -777,8 +778,12 @@ async def aspirate( # Checks for resource in resources: - if isinstance(resource.parent, Plate) and resource.parent.has_lid(): - raise ValueError("Aspirating from a well with a lid is not supported.") + if ( + isinstance(resource.parent, Plate) + and resource.parent.has_lid() + and not isinstance(resource.parent.lid, Filter) + ): + raise ValueError("Aspirating from a well with a non-filter lid is not supported.") self._make_sure_channels_exist(use_channels) assert len(resources) == len(vols) == len(offsets) == len(flow_rates) == len(liquid_height) @@ -1000,8 +1005,12 @@ async def dispense( raise BlowOutVolumeError("Blowout volume is larger than aspirated volume") for resource in resources: - if isinstance(resource.parent, Plate) and resource.parent.has_lid(): - raise ValueError("Dispensing to plate with lid") + if ( + isinstance(resource.parent, Plate) + and resource.parent.has_lid() + and not isinstance(resource.parent.lid, Filter) + ): + raise ValueError("Aspirating from a well with a non-filter lid is not supported.") assert len(vols) == len(offsets) == len(flow_rates) == len(liquid_height)