diff --git a/science_jubilee/Machine.py b/science_jubilee/Machine.py index 8a5dbc4..1fa8826 100644 --- a/science_jubilee/Machine.py +++ b/science_jubilee/Machine.py @@ -359,13 +359,7 @@ def position(self): def load_deck(self, deck_filename: str, path :str = os.path.join(os.path.dirname(__file__), 'decks', 'deck_definition')): # do thing #make sure filename has json - if deck_filename[-4:] != 'json': - deck_filename = deck_filename + '.json' - - config_path = os.path.join(path, deck_filename) - with open(config_path, "r") as f: - deck_config = json.load(f) - deck = Deck(deck_config) + deck = Deck(deck_filename, path=path) self.deck = deck return deck diff --git a/science_jubilee/decks/Deck.py b/science_jubilee/decks/Deck.py index d9dc50c..da3e72e 100644 --- a/science_jubilee/decks/Deck.py +++ b/science_jubilee/decks/Deck.py @@ -30,7 +30,7 @@ class Deck(SlotSet): def __init__(self, deck_filename, path :str = os.path.join(os.path.dirname(__file__), 'deck_definition')): # load in the deck configuration file - if deck_filename[-4] != 'json': + if deck_filename[-4:] != 'json': deck_filename = deck_filename + '.json' config_path = os.path.join(path, f"{deck_filename}" ) diff --git a/science_jubilee/decks/configs/lab_automation_deck.json b/science_jubilee/decks/configs/lab_automation_deck.json deleted file mode 100644 index d74e2c3..0000000 --- a/science_jubilee/decks/configs/lab_automation_deck.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "deck_type": "Lab Automation Deck", - "deck_slots": { - "total": 6, - "type": - }, - "slots": { - -1: { - "offset": , - "has_labware": false, - "labware": null - }, - - 0: { - "offset": , - "has_labware": false, - "labware": null - }, - - 1: { - "offset": , - "has_labware": false, - "labware": null - }, - - 2: { - "offset": , - "has_labware": false, - "labware": null - }, - - 3: { - "offset": , - "has_labware": false, - "labware": null - }, - - 4: { - "offset": , - "has_labware": false, - "labware": null - }, - - 5: { - "offset": , - "has_labware": false, - "labware": null - }, - - }, - "offset_from": { - "corner": top_right - }, - "material": { - "plate": Aluminum, - "mask": Delrin - } -} \ No newline at end of file diff --git a/science_jubilee/decks/deck_definition/lab_automation_deck.json b/science_jubilee/decks/deck_definition/lab_automation_deck.json index 5c41efc..e706ea4 100644 --- a/science_jubilee/decks/deck_definition/lab_automation_deck.json +++ b/science_jubilee/decks/deck_definition/lab_automation_deck.json @@ -7,48 +7,48 @@ "slots": { "0": { "offset": [ - 15.3, - 7.7 + 14.9, + 4.4 ], "has_labware": false, "labware": null }, "1": { "offset": [ - 155.8, - 7.9 + 157.0, + 5.8 ], "has_labware": false, "labware": null }, "2": { "offset": [ - 15.0, - 104.6 + 14.3, + 100.8 ], "has_labware": false, "labware": null }, "3": { "offset": [ - 155.6, - 104.7 + 154.9, + 98.3 ], "has_labware": false, "labware": null }, "4": { "offset": [ - 15.1, - 201.3 + 17.1, + 201.1 ], "has_labware": false, "labware": null }, "5": { "offset": [ - 155.3, - 201.4 + 157.0, + 201.9 ], "has_labware": false, "labware": null diff --git a/science_jubilee/decks/configs/lab_automation_deck_blair.json b/science_jubilee/decks/deck_definition/lab_automation_deck_blair.json similarity index 100% rename from science_jubilee/decks/configs/lab_automation_deck_blair.json rename to science_jubilee/decks/deck_definition/lab_automation_deck_blair.json diff --git a/science_jubilee/labware/Labware.py b/science_jubilee/labware/Labware.py index d0291a5..a155542 100644 --- a/science_jubilee/labware/Labware.py +++ b/science_jubilee/labware/Labware.py @@ -1,7 +1,7 @@ import numpy as np from dataclasses import dataclass from itertools import chain -from typing import List, Dict, Tuple +from typing import List, Dict, Tuple, Union, Iterable, NamedTuple import os import json from pathlib import Path @@ -57,14 +57,26 @@ def z(self, new_z): self._z = new_z @property - def top(self): + def top_(self): """Defines the top-most point of the well""" return self.z + self.depth @property - def bottom(self): + def bottom_(self): """Defines the bottom-most point of the well""" return self.z + + def bottom(self, z: float = 0.0): + from_bottom_z = self.bottom_ +z + coord = (self.x, self.y, from_bottom_z) + + return Location(coord, self) + + def top(self, z: float = 0.0 ): + from_top_z = self.top_ +z + coord = (self.x, self.y, from_top_z) + + return Location(coord, self) @dataclass(repr=False) @@ -111,7 +123,7 @@ def __init__(self, labware_filename: str, offset: Tuple[float] = None, order : s path :str = os.path.join(os.path.dirname(__file__), 'labware_definition')): # load in the labware configuration file - if labware_filename[-4] != 'json': + if labware_filename[-4:] != 'json': labware_filename = labware_filename + '.json' config_path = os.path.join( @@ -277,4 +289,69 @@ def withWellOrder(self, order) -> list: else: print('Order needs to be either rows or columns') - self.wells = ordered_wells \ No newline at end of file + self.wells = ordered_wells + +## Adapted from Opentrons API opentrons.types## +class Point(NamedTuple): + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 + + def add(self, other): + if not isinstance(other, Point): + return NotImplemented + return Point(self.x + other.x, self.y + other.y, self.z + other.z) + + def substract(self, other): + if not isinstance(other, Point): + return NotImplemented + return Point(self.x - other.x, self.y - other.y, self.z - other.z) + + def multiply(self, other: Union[int, float]): + if not isinstance(other, (float, int)): + return NotImplemented + return Point(self.x * other, self.y * other, self.z * other) + + def absolute(self): + return Point(abs(self.x), abs(self.y), abs(self.z)) + + def __repr__(self) -> str: + display= "x:{}, y: {}, z:{}".format(self.x, self.y, self.z) + return display + + +class Location: + """A location to target as a motion. + + The location contains a :py:class:`.Point` and possibly an associated + :py:class:`Labware` or :py:class:`Well` instance. + + """ + + def __init__(self, point: Point, labware: Union[Well, Labware]): + self._point = point + self._labware = labware + # todo(mm, 2021-10-01): Figure out how to get .point and .labware to show up + # in the rendered docs, and then update the class docstring to use cross-references. + + @property + def point(self) -> Point: + return self._point + + @property + def labware(self): + return self._labware + + def __iter__(self) -> Iterable[Union[Point, Well, Labware]]: + """Iterable interface to support unpacking. Like a tuple.""" + return iter(( self._point, self._labware)) + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, Location) + and other._point == self._point + and other._labware == self._labware + ) + + def __repr__(self) -> str: + return f"Location(point={repr(self._point)}, labware={self._labware})" diff --git a/science_jubilee/tools/Pipette.py b/science_jubilee/tools/Pipette.py index 80827f1..c3bd77a 100644 --- a/science_jubilee/tools/Pipette.py +++ b/science_jubilee/tools/Pipette.py @@ -3,7 +3,7 @@ import logging import os -from labware.Labware import Labware, Well +from labware.Labware import Labware, Well, Location from .Tool import Tool, ToolStateError, ToolConfigurationError from typing import Tuple, Union @@ -14,17 +14,17 @@ class Pipette(Tool): - def __init__(self, machine, index, name, tiprack, brand, model, max_volume, + def __init__(self, machine, index, name, brand, model, max_volume, min_volume, zero_position, blowout_position, drop_tip_position, mm_to_ul): #TODO:Removed machine from init, check if this should be asigned here or is added later - super().__init__(index, name, tiprack = tiprack, brand = brand, + super().__init__(index, name, brand = brand, model = model, max_volume = max_volume, min_volume = min_volume, zero_position = zero_position, blowout_position = blowout_position, drop_tip_position = drop_tip_position, mm_to_ul = mm_to_ul) self._machine = machine self.has_tip = False - self.is_active_tool = False + self.is_active_tool = False # TODO: add a way to change this to True/False and check before performing action with tool self.first_available_tip = None self.tool_offset = self._machine.tool_z_offsets[self.index] self.is_primed = False @@ -32,33 +32,27 @@ def __init__(self, machine, index, name, tiprack, brand, model, max_volume, @classmethod def from_config(cls, machine, index, name, config_file: str, - path :str = os.path.join(os.path.dirname(__file__), 'configs'), - tiprack: Labware = None): + path :str = os.path.join(os.path.dirname(__file__), 'configs')): config = os.path.join(path,config_file) with open(config) as f: kwargs = json.load(f) - kwargs['tiprack'] = tiprack #return cls(machine=machine, index=index, name=name, **kwargs) return cls(machine, index, name, **kwargs) + @staticmethod - def _getxyz(location: Union[Well, Tuple]): + def _getxyz(location: Union[Well, Tuple, Location]): if type(location) == Well: x, y, z = location.x, location.y, location.z elif type(location) == Tuple: x, y, z = location + elif type(location)==Location: + x,y,z= location._point else: raise ValueError("Location should be of type Well or Tuple") return x,y,z - - - @staticmethod - def _getTopBottom(location: Well): - top = location.top - bottom = location.bottom - return top, bottom - + def vol2move(self, vol): #Function that converts uL to movement """ @@ -75,7 +69,7 @@ def vol2move(self, vol): The corresponding v-axix movement for the desired volume of liquid """ - dv = vol / self.mm_to_ul + dv = vol * self.mm_to_ul # will need to change this value return dv @@ -103,8 +97,7 @@ def _aspirate(self, vol: float, s:int = 2000): self._machine.move_to(v=end_pos, s=s ) - def aspirate(self, vol: float, location : Union[Well, Tuple], s:int = 2000, - from_bottom :float =10, from_top :float = None): + def aspirate(self, vol: float, location : Union[Well, Tuple, Location], s:int = 2000): if self.has_tip is False: raise ToolStateError ("Error: tip needs to be attached before aspirating liquid") @@ -115,14 +108,12 @@ def aspirate(self, vol: float, location : Union[Well, Tuple], s:int = 2000, if type(location) == Well: self.current_well = location - - top, bottom = self._getTopBottom(location) - if from_bottom is not None : - z = bottom+ from_bottom - elif from_top is not None: - z = top + from_top - else: - pass + if z == location.z: + z= z+10 + elif type(location) == Location: + self.current_well = location._labware + else: + pass self._machine.safe_z_movement() self._machine.move_to(x=x, y=y) @@ -144,21 +135,20 @@ def _dispense(self,vol: float, s:int = 2000): # raise ToolStateError ("Error : The volume to be dispensed is greater than what was aspirated") self._machine.move_to(v = end_pos, s=s ) - def dispense(self, vol: float, location :Union[Well, Tuple], s:int = 2000, - from_bottom :float =10, from_top :float = None): + def dispense(self, vol: float, location :Union[Well, Tuple, Location], s:int = 2000): x, y, z = self._getxyz(location) if type(location) == Well: self.current_well = location - - top, bottom = self._getTopBottom(location) - if from_bottom is not None : - z = bottom+ from_bottom - elif from_top is not None: - z = top + from_top + if z == location.z: + z= z+10 else: pass + elif type(location) == Location: + self.current_well = location._labware + else: + pass self._machine.safe_z_movement() self._machine.move_to(x=x, y=y) @@ -166,17 +156,19 @@ def dispense(self, vol: float, location :Union[Well, Tuple], s:int = 2000, self._dispense(vol, s=s) - def transfer(self, vol: float, source_well: Union[Well, Tuple], - destination_well :Union[Well, Tuple] , s:int = 3000, + def transfer(self, vol: float, source_well: Union[Well, Tuple, Location], + destination_well :Union[Well, Tuple, Location] , s:int = 3000, blowout= None, mix_before: tuple = None, mix_after: tuple = None, new_tip : str = 'always'): #TODO: check that tip picked up and get a new one if not #TODO: ADD A Distance from bottom of well/top to dispense at - vol_ = self.vol2move(vol) -1 + vol_ = self.vol2move(vol) # get locations xs, ys, zs = self._getxyz(source_well) + if zs == source_well.z: + zs= zs+5 if self.is_primed == True: pass @@ -190,13 +182,21 @@ def transfer(self, vol: float, source_well: Union[Well, Tuple], if isinstance(destination_well, list): for well in destination_well: xd, yd, zd =self._getxyz(well) + if zd == well.z: + zd= zd+5 + else: + pass + # zd_top, zd_bottom = self._getTopBottom(well) self._machine.safe_z_movement() self._machine.move_to(x= xs, y=ys) - self._machine.move_to(z = zs+5) - self.current_well = source_well + self._machine.move_to(z = zs) + if type(source_well)== Well: + self.current_well = source_well + elif type(source_well)==Location: + self.current_well = source_well._labware self._aspirate(vol_, s=s) if mix_before: @@ -206,8 +206,11 @@ def transfer(self, vol: float, source_well: Union[Well, Tuple], self._machine.safe_z_movement() self._machine.move_to(x=xd, y=yd) - self._machine.move_to(z=zd+7) - self.current_well = well + self._machine.move_to(z=zd) + if type(well)==Well: + self.current_well = well + elif type(well)==Location: + self.current_well = well._labware self._dispense(vol_, s=s) if mix_after: @@ -229,7 +232,7 @@ def blowout(self, s : int = 3000): """ well = self.current_well - self._machine.move_to(z = well.top + 5 ) + self._machine.move_to(z = well.top_ + 5 ) self._machine.move_to(v = self.blowout_position, s=s) self.prime() @@ -239,23 +242,57 @@ def air_gap(self, vol): dv = self.vol2move(vol)*-1 well = self.current_well - self._machine.move_to(z = well.top + 20) + self._machine.move_to(z = well.top_ + 20) self._machine.move(v= -1*dv) def mix(self, vol: float, n: int, s: int = 5000): v = self.vol2move(vol)*-1 - self._machine.move_to(z = self.current_well.top+2) + self._machine.move_to(z = self.current_well.top_+2) self.prime() # self._machine.move(dz = -17) - self._machine.move_to(z= self.current_well.z-0.5) + # TODO: figure out a better way to indicate mixing height position that si tnot hardcoded + self._machine.move_to(z= self.current_well.z) for i in range(0,n): self._aspirate(vol, s=s) #self._machine.move_to(v=v, s=s) self.prime(s=s) +## In progress (2023-10-12) To test + def stir(self, n_times: int = 1, height: float= None): + + z= self.current_well.z + 0.5 # place pieptte tip close to the bottom + pos = self._machine.get_position() + x_ = float(pos['X']) + y_ = float(pos['Y']) + z_ = float(pos['Z']) + + # check position first + if x_ != round(self.current_well.x) and y_ != round(self.current_well.y, 2): + raise ToolStateError("Error: Pipette shuold be in a well before it can stir") + elif z_ != round(z,2): + self._machine.move_to(z=z) + + radius = self.current_well.diameter/2 -(self.current_well.diameter/6) # adjusted so that it does not hit the walls fo the well + + for n in range(n_times): + x_sp = self.current_well.x + y_sp = self.current_well.y + I = -1*radius + J = 0 # keeping same y so relative y difference is 0 + if height: + Z = z + height + self._machine.gcode(f'G2 X{x_sp} Y{y_sp} Z{Z} I{I} J{J}') + self._machine.gcode(f'M400') # wait until movement is completed + self._machine.move_to(z=z) + else: + self._machine.gcode(f'G2 X{x_sp} Y{y_sp} I{I} J{J}') + self._machine.gcode(f'M400') # wait until movement is completed + + + def update_z_offset(self, tip: bool = None): if isinstance(self.tiprack, list): @@ -269,7 +306,6 @@ def update_z_offset(self, tip: bool = None): new_z = self.tool_offset self._machine.gcode(f'G10 P{self.index} Z{new_z}') - # self._machine. def add_tiprack(self, tiprack: Union[Labware, list]): if isinstance(tiprack, list): @@ -333,7 +369,6 @@ def return_tip(self, location: Well = None): self.update_z_offset(tip=False) - def _drop_tip(self): """ Moves the plunger to eject the pipette tip diff --git a/science_jubilee/tools/WebCamera.py b/science_jubilee/tools/WebCamera.py index beed9e5..d9c4987 100644 --- a/science_jubilee/tools/WebCamera.py +++ b/science_jubilee/tools/WebCamera.py @@ -8,7 +8,7 @@ import matplotlib.pyplot as plt import numpy as np -from labware.Labware import Well +from labware.Labware import Well, Location from typing import Tuple, Union from .Tool import Tool @@ -38,11 +38,13 @@ def from_config(cls, machine, index, name, config_file: str, return cls(machine=machine, index=index, name=name,**kwargs) @staticmethod - def _getxyz(location: Union[Well, Tuple]): + def _getxyz(location: Union[Well, Tuple, Location]): if type(location) == Well: x, y, z = location.x, location.y, location.z elif type(location) == Tuple: x, y, z = location + elif type(location)== Location: + x,y,z= location._point else: raise ValueError("Location should be of type Well or Tuple") diff --git a/science_jubilee/tools/configs/P300.json b/science_jubilee/tools/configs/P300.json index 05e9ea5..961ad27 100644 --- a/science_jubilee/tools/configs/P300.json +++ b/science_jubilee/tools/configs/P300.json @@ -6,5 +6,5 @@ "zero_position": 310, "blowout_position": 350, "drop_tip_position": 425, - "mm_per_ul": 1.10 + "mm_per_ul": 0.91 } \ No newline at end of file diff --git a/science_jubilee/tools/configs/P300_config.json b/science_jubilee/tools/configs/P300_config.json index 0dc254e..90661d1 100644 --- a/science_jubilee/tools/configs/P300_config.json +++ b/science_jubilee/tools/configs/P300_config.json @@ -6,5 +6,5 @@ "zero_position": 310, "blowout_position": 350, "drop_tip_position": 425, - "mm_to_ul": 1.10 + "mm_to_ul": 0.91 } \ No newline at end of file