def _init_source_data(self): """ Initialises the source rack related values and lookups. """ for rack_pos in get_positions_for_shape( self.reservoir_specs.rack_shape): self._source_volumes[rack_pos] = 0 self._source_dead_volume = self.reservoir_specs.min_dead_volume \ * VOLUME_CONVERSION_FACTOR self._source_max_volume = self.reservoir_specs.max_volume \ * VOLUME_CONVERSION_FACTOR self.__emtpy_pos_manager = EmptyPositionManager( rack_shape=self.reservoir_specs.rack_shape)
def _init_source_data(self): """ Initialises the source rack related values and lookups. """ for rack_pos in get_positions_for_shape(self.reservoir_specs.rack_shape): self._source_volumes[rack_pos] = 0 self._source_dead_volume = self.reservoir_specs.min_dead_volume \ * VOLUME_CONVERSION_FACTOR self._source_max_volume = self.reservoir_specs.max_volume \ * VOLUME_CONVERSION_FACTOR self.__emtpy_pos_manager = EmptyPositionManager( rack_shape=self.reservoir_specs.rack_shape)
class SampleDilutionWorklistWriter(BiomekWorklistWriter): """ An abstract base class writing worklist for sample dilution worklists. **Return Value:** Stream for an CSV file. """ NAME = 'Biomek Dilution Worklist Writer' TRANSFER_TYPE = TRANSFER_TYPES.SAMPLE_DILUTION #: The name of the optional diluent info column. DILUENT_INFO_HEADER = 'DiluentInformation' #: The index of the optional diluent info column. DILUENT_INFO_INDEX = 5 def __init__(self, planned_worklist, target_rack, source_rack_barcode, reservoir_specs, pipetting_specs=None, ignored_positions=None, parent=None): """ Constructor. :param str source_rack_barcode: The barcode for the source rack or reservoir. :type source_rack_barcode: :class:`str` """ BiomekWorklistWriter.__init__(self, planned_worklist, target_rack, pipetting_specs=pipetting_specs, ignored_positions=ignored_positions, parent=parent) #: The barcode for the source rack or reservoir. self.source_rack_barcode = source_rack_barcode #: The specs for the source rack or reservoir. self.reservoir_specs = reservoir_specs #: The maximum volume of source rack container. self._source_max_volume = None #: Values for a column presenting diluent information. self._diluent_info_values = None #: Maps source position amounts (volumes) onto diluents. self._diluent_map = None #: Maps total diluent amounts (volumes) onto diluents. self._amount_map = None #: The last used source rack position. self.__last_source_rack_pos = None #: Stores and return empty positions in the source rack. self.__emtpy_pos_manager = None # If True, some dilution had to be split because they exceeded the # maximum allowed transfer volume. self.__has_split_volumes = False # If True, some source containers could not take up all liquid required # so that there is at least one additional source container for # an diluent. self.__split_sources = None def reset(self): """ Resets all values except for input values. """ BiomekWorklistWriter.reset(self) self._source_max_volume = None self._diluent_info_values = [] self._amount_map = dict() self._diluent_map = dict() self.__has_split_volumes = False self.__split_sources = set() self.__emtpy_pos_manager = None self.__last_source_rack_pos = None def _check_input(self): """ Checks the input values. """ BiomekWorklistWriter._check_input(self) self._check_input_class('source rack barcode', self.source_rack_barcode, basestring) self._check_input_class('reservoir specs', self.reservoir_specs, ReservoirSpecs) def _init_source_data(self): """ Initialises the source rack related values and lookups. """ for rack_pos in get_positions_for_shape(self.reservoir_specs.rack_shape): self._source_volumes[rack_pos] = 0 self._source_dead_volume = self.reservoir_specs.min_dead_volume \ * VOLUME_CONVERSION_FACTOR self._source_max_volume = self.reservoir_specs.max_volume \ * VOLUME_CONVERSION_FACTOR self.__emtpy_pos_manager = EmptyPositionManager( rack_shape=self.reservoir_specs.rack_shape) def _generate_column_values(self): """ This method generates the value lists for the CSV columns. """ target_rack_barcode = self.target_rack.barcode sorted_transfers = self.__get_sorted_transfers() for pt in sorted_transfers: if pt.target_position in self.ignored_positions: continue if not self._check_transfer_volume(pt.volume, pt.target_position): continue split_volumes = self.__split_into_transferable_amounts(pt) for i in range(len(split_volumes)): volume = round(split_volumes[i], 1) source_pos = self.__get_and_adjust_source_position( pt.diluent_info, volume, i) if self.has_errors(): break self._source_rack_values.append(self.source_rack_barcode) self._source_pos_values.append(source_pos.label) self._volume_values.append(get_trimmed_string(volume)) self._target_rack_values.append(target_rack_barcode) self._target_pos_values.append(pt.target_position.label) self._diluent_info_values.append(pt.diluent_info) if self.__has_split_volumes: msg = 'Some dilution volumes exceed the allowed maximum transfer ' \ 'volume of %s ul. The dilution volumes have been distributed ' \ 'over different source wells. Have a look on the generated ' \ 'worklist file for details, please.' \ % (get_trimmed_string(self._max_transfer_volume)) self.add_warning(msg) if len(self.__split_sources) > 0: msg = 'The source for the following diluents has been split ' \ 'and distributed over several containers because one ' \ 'single container could not have taken up the required ' \ 'volume (max volume of a source container: %s ul, dead ' \ 'volume: %s ul): %s. Have a look onto the generated ' \ 'worklist files for details, please.' \ % (get_trimmed_string(self.reservoir_specs.max_volume \ * VOLUME_CONVERSION_FACTOR), get_trimmed_string(self.reservoir_specs.max_dead_volume \ * VOLUME_CONVERSION_FACTOR), list(self.__split_sources)) self.add_warning(msg) def __get_sorted_transfers(self): # Sorts the planned transfers of the worklist by source position. target_positions = dict() for plt in self.planned_worklist: target_pos = plt.target_position target_positions[target_pos] = plt sorted_target_positions = sort_rack_positions(target_positions.keys()) sorted_transfers = [] for target_pos in sorted_target_positions: plt = target_positions[target_pos] sorted_transfers.append(plt) return sorted_transfers def __split_into_transferable_amounts(self, planned_transfer): # Checks whether the volume is larger than the allowed maximum # transfer volume and splits it into transferable amounts. volume = round(planned_transfer.volume * VOLUME_CONVERSION_FACTOR, 1) if volume <= self._max_transfer_volume: return [volume] self.__has_split_volumes = True no_volumes = ceil(volume / self._max_transfer_volume) amounts = [] partial_vol = round_up((volume / no_volumes), 1) while volume > 0: if volume < partial_vol: amounts.append(volume) volume = 0 else: amounts.append(partial_vol) volume = volume - partial_vol amounts.sort() return amounts def __get_and_adjust_source_position(self, diluent_info, volume, i): # Returns the source position for the diluent. diluent_marker = '%s#%i' % (diluent_info, i) if not self._diluent_map.has_key(diluent_marker): self.__init_new_diluent_source(diluent_marker, volume) else: source_pos = self._diluent_map[diluent_marker] stored_amount = self._amount_map[source_pos] new_amount = stored_amount + volume if new_amount > self._source_max_volume: self.__split_sources.add(str(diluent_info)) self.__init_new_diluent_source(diluent_marker, volume) else: self._amount_map[source_pos] = new_amount return self._diluent_map[diluent_marker] def __init_new_diluent_source(self, diluent_marker, volume): # Initialises a new source volume container in the source reservoir. try: source_pos = self.__emtpy_pos_manager.get_empty_position() except ValueError: msg = 'There is not enough space for all source containers ' \ 'in the source rack or reservoir!' self.add_error(msg) source_pos = None self._diluent_map[diluent_marker] = source_pos if not source_pos is None: self._amount_map[source_pos] = volume + self._source_dead_volume def _init_column_maps(self): """ Initialises the CsvColumnParameters object for the :attr:`_column_map_list`. """ BiomekWorklistWriter._init_column_maps(self) diluent_info_column = \ CsvColumnParameters.create_csv_parameter_map( self.DILUENT_INFO_INDEX, self.DILUENT_INFO_HEADER, self._diluent_info_values) self._column_map_list.append(diluent_info_column)
class SampleDilutionWorklistWriter(BiomekWorklistWriter): """ An abstract base class writing worklist for sample dilution worklists. **Return Value:** Stream for an CSV file. """ NAME = 'Biomek Dilution Worklist Writer' TRANSFER_TYPE = TRANSFER_TYPES.SAMPLE_DILUTION #: The name of the optional diluent info column. DILUENT_INFO_HEADER = 'DiluentInformation' #: The index of the optional diluent info column. DILUENT_INFO_INDEX = 5 def __init__(self, planned_worklist, target_rack, source_rack_barcode, reservoir_specs, pipetting_specs=None, ignored_positions=None, parent=None): """ Constructor. :param str source_rack_barcode: The barcode for the source rack or reservoir. :type source_rack_barcode: :class:`str` """ BiomekWorklistWriter.__init__(self, planned_worklist, target_rack, pipetting_specs=pipetting_specs, ignored_positions=ignored_positions, parent=parent) #: The barcode for the source rack or reservoir. self.source_rack_barcode = source_rack_barcode #: The specs for the source rack or reservoir. self.reservoir_specs = reservoir_specs #: The maximum volume of source rack container. self._source_max_volume = None #: Values for a column presenting diluent information. self._diluent_info_values = None #: Maps source position amounts (volumes) onto diluents. self._diluent_map = None #: Maps total diluent amounts (volumes) onto diluents. self._amount_map = None #: The last used source rack position. self.__last_source_rack_pos = None #: Stores and return empty positions in the source rack. self.__emtpy_pos_manager = None # If True, some dilution had to be split because they exceeded the # maximum allowed transfer volume. self.__has_split_volumes = False # If True, some source containers could not take up all liquid required # so that there is at least one additional source container for # an diluent. self.__split_sources = None def reset(self): """ Resets all values except for input values. """ BiomekWorklistWriter.reset(self) self._source_max_volume = None self._diluent_info_values = [] self._amount_map = dict() self._diluent_map = dict() self.__has_split_volumes = False self.__split_sources = set() self.__emtpy_pos_manager = None self.__last_source_rack_pos = None def _check_input(self): """ Checks the input values. """ BiomekWorklistWriter._check_input(self) self._check_input_class('source rack barcode', self.source_rack_barcode, basestring) self._check_input_class('reservoir specs', self.reservoir_specs, ReservoirSpecs) def _init_source_data(self): """ Initialises the source rack related values and lookups. """ for rack_pos in get_positions_for_shape( self.reservoir_specs.rack_shape): self._source_volumes[rack_pos] = 0 self._source_dead_volume = self.reservoir_specs.min_dead_volume \ * VOLUME_CONVERSION_FACTOR self._source_max_volume = self.reservoir_specs.max_volume \ * VOLUME_CONVERSION_FACTOR self.__emtpy_pos_manager = EmptyPositionManager( rack_shape=self.reservoir_specs.rack_shape) def _generate_column_values(self): """ This method generates the value lists for the CSV columns. """ target_rack_barcode = self.target_rack.barcode sorted_transfers = self.__get_sorted_transfers() for pt in sorted_transfers: if pt.target_position in self.ignored_positions: continue if not self._check_transfer_volume(pt.volume, pt.target_position): continue split_volumes = self.__split_into_transferable_amounts(pt) for i in range(len(split_volumes)): volume = round(split_volumes[i], 1) source_pos = self.__get_and_adjust_source_position( pt.diluent_info, volume, i) if self.has_errors(): break self._source_rack_values.append(self.source_rack_barcode) self._source_pos_values.append(source_pos.label) self._volume_values.append(get_trimmed_string(volume)) self._target_rack_values.append(target_rack_barcode) self._target_pos_values.append(pt.target_position.label) self._diluent_info_values.append(pt.diluent_info) if self.__has_split_volumes: msg = 'Some dilution volumes exceed the allowed maximum transfer ' \ 'volume of %s ul. The dilution volumes have been distributed ' \ 'over different source wells. Have a look on the generated ' \ 'worklist file for details, please.' \ % (get_trimmed_string(self._max_transfer_volume)) self.add_warning(msg) if len(self.__split_sources) > 0: msg = 'The source for the following diluents has been split ' \ 'and distributed over several containers because one ' \ 'single container could not have taken up the required ' \ 'volume (max volume of a source container: %s ul, dead ' \ 'volume: %s ul): %s. Have a look onto the generated ' \ 'worklist files for details, please.' \ % (get_trimmed_string(self.reservoir_specs.max_volume \ * VOLUME_CONVERSION_FACTOR), get_trimmed_string(self.reservoir_specs.max_dead_volume \ * VOLUME_CONVERSION_FACTOR), list(self.__split_sources)) self.add_warning(msg) def __get_sorted_transfers(self): # Sorts the planned transfers of the worklist by source position. target_positions = dict() for plt in self.planned_worklist: target_pos = plt.target_position target_positions[target_pos] = plt sorted_target_positions = sort_rack_positions(target_positions.keys()) sorted_transfers = [] for target_pos in sorted_target_positions: plt = target_positions[target_pos] sorted_transfers.append(plt) return sorted_transfers def __split_into_transferable_amounts(self, planned_transfer): # Checks whether the volume is larger than the allowed maximum # transfer volume and splits it into transferable amounts. volume = round(planned_transfer.volume * VOLUME_CONVERSION_FACTOR, 1) if volume <= self._max_transfer_volume: return [volume] self.__has_split_volumes = True no_volumes = ceil(volume / self._max_transfer_volume) amounts = [] partial_vol = round_up((volume / no_volumes), 1) while volume > 0: if volume < partial_vol: amounts.append(volume) volume = 0 else: amounts.append(partial_vol) volume = volume - partial_vol amounts.sort() return amounts def __get_and_adjust_source_position(self, diluent_info, volume, i): # Returns the source position for the diluent. diluent_marker = '%s#%i' % (diluent_info, i) if not self._diluent_map.has_key(diluent_marker): self.__init_new_diluent_source(diluent_marker, volume) else: source_pos = self._diluent_map[diluent_marker] stored_amount = self._amount_map[source_pos] new_amount = stored_amount + volume if new_amount > self._source_max_volume: self.__split_sources.add(str(diluent_info)) self.__init_new_diluent_source(diluent_marker, volume) else: self._amount_map[source_pos] = new_amount return self._diluent_map[diluent_marker] def __init_new_diluent_source(self, diluent_marker, volume): # Initialises a new source volume container in the source reservoir. try: source_pos = self.__emtpy_pos_manager.get_empty_position() except ValueError: msg = 'There is not enough space for all source containers ' \ 'in the source rack or reservoir!' self.add_error(msg) source_pos = None self._diluent_map[diluent_marker] = source_pos if not source_pos is None: self._amount_map[source_pos] = volume + self._source_dead_volume def _init_column_maps(self): """ Initialises the CsvColumnParameters object for the :attr:`_column_map_list`. """ BiomekWorklistWriter._init_column_maps(self) diluent_info_column = \ CsvColumnParameters.create_csv_parameter_map( self.DILUENT_INFO_INDEX, self.DILUENT_INFO_HEADER, self._diluent_info_values) self._column_map_list.append(diluent_info_column)