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))
Beispiel #4
0
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
Beispiel #5
0
    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)
Beispiel #6
0
 def update_calibrator(self):
     self.calibrator = Calibrator(self.robot._deck, self.calibration_data)
Beispiel #7
0
    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()