def test_convert(self): deck = self.generate_deck() calibration_data = { 'A1': { 'type': 'Slot', 'delta': (1, 1, 1), 'children': { 'tube_rack': { 'type': 'tube-rack-2ml', 'delta': (1, 1, 1), 'children': { 'Red': { 'type': 'Well', 'delta': (1, 1, 1) }, 'Blue': { 'type': 'Well', 'delta': (2, 2, 2) } } } } } } my_calibrator = Calibrator(deck, calibration_data) res = my_calibrator.convert(deck['A1']['tube_rack']['Blue'], deck['A1']['tube_rack']['Blue'].center()) self.assertEqual(res, (29.0, 24.0, 4.0)) res = my_calibrator.convert(deck['A1']['tube_rack']) self.assertEqual(res, (7, 12, 2))
def test_calibrate(self): deck = self.generate_deck() calibration_data = {} my_calibrator = Calibrator(deck, calibration_data) current_position = (14, 19, -1) tube_rack = deck['A1']['tube_rack'] expected = tube_rack['Red'].center(tube_rack) new_calibration_data = my_calibrator.calibrate( calibration_data, (deck['A1']['tube_rack'], expected), current_position) expected_result = { 'A1': { 'children': { 'tube_rack': { 'delta': (-1.0, -1.0, -1.0), 'type': 'Container' } } } } self.assertDictEqual(new_calibration_data, expected_result) red = deck['A1']['tube_rack']['Red'] self.assertEqual( my_calibrator.convert(red) + red.center(), current_position)
def test_apply_calibration(self): deck = self.generate_deck() calibration_data = { 'A1': { 'type': 'Slot', 'delta': (1, 1, 1), 'children': { 'tube_rack': { 'type': 'tube-rack-2ml', 'delta': (1, 1, 1), 'children': { 'Red': { 'type': 'Well', 'delta': (1, 1, 1) }, 'Blue': { 'type': 'Well', 'delta': (2, 2, 2) } } } } } } my_calibrator = Calibrator(deck, calibration_data) red = deck['A1']['tube_rack']['Red'] blue = deck['A1']['tube_rack']['Blue'] self.assertEqual(my_calibrator.convert(red), (13, 18, 3)) self.assertEqual(my_calibrator.convert(blue), (24, 19, 4))
class Instrument(object): """ This class represents instrument attached to the :any:`Robot`: :Pipette:, :Magbead:. It gives the instruments ability to CRUD their calibration data, and gives access to some common methods across instruments """ calibration_key = "unique_name" persisted_attributes = [] persisted_defaults = {} calibrator = Calibrator(Robot()._deck, {}) def reset(self): """ Placeholder for instruments to reset their state between runs """ pass def setup_simulate(self, *args, **kwargs): """ Placeholder for instruments to prepare their state for simulation """ pass def teardown_simulate(self, *args, **kwargs): """ Placeholder for instruments to reverse :meth:`setup_simulate` """ pass def create_command(self, do, setup=None, description=None, enqueue=True): """ Creates an instance of Command to be appended to the :any:`Robot` run queue. Parameters ---------- do : callable The method to execute on the robot. This usually includes moving an instrument's motors, or the robot head setup : callable The method to execute just before `do()`, which includes updating the instrument's state description : str Human-readable description of the action taking place enqueue : bool If set to `True` (default), the method will be appended to the robots list of commands for executing during :any:`run` or :any:`simulate`. If set to `False`, the method will skip the command queue and execute immediately Examples -------- .. >>> instrument = Instrument() >>> def setup(): >>> print('hello') >>> def do(): >>> print(' world') >>> description = 'printing "hello world"' >>> instrument.create_command(do, setup, description) hello >>> robot.simulate() hello world >>> instrument.create_command(do, setup, description, enqueue=False) hello world """ command = Command(do=do, setup=setup, description=description) if enqueue: Robot().add_command(command) else: command() def init_calibrations(self, key, attributes=None): """ Creates empty calibrations data if not already present Parameters ---------- key : str The unique string to save this instrument's calibation data attributes : list A list of this instrument's attribute names to be saved """ self.calibration_key = key if isinstance(attributes, list): self.persisted_attributes = attributes for key in attributes: self.persisted_defaults[key] = copy.copy(getattr(self, key)) if not os.path.isdir(self._get_calibration_dir()): os.mkdir(self._get_calibration_dir()) file_path = self._get_calibration_file_path() if not os.path.isfile(file_path): with open(file_path, 'a') as f: f.write(json.dumps({})) def update_calibrations(self): """ Saves the instrument's peristed attributes to file """ last_persisted_data = self._read_calibrations() last_persisted_data[self.calibration_key] = (self._strip_vector( self._build_calibration_data())) last_persisted_data = self._strip_vector(last_persisted_data) with open(self._get_calibration_file_path(), 'w') as f: f.write(json.dumps(last_persisted_data, indent=4)) def load_persisted_data(self): """ Loads and sets the instrument's peristed attributes from file """ last_persisted_data = self._get_calibration() if last_persisted_data: last_persisted_data = self._restore_vector(last_persisted_data) for key, val in last_persisted_data.items(): setattr(self, key, val) def delete_calibration_data(self): """ Set the instrument's properties to their initialized values, and saves those initialized values to file """ for key, val in self.persisted_defaults.items(): setattr(self, key, val) self.update_calibrations() def delete_calibration_file(self): """ Deletes the entire calibrations file """ file_path = self._get_calibration_file_path() if os.path.exists(file_path): os.remove(file_path) def _get_calibration_dir(self): """ :return: the directory to save calibration data """ DATA_DIR = os.environ.get('APP_DATA_DIR') or os.getcwd() return os.path.join(DATA_DIR, CALIBRATIONS_FOLDER) def _get_calibration_file_path(self): """ :return: the absolute file path of the calibration file """ return os.path.join(self._get_calibration_dir(), CALIBRATIONS_FILE) def _get_calibration(self): """ :return: this instrument's saved calibrations data """ return self._read_calibrations().get(self.calibration_key) def _build_calibration_data(self): """ :return: copy of this instrument's persisted attributes """ calibration = {} for attr in self.persisted_attributes: calibration[attr] = copy.copy(getattr(self, attr)) return calibration def _read_calibrations(self): """ Reads calibration data from file system. :return: json of calibration data """ with open(self._get_calibration_file_path()) as f: try: loaded_json = json.load(f) except json.decoder.JSONDecodeError: loaded_json = {} return self._restore_vector(loaded_json) def _strip_vector(self, obj, root=True): """ Iterates through a dictionary, converting Vector classes to serializable dictionaries :return: json of calibration data """ obj = (copy.deepcopy(obj) if root else obj) for key, val in obj.items(): if isinstance(val, Vector): res = json.dumps(val, cls=VectorEncoder) obj[key] = res elif isinstance(val, dict): self._strip_vector(val, root=False) return obj def _restore_vector(self, obj, root=True): """ Iterates through a dictionary, converting serializable Vector dictionaries to Vector classes :return: json of calibration data """ obj = (copy.deepcopy(obj) if root else obj) for key, val in obj.items(): if isinstance(val, dict): self._restore_vector(val, root=False) elif isinstance(val, str): try: res = Vector(json.loads(val)) obj[key] = res except JSON_ERROR: pass return obj
def __init__(self, robot, axis, name=None, channels=1, min_volume=0, max_volume=None, trash_container=None, tip_racks=[], aspirate_speed=300, dispense_speed=500): self.robot = robot self.axis = axis self.channels = channels if not name: name = self.__class__.__name__ self.name = name if isinstance(trash_container, Container) and len(trash_container) > 0: trash_container = trash_container[0] self.trash_container = trash_container self.tip_racks = tip_racks self.starting_tip = None # default mm above tip to execute drop-tip # this gives room for the drop-tip mechanism to work self._drop_tip_offset = 15 self.reset_tip_tracking() self.robot.add_instrument(self.axis, self) self.placeables = [] self.previous_placeable = None self.current_volume = 0 self.speeds = {'aspirate': aspirate_speed, 'dispense': dispense_speed} self.min_volume = min_volume self.max_volume = max_volume or (min_volume + 1) # FIXME default_positions = { 'top': 0.0101, 'bottom': 10.0101, 'blow_out': 12.0101, 'drop_tip': 14.0101 } self.positions = {} self.positions.update(default_positions) self.calibrated_positions = copy.deepcopy(default_positions) self.calibration_data = {} # Pipette properties to persist between sessions persisted_attributes = ['calibration_data', 'positions', 'max_volume'] persisted_key = '{axis}:{name}'.format(axis=self.axis, name=self.name) self.init_calibrations(key=persisted_key, attributes=persisted_attributes) self.load_persisted_data() for key, val in self.positions.items(): if val is None: self.positions[key] = default_positions[key] self.calibrator = Calibrator(self.robot._deck, self.calibration_data)
def update_calibrator(self): self.calibrator = Calibrator(self.robot._deck, self.calibration_data)
def __init__(self, axis, name=None, channels=1, min_volume=0, max_volume=None, trash_container=None, tip_racks=[], aspirate_speed=300, dispense_speed=500): self.axis = axis self.channels = channels if not name: name = self.__class__.__name__ self.name = name self.trash_container = trash_container self.tip_racks = tip_racks # default mm above tip to execute drop-tip # this gives room for the drop-tip mechanism to work self._drop_tip_offset = 15 self.reset_tip_tracking() self.robot = Robot.get_instance() self.robot.add_instrument(self.axis, self) self.motor = self.robot.get_motor(self.axis) self.placeables = [] self.previous_placeable = None self.current_volume = 0 self.speeds = {'aspirate': aspirate_speed, 'dispense': dispense_speed} self.min_volume = min_volume self.max_volume = max_volume or (min_volume + 1) self.positions = { 'top': None, 'bottom': None, 'blow_out': None, 'drop_tip': None } self.calibrated_positions = copy.deepcopy(self.positions) self.calibration_data = {} # Pipette properties to persist between sessions persisted_attributes = ['calibration_data', 'positions', 'max_volume'] persisted_key = '{axis}:{name}'.format(axis=self.axis, name=self.name) self.init_calibrations(key=persisted_key, attributes=persisted_attributes) self.load_persisted_data() self.calibrator = Calibrator(self.robot._deck, self.calibration_data) # if the user passed an initialization value, # overwrite the loaded persisted data with it if isinstance(max_volume, (int, float, complex)) and max_volume > 0: self.max_volume = max_volume self.update_calibrations()