def __create_stock_rack_worklist_series(self): # The transfer for each worklist series are derived from the stock # rack layouts. ticket_number = self._iso_request.experiment_metadata.ticket_number robot_specs = self._get_stock_transfer_pipetting_specs() for sr_marker in sorted(self._stock_rack_layouts.keys()): sr_layout = self._stock_rack_layouts[sr_marker] worklist_series = WorklistSeries() for rack_marker in self.__get_sorted_plate_markers(): transfers = [] for sr_pos in sr_layout.working_positions(): psts = sr_pos.get_planned_sample_transfers(rack_marker) transfers.extend(psts) if len(transfers) < 1: continue wl_index = len(worklist_series) wl_label = LABELS.create_worklist_label( ticket_number, worklist_number=(wl_index + 1), target_rack_marker=rack_marker, source_rack_marker=sr_marker) worklist = PlannedWorklist( label=wl_label, transfer_type=TRANSFER_TYPES.SAMPLE_TRANSFER, planned_liquid_transfers=transfers, pipetting_specs=robot_specs) worklist_series.add_worklist(wl_index, worklist) self.__stock_transfer_series[sr_marker] = worklist_series
def __create_stock_rack_worklist_series(self): # The transfer for each worklist series are derived from the stock # rack layouts. ticket_number = self._iso_request.experiment_metadata.ticket_number robot_specs = self._get_stock_transfer_pipetting_specs() for sr_marker in sorted(self._stock_rack_layouts.keys()): sr_layout = self._stock_rack_layouts[sr_marker] worklist_series = WorklistSeries() for rack_marker in self.__get_sorted_plate_markers(): transfers = [] for sr_pos in sr_layout.working_positions(): psts = sr_pos.get_planned_sample_transfers(rack_marker) transfers.extend(psts) if len(transfers) < 1: continue wl_index = len(worklist_series) wl_label = LABELS.create_worklist_label(ticket_number, worklist_number=(wl_index + 1), target_rack_marker=rack_marker, source_rack_marker=sr_marker) worklist = PlannedWorklist(label=wl_label, transfer_type=TRANSFER_TYPES.SAMPLE_TRANSFER, planned_liquid_transfers=transfers, pipetting_specs=robot_specs) worklist_series.add_worklist(wl_index, worklist) self.__stock_transfer_series[sr_marker] = worklist_series
def run(self): self.reset() self.add_info('Generate worklist series ...') self.__check_input() if not self.has_errors(): self.__series_map[self.ISO_KEY] = WorklistSeries() self.__series_map[self.JOB_KEY] = WorklistSeries() self._sort_layouts() self.__create_buffer_worklists() self.__create_transfer_worklists() if not self.has_errors(): self.return_value = self.__series_map self.add_info('Worklist series generation completed.')
def run(self): self.reset() self.add_info('Start worklist generation ...') self.__check_input() if not self.has_errors(): self.__sort_into_sectors() if not self.has_errors(): self.__worklist_series = WorklistSeries() self.__create_stock_rack_buffer_worklists() self.__create_source_plate_buffer_worklists() self.__create_stock_to_prep_worklists() self.__create_prep_to_aliquot_worklist() if not self.has_errors(): self.return_value = self.__worklist_series self.add_info('Worklist generation completed.')
def __make_stock_rack_worklist_series(self, label, volume, layout): # Builds a sector 0 -> sector 0 rack transfer worklist series. pip_specs_cy = get_pipetting_specs_cybio() wl_series = WorklistSeries() psts = [] if layout is None: # Rack transfer (only for pool stock rack -> prep plate transfer). pst = PlannedRackSampleTransfer.get_entity(volume, 1, 0, 0) psts.append(pst) pwl_type = TRANSFER_TYPES.RACK_SAMPLE_TRANSFER else: # Sample transfers (for single stock transfer). for rack_pos in layout.get_positions(): pst = PlannedSampleTransfer.get_entity(volume, rack_pos, rack_pos) psts.append(pst) pwl_type = TRANSFER_TYPES.SAMPLE_TRANSFER wl = PlannedWorklist(label, pwl_type, pip_specs_cy, planned_liquid_transfers=psts) wl_series.add_worklist(0, wl) return wl_series
def run(self): self.reset() self.add_info("Start worklist generation ...") self.__check_input() if not self.has_errors(): self.__sort_into_sectors() if not self.has_errors(): self.__worklist_series = WorklistSeries() if not self.pool_buffer_volume is None: self.__create_pool_rack_buffer_worklists() if not self.has_errors(): self.__create_preparation_plate_buffer_worklists() if not self.has_errors(): self.__create_prep_to_aliquot_worklist() if not self.has_errors(): self.return_value = self.__worklist_series self.add_info("Worklist generation completed.")
def __generate_cell_plate_worklist_for_experiment_design( self, transfer_index, cell_index): # Generates the cell plate worklists for the experiment design as # storage location. self.add_debug('Create cell plate worklists for experiment design ...') if self.__design_series is None: self.__design_series = WorklistSeries() transfer_generator = _CybioTransferWorklistGenerator(self.label, parent=self) self.__generate_transfer_worklist(transfer_generator, transfer_index, self.__design_series) for rack_pos, tf_pos in self.source_layout.iterpositions(): tf_pos.cell_plate_positions = [rack_pos] cell_generator = _CellSuspensionWorklistGenerator(self.label, self.source_layout, parent=self) self.__generate_cell_worklist(cell_generator, cell_index, self.__design_series)
def __generate_cell_plate_worklist_for_racks(self, transfer_index, cell_index): # Generates the cell plate worklists for the experiment design racks # as storage locations. self.add_debug('Create cell plate worklists for design racks ...') for design_rack in self.experiment_design.experiment_design_racks: worklist_series = WorklistSeries() completed_layout = self.design_rack_associations[design_rack.label] label = '%s-%s' % (self.label, design_rack.label) transfer_generator = \ _BiomekTransferWorklistGenerator(label, completed_layout, parent=self) self.__generate_transfer_worklist(transfer_generator, transfer_index, worklist_series) cell_generator = _CellSuspensionWorklistGenerator(label, completed_layout, parent=self) self.__generate_cell_worklist(cell_generator, cell_index, worklist_series) design_rack.worklist_series = worklist_series
class StockSampleCreationWorklistGenerator(BaseTool): """ Creates the worklist series containing the worklists involved in pool stock sample creation. This comprises only one worklist which deals with the addition of buffer to the future pool stock tubes. The tool will create planned sample dilutions for all positions of a 8x12 rack shape. **Return Value:** worklist series (:class:`thelma.entities.liquidtransfer.WorklistSeries`). """ NAME = 'Pool Creation Worklist Generator' #: The index for the buffer worklist within the series. BUFFER_WORKLIST_INDEX = 0 def __init__(self, volume_calculator, iso_request_label, parent=None): """ Constructor. :param volume_calculator: Determines transfer and dilution volumes for pool stock sample ISO requests. :type volume_calculator: :class:`VolumeCalculator` :param int number_designs: The number of single molecule designs for each pool (positive number). :param int target_volume: The final volume for the new pool stock tubes in ul (positive number). :param int target_concentration: The final pool concentration for the new pool stock tubes in nM (positive number). :param str iso_request_label: The plate set label of the ISO request to be created - will be used as part of the worklist name. """ BaseTool.__init__(self, parent=parent) #: The plate set label of the ISO request to be created - will be used #: as part of the worklist name. self.iso_request_label = iso_request_label #: The :class:`VolumeCalculator` determines transfer and buffer volumes #: and might also adjust the target volume for the ISO request. self.volume_calculator = volume_calculator #: The worklist series for the ISO request. self.__worklist_series = None def reset(self): BaseTool.reset(self) self.__worklist_series = WorklistSeries() def run(self): self.reset() self.add_info('Start worklist series generation ...') self.__check_input() if not self.has_errors(): self.__create_transfers() if not self.has_errors(): self.return_value = self.__worklist_series self.add_info('Worklist series generation completed.') def __check_input(self): """ Checks the initialisation values. """ self.add_debug('Check input ...') self._check_input_class('volume calculator', self.volume_calculator, VolumeCalculator) self._check_input_class('ISO request label', self.iso_request_label, basestring) def __create_transfers(self): # Creates a :class:`PlannedSampleDilution` for each rack position # in a 8x12 rack shape. self.add_debug('Create transfers ...') self._run_and_record_error( self.volume_calculator.calculate, 'Error when trying to determine buffer volume: ', ValueError) buffer_volume = self.volume_calculator.get_buffer_volume() if buffer_volume is not None: volume = buffer_volume / VOLUME_CONVERSION_FACTOR wl_label = LABELS.create_buffer_worklist_label( self.iso_request_label) wl = PlannedWorklist(wl_label, TRANSFER_TYPES.SAMPLE_DILUTION, get_pipetting_specs_cybio()) for rack_pos in get_positions_for_shape(RACK_SHAPE_NAMES.SHAPE_96): psd = PlannedSampleDilution.get_entity( volume=volume, target_position=rack_pos, diluent_info=DILUENT_INFO) wl.planned_liquid_transfers.append(psd) self.__worklist_series.add_worklist(self.BUFFER_WORKLIST_INDEX, wl)
class LibraryCreationWorklistGenerator(BaseTool): """ Creates the worklist series for containing the worklists involved in library creation. 1. buffer addition into the pool sample stock racks (1 for each quadrant) 2. buffer addition into preparation plates (1 for each quadrant) 3. rack transfers from normal stock racks into pool stock racks (as usually the take out worklists are not stored as part of this worklist series but at the sample stock racks as container transfer worklist to allow for container tracking) 4. rack transfer from stock rack to preparation plate 5. rack transfer from preparation plates to aliquot plate **Return Value:** worklist series (:class:`thelma.entities.liquidtransfer.WorklistSeries`). """ NAME = 'Library Creation Worklist Generator' #: Name pattern for the worklists that add annealing buffer to the pool #: stock racks. The placeholders will contain the library name and the #: quadrant sector. LIBRARY_STOCK_BUFFER_WORKLIST_LABEL = '%s_stock_buffer_Q%i' #: Name pattern for the worklists that add annealing buffer to the pool #: preparation plates. The placeholders will contain the library name and #: the quadrant sector. LIBRARY_PREP_BUFFER_WORKLIST_LABEL = '%s_prep_buffer_Q%i' #: Name pattern for the worklists that transfers the pool from the pool #: stock rack to the preparation plate. The placeholder will contain the #: library name. STOCK_TO_PREP_TRANSFER_WORKLIST_LABEL = '%s_stock_to_prep' #: Name pattern for the worklists that transfers the pool from the #: preparation plate to the final library aliqut plate. The placeholder will #: contain the library name. PREP_TO_ALIQUOT_TRANSFER_WORKLIST_LABEL = '%s_prep_to_aliquot' #: The dilution info for the dilution worklists. DILUTION_INFO = 'annealing buffer' def __init__(self, base_layout, stock_concentration, library_name, parent=None): """ Constructor. :param base_layout: The layout defining which positions of the layout are allowed to take up library samples. :type base_layout: :class:`LibraryBaseLayout` :param int stock_concentration: The concentration of the single source molecule designs in the stock in nM (positive number). :param str library_name: The name of the library to be created. """ BaseTool.__init__(self, parent=parent) #: Defines which positions of the layout are allowed to take up #: library samples. self.base_layout = base_layout #: The concentration of the single source molecule designs in the #: stock in nM. self.stock_concentration = stock_concentration #: The name of the library to be created. self.library_name = library_name #: The worklist series for the ISO request. self.__worklist_series = None #: The last used worklist index (within the series). self.__last_worklist_index = None #: The base layout for each quadrant. self.__quadrant_layouts = None #: The volume transferred from the pool stock rack to the preparation #: plate. self.__stock_to_prep_vol = None def reset(self): BaseTool.reset(self) self.__worklist_series = None self.__last_worklist_index = -1 self.__quadrant_layouts = dict() self.__stock_to_prep_vol = None def run(self): self.reset() self.add_info('Start worklist generation ...') self.__check_input() if not self.has_errors(): self.__sort_into_sectors() if not self.has_errors(): self.__worklist_series = WorklistSeries() self.__create_stock_rack_buffer_worklists() self.__create_source_plate_buffer_worklists() self.__create_stock_to_prep_worklists() self.__create_prep_to_aliquot_worklist() if not self.has_errors(): self.return_value = self.__worklist_series self.add_info('Worklist generation completed.') def __check_input(self): # Checks the initialisation values. self.add_debug('Check input ...') self._check_input_class('base library layout', self.base_layout, LibraryBaseLayout) self._check_input_class('library name', self.library_name, basestring) if not is_valid_number(self.stock_concentration): msg = 'The stock concentration for the single source molecules ' \ 'must be a positive number (obtained: %s).' \ % (self.stock_concentration) self.add_error(msg) def __sort_into_sectors(self): # Create a rack layout for each quadrant. self.add_debug('Sort positions into sectors ...') quadrant_positions = QuadrantIterator.sort_into_sectors( self.base_layout, NUMBER_SECTORS) rack_shape_96 = get_96_rack_shape() for sector_index, positions in quadrant_positions.iteritems(): if len(positions) < 1: continue base_layout = LibraryBaseLayout(shape=rack_shape_96) for pos in positions: base_layout.add_position(pos) if len(base_layout) > 0: self.__quadrant_layouts[sector_index] = base_layout if len(self.__quadrant_layouts) < NUMBER_SECTORS: missing_sectors = [] for sector_index in range(NUMBER_SECTORS): if not self.__quadrant_layouts.has_key(sector_index): missing_sectors.append(str(sector_index + 1)) msg = 'Some rack sectors are empty. You do not require stock ' \ 'racks for them: %s!' % (', '.join(missing_sectors)) self.add_warning(msg) def __create_stock_rack_buffer_worklists(self): # These worklists are responsible for the addition of annealing buffer # to the pool stock rack. There is 1 worklist for each quadrant. self.add_debug('Create stock rack buffer worklists ...') buffer_volume = get_stock_pool_buffer_volume() for sector_index, base_layout in self.__quadrant_layouts.iteritems(): label = self.LIBRARY_STOCK_BUFFER_WORKLIST_LABEL % ( self.library_name, (sector_index + 1)) self.__create_buffer_worklist(base_layout, buffer_volume, label, sector_index) def __create_source_plate_buffer_worklists(self): # These worklists are responsible for the addition of annealing # buffer to the pool preparation plate. There is 1 worklist for each # quadrant. self.add_debug('Create preparation plate buffer worklist ...') self.__stock_to_prep_vol = get_source_plate_transfer_volume() buffer_volume = PREPARATION_PLATE_VOLUME - self.__stock_to_prep_vol for sector_index, base_layout in self.__quadrant_layouts.iteritems(): label = self.LIBRARY_PREP_BUFFER_WORKLIST_LABEL % ( self.library_name, (sector_index + 1)) self.__create_buffer_worklist(base_layout, buffer_volume, label, sector_index) def __create_buffer_worklist(self, quadrant_layout, buffer_volume, label, sector_index): # Creates buffer dilutions worklist for a particular quadrant # and adds it to the worklist series. volume = buffer_volume / VOLUME_CONVERSION_FACTOR planned_transfers = [] translator = RackSectorTranslator( number_sectors=NUMBER_SECTORS, source_sector_index=sector_index, target_sector_index=0, enforce_type=RackSectorTranslator.ONE_TO_MANY) for rack_pos_384 in quadrant_layout.get_positions(): rack_pos_96 = translator.translate(rack_pos_384) planned_transfer = PlannedSampleDilution.get_entity( volume, self.DILUTION_INFO, rack_pos_96) planned_transfers.append(planned_transfer) worklist = PlannedWorklist(label=label, planned_transfers=planned_transfers) self.__last_worklist_index += 1 self.__worklist_series.add_worklist(self.__last_worklist_index, worklist) def __create_stock_to_prep_worklists(self): # This rack transfer worklist (transfer from pool stock rack to # preparation plate) is executed once for each quadrant. self.add_debug('Add worklist for transfer to preparation plate ...') label = self.STOCK_TO_PREP_TRANSFER_WORKLIST_LABEL \ % (self.library_name) volume = self.__stock_to_prep_vol / VOLUME_CONVERSION_FACTOR rack_transfer = PlannedRackSampleTransfer.get_entity(volume, 1, 0, 0) worklist = PlannedWorklist(label=label, planned_transfers=[rack_transfer]) self.__last_worklist_index += 1 self.__worklist_series.add_worklist(self.__last_worklist_index, worklist) def __create_prep_to_aliquot_worklist(self): # There is one rack transfer for each sector (many-to-one transfer). # Each transfer is executed once per aliquot plate. self.add_debug('Add worklist for transfer into aliquot plates ...') volume = ALIQUOT_PLATE_VOLUME / VOLUME_CONVERSION_FACTOR rack_transfers = [] for sector_index in self.__quadrant_layouts.keys(): rack_transfer = PlannedRackSampleTransfer.get_entity( volume, NUMBER_SECTORS, 0, sector_index) rack_transfers.append(rack_transfer) label = self.PREP_TO_ALIQUOT_TRANSFER_WORKLIST_LABEL % ( self.library_name) worklist = PlannedWorklist(label=label, planned_transfers=rack_transfers) self.__last_worklist_index += 1 self.__worklist_series.add_worklist(self.__last_worklist_index, worklist)
def reset(self): LayoutParserHandler.reset(self) self.__worklist_series = WorklistSeries() self.__rack_agg = get_root_aggregate(IRack) self.__missing_rack_barcodes = []
class GenericSampleTransferPlanParserHandler(LayoutParserHandler): """ Converts the data from the :class:`GenericSampleTransferPlanParser` into a :class:`WorklistSeries`. **Return Value:** :class:`WorklistSeries` """ NAME = 'Generic Sample Transfer Plan Parser Handler' _PARSER_CLS = GenericSampleTransferPlanParser def __init__(self, stream, allow_rack_creation=False, parent=None): """ Constructor. :param bool allow_rack_creation: Flag indicating if it is allowed to create racks. This option can only be set if the racks are not used for worklist printing or execution right away (because the barcode is generated by the DB upon persisting). :default allow_rack_creation: *False* """ LayoutParserHandler.__init__(self, stream, parent=parent) #: Is it allowed to create racks? This option can only be set to *True* #: if the racks are not used for worklist printing or execution right #: away (because the barcode is generated by the DB upon persisting). self.allow_rack_creation = allow_rack_creation #: The :class:`WorklistSeries` to be generated. self.__worklist_series = None #: The :class:`RackOrReservoirItem` objects mapped onto rack identifiers. self.__ror_map = dict() #: The aggregate used to fetch racks. self.__rack_agg = None #: Intermediate error storage. self.__missing_rack_barcodes = None #: Intermediate error storage. self.__missing_rack_barcodes = None def reset(self): LayoutParserHandler.reset(self) self.__worklist_series = WorklistSeries() self.__rack_agg = get_root_aggregate(IRack) self.__missing_rack_barcodes = [] def get_racks_and_reservoir_items(self): """ Returns the :class:`RackOrReservoirItem` objects found in the sheet. """ return self._get_additional_value(self.__ror_map.values()) def _initialize_parser_keys(self): """ We need to set the allowed rack shapes (that is all available rack shapes) and the transfer role markers. """ self.parser.source_role_marker = TRANSFER_ROLES.SOURCE self.parser.target_role_marker = TRANSFER_ROLES.TARGET rack_shape_agg = get_root_aggregate(IRackShape) rack_shape_agg.filter = None self.parser.allowed_rack_dimensions = [(rs.number_rows, rs.number_columns) for rs in rack_shape_agg] def _convert_results_to_entity(self): # Assembles a worklist series from the parsed sheet. self.add_info('Start conversion into worklist series ...') self._check_input_class('"allow_rack_creation" flag', self.allow_rack_creation, bool) if not self.has_errors(): self.__get_or_generate_racks() if not self.has_errors(): self.__create_worklists() if not self.has_errors(): self.add_info('Conversion completed.') self.return_value = self.__worklist_series def __get_or_generate_racks(self): # Racks (recognized by specs) are fetched from the DB. # Plates (recognized by specs) are fetched from the DB. # Reservoirs are generated. self.add_debug('Fetch or generate racks ...') for rack_container in self.parser.rack_containers.values(): data_item = self.__create_rack_or_reservoir_data(rack_container) if data_item is None: break self.__ror_map[data_item.identifier] = data_item def __create_rack_or_reservoir_data(self, rack_container): # Determines whether the specified items is a rack or a reservoirs. # Fetches racks for which there are barcodes or generates racks # without barcode. if rack_container.specs is None: rack = self.__get_rack_for_barcode(rack_container.barcode) if rack is None: return None else: rs = self.__get_reservoir_specs_for_rack(rack) if rs is None: return None data_item = \ RackOrReservoirItem(is_rack=True, reservoir_specs=rs, identifier=rack_container.rack_label) data_item.set_rack(rack) return data_item else: rs = self.__get_reservoir_specs(rack_container) if rs is None: return None is_rack = RESERVOIR_SPECS_NAMES.is_rack_spec(rs) data_item = \ RackOrReservoirItem(is_rack=is_rack, reservoir_specs=rs, identifier=rack_container.rack_label) barcode = rack_container.barcode if is_rack: if barcode is None and not self.allow_rack_creation: msg = 'When printing or executing worklists directly ' \ 'all used racks must already be stored in the DB ' \ 'and be specified by barcode. Rack "%s" does not ' \ 'have a barcode.' % (rack_container.identifier) self.add_error(msg) rack = None elif barcode is None: rack = self.__create_plate(rack_container, rs) else: rack = self.__get_rack_for_barcode(barcode) rs = self.__get_reservoir_specs_for_rack(rack, rs) if rs is None: return None data_item.reservoir_specs = rs if rack is None: return None data_item.set_rack(rack) else: if barcode is None: barcode = data_item.identifier data_item.barcode = barcode return data_item def __get_reservoir_specs(self, rack_container): # Also records an error message if the spec has not been found. try: rs = get_reservoir_spec(rack_container.specs.lower()) except ValueError as ve: msg = 'Error when trying to fetch reservoir specs for rack "%s": ' \ '%s' % (rack_container.identifier, ve) self.add_error(msg) result = None else: result = rs return result def __get_rack_for_barcode(self, barcode): # Also records error message if the aggregate did not return a rack. rack = self.__rack_agg.get_by_slug(barcode) if rack is None: msg = 'Could not find rack "%s" in the DB!' % (barcode) self.add_error(msg) return rack def __get_reservoir_specs_for_rack(self, rack, reservoir_specs=None): # Fetches the reservoir specs for a rack. If there is already a specs # in the file, the specs are compared. try: rs = get_reservoir_specs_from_rack_specs(rack.specs) except ValueError as ve: msg = 'Error when trying to determine reservoir specs for ' \ 'rack "%s" (rack specs "%s"): %s' \ % (rack.barcode, rack.specs.name, ve) self.add_error(msg) result = None else: if reservoir_specs is not None and not rs == reservoir_specs: msg = 'You specified a wrong reservoir spec for rack "%s" ' \ '("%s" instead of "%s"). Will use spec "%s".' \ % (rack.barcode, reservoir_specs.name, rs.name, rs.name) self.add_warning(msg) result = rs return result def __create_plate(self, rack_container, reservoir_specs): # Creates a new plate incl. label. The plate specs are derived # from the reservoir specs. # Creating of stock racks is not allowed. try: plate_specs = get_rack_specs_from_reservoir_specs(reservoir_specs) except ValueError as ve: msg = 'Error when trying to determine plate specs for rack "%s": ' \ '%s.' % (rack_container.identifier, ve) self.add_error(msg) result = None else: rack_id = rack_container.identifier plate_label = '%s_%s' % (self.parser.worklist_prefix, rack_id) if len(plate_label) > MAX_PLATE_LABEL_LENGTH: msg = 'The label that has been generated for the new plate ' \ '"%s" ("%s") is longer than %i characters (%i ' \ 'characters). You will not be able to print this ' \ 'label properly. To circumvent this problem choose ' \ 'a shorter rack identifier or a shorter worklist ' \ 'prefix.' \ % (rack_id, plate_label, MAX_PLATE_LABEL_LENGTH, len(plate_label)) self.add_warning(msg) result = plate_specs.create_rack(label=plate_label, status=get_item_status_future()) return result def __create_worklists(self): # Creates :class:`PlannedWorklist` objects and adds them to the # :attr:`__worklist_series` self.add_debug('Create worklists ...') for step_number in sorted(self.parser.step_containers.keys()): step_container = self.parser.step_containers[step_number] if not self.__has_consistent_rack_shapes(step_container): break transfer_type = self.__get_transfer_type(step_container) if transfer_type is None: break if not self.__has_only_racks_as_target(step_container): break planned_liquid_transfers = self.__get_planned_liquid_transfers( step_container, transfer_type) if planned_liquid_transfers is None: break robot_specs = self.__get_robot_specs(step_container) wl_label = '%s_%i' % (self.parser.worklist_prefix, step_number) worklist = PlannedWorklist(label=wl_label, transfer_type=transfer_type, planned_liquid_transfers=planned_liquid_transfers, pipetting_specs=robot_specs) self.__worklist_series.add_worklist(step_number, worklist) for role, rack_ids in step_container.rack_containers.iteritems(): for rack_id in rack_ids: data_item = self.__ror_map[rack_id] data_item.add_worklist(role, worklist) def __has_consistent_rack_shapes(self, step_container): # Makes sure the rack items assigned to the source and target layouts # of a step have the requested rack shape. for role, layout_container in step_container.layouts.iteritems(): rack_shape = self._convert_to_rack_shape(layout_container.shape) if rack_shape is None: return False for rack_id in step_container.rack_containers[role]: data_item = self.__ror_map[rack_id] if not data_item.rack_shape == rack_shape: msg = 'The rack shape for layout at %s (%s) does not ' \ 'match the rack shape for rack "%s" (%s).' \ % (layout_container.get_starting_cell_name(), layout_container.shape.name, data_item.identifier, data_item.rack_shape.name) self.add_error(msg) return False return True def __get_transfer_type(self, step_container): # Possible types are SAMPLE_DILUTION and SAMPLE_TRANSFER. is_dilution = False for rack_id in step_container.rack_containers[TRANSFER_ROLES.SOURCE]: data_item = self.__ror_map[rack_id] if not data_item.is_rack: is_dilution = True elif is_dilution: # this should not happen because reservoirs have other rack # shapes (consistency has already been tested at this point). # However, there might be other reservoirs in the future. msg = 'The source types for step %i are inconsistent. There ' \ 'must be either all racks or all reservoirs!' \ % (step_container.number) self.add_error(msg) return None if is_dilution: transfer_type = TRANSFER_TYPES.SAMPLE_DILUTION else: transfer_type = TRANSFER_TYPES.SAMPLE_TRANSFER return transfer_type def __has_only_racks_as_target(self, step_container): # Reservoirs must not be targets. result = True for rack_id in step_container.rack_containers[TRANSFER_ROLES.TARGET]: data_item = self.__ror_map[rack_id] if not data_item.is_rack: msg = 'The target for step %i is a reservoirs. Reservoirs ' \ 'may only serve as sources!' % (step_container.number) self.add_error(msg) result = False break return result def __get_robot_specs(self, step_container): # Always returns a Biomek spec. If the source racks are tube racks # the stock settings are returned. result = get_pipetting_specs_biomek() for rack_id in step_container.rack_containers[TRANSFER_ROLES.SOURCE]: data_item = self.__ror_map[rack_id] if isinstance(data_item.rack, TubeRack): result = get_pipetting_specs_biomek_stock() break return result def __get_planned_liquid_transfers(self, step_container, transfer_type): # Create :class:`PlannedSampleDilution`s for dilution (requires # valid diluent), otherwise it creates # :class:`PlannedSampleTransfer`s. planned_liquid_transfers = [] for transfer_container in step_container.get_transfer_containers(): src_positions = transfer_container.get_source_positions() trg_positions = transfer_container.get_target_positions() volume = float(transfer_container.volume) / VOLUME_CONVERSION_FACTOR if transfer_type == TRANSFER_TYPES.SAMPLE_DILUTION: diluent = transfer_container.diluent if diluent is None or not len(str(diluent)) > 1: msg = 'A diluent must be at least 2 characters long! ' \ 'Change the diluent for step %i code %s, ' \ 'please.' \ % (step_container.number, transfer_container.code) self.add_error(msg) return None for trg_pos_container in trg_positions: trg_pos = self._convert_to_rack_position(trg_pos_container) kw = dict(target_position=trg_pos, volume=volume) if transfer_type == TRANSFER_TYPES.SAMPLE_DILUTION: kw['diluent_info'] = str(transfer_container.diluent) psd = PlannedSampleDilution.get_entity(**kw) planned_liquid_transfers.append(psd) else: for src_pos_container in src_positions: src_pos = self._convert_to_rack_position( src_pos_container) kw['source_position'] = src_pos pst = PlannedSampleTransfer.get_entity(**kw) planned_liquid_transfers.append(pst) return planned_liquid_transfers
class LibraryCreationWorklistGenerator(BaseTool): """ Creates the worklist series for containing the worklists involved in library creation. Preparation route with pool stock racks (default): 1. Buffer addition into the pool stock racks (1 for each quadrant) 2. Buffer addition into preparation plates (1 for each quadrant) 3. Rack transfers from normal stock racks into pool stock racks (the take out worklists are not stored as part of this worklist series but at the sample stock racks as container transfer worklist to allow for container tracking) 4. Rack transfer from stock rack to preparation plate 5. Rack transfer from preparation plates to aliquot plate. For the preparation route without pool stock racks, steps 1 and 3 are left out and the stock transfer is made directly into the preparation plate (with the buffer volume adjusted accordingly). **Return Value:** worklist series (:class:`thelma.entities.liquidtransfer.WorklistSeries`). """ NAME = 'Library Creation Worklist Generator' #: Name pattern for the worklists that add annealing buffer to the pool #: stock racks. The placeholders will contain the library name and the #: quadrant sector. LIBRARY_STOCK_BUFFER_WORKLIST_LABEL = '%s_stock_buffer_Q%i' #: Name pattern for the worklists that add annealing buffer to the pool #: preparation plates. The placeholders will contain the library name and #: the quadrant sector. LIBRARY_PREP_BUFFER_WORKLIST_LABEL = '%s_prep_buffer_Q%i' #: Name pattern for the worklists that transfers the pool from the pool #: stock rack to the preparation plate. The placeholder will contain the #: library name. STOCK_TO_PREP_TRANSFER_WORKLIST_LABEL = '%s_stock_to_prep' #: Name pattern for the worklists that transfers the pool from the #: preparation plate to the final library aliqut plate. The placeholder will #: contain the library name. PREP_TO_ALIQUOT_TRANSFER_WORKLIST_LABEL = '%s_prep_to_aliquot' #: The dilution info for the dilution worklists. DILUTION_INFO = 'annealing buffer' def __init__(self, base_layout, stock_concentration, library_name, preparation_buffer_volume, pool_buffer_volume=None, parent=None): """ Constructor. :param base_layout: Layout defining which positions of the layout are allowed to take up library samples. :type base_layout: :class:`LibraryBaseLayout` :param int stock_concentration: Concentration of the single source molecule designs in the stock in nM (positive number). :param str library_name: Name of the library to be created. :param float preparation_buffer_volume: Buffer volume for preparation plates in ul. :param float pool_buffer_volume: Buffer volume for pool plates in ul. May be `None` if no pool racks are created. """ BaseTool.__init__(self, parent=parent) self.base_layout = base_layout self.stock_concentration = stock_concentration self.library_name = library_name self.preparation_buffer_volume = preparation_buffer_volume self.pool_buffer_volume = pool_buffer_volume #: The worklist series for the ISO request. self.__worklist_series = None #: The last used worklist index (within the series). self.__last_worklist_index = None #: The base layout for each sector. self.__sector_layouts = None def reset(self): BaseTool.reset(self) self.__worklist_series = None self.__last_worklist_index = 0 self.__sector_layouts = dict() def run(self): self.reset() self.add_info('Start worklist generation ...') self.__check_input() if not self.has_errors(): self.__sort_into_sectors() if not self.has_errors(): self.__worklist_series = WorklistSeries() if not self.pool_buffer_volume is None: self.__create_pool_rack_buffer_worklists() if not self.has_errors(): self.__create_preparation_plate_buffer_worklists() if not self.has_errors(): self.__create_prep_to_aliquot_worklist() if not self.has_errors(): self.return_value = self.__worklist_series self.add_info('Worklist generation completed.') def __check_input(self): # Checks the initialisation values. self.add_debug('Checking input.') self._check_input_class('base library layout', self.base_layout, LibraryBaseLayout) self._check_input_class('library name', self.library_name, basestring) if not is_valid_number(self.stock_concentration): msg = 'The stock concentration for the single source molecules ' \ 'must be a positive number (obtained: %s).' \ % (self.stock_concentration) self.add_error(msg) def __sort_into_sectors(self): # Create a rack layout for each quadrant. self.add_debug('Sorting positions into sectors.') self.__sector_layouts = \ get_sector_layouts_for_384_layout(self.base_layout, LibraryBaseLayout) if len(self.__sector_layouts) < NUMBER_SECTORS: missing_sector_labels = [str(si + 1) for si in range(NUMBER_SECTORS) if not si in self.__sector_layouts] msg = 'Some rack sectors are empty. You do not require stock ' \ 'racks for them: %s!' % ', '.join(missing_sector_labels) self.add_warning(msg) def __create_pool_rack_buffer_worklists(self): # These worklists are responsible for the addition of annealing buffer # to the pool rack. There is 1 worklist for each quadrant. self.add_debug('Creating pool buffer worklists.') for sector_index, sector_layout in self.__sector_layouts.iteritems(): label = self.LIBRARY_STOCK_BUFFER_WORKLIST_LABEL \ % (self.library_name, sector_index + 1) self.__create_buffer_worklist(sector_layout, self.pool_buffer_volume, label) def __create_preparation_plate_buffer_worklists(self): # These worklists are responsible for the addition of annealing # buffer to the pool preparation plate. There is 1 worklist for each # quadrant. self.add_debug('Creating preparation plate buffer worklists.') for sector_index, sector_layout in self.__sector_layouts.iteritems(): label = self.LIBRARY_PREP_BUFFER_WORKLIST_LABEL % ( self.library_name, (sector_index + 1)) self.__create_buffer_worklist(sector_layout, self.preparation_buffer_volume, label) def __create_buffer_worklist(self, sector_layout, buffer_volume, label): # Creates the buffer dilution worklist for a particular quadrant # and adds it to the worklist series. volume = buffer_volume / VOLUME_CONVERSION_FACTOR ptfs = [] for rack_pos_96 in sector_layout.get_positions(): planned_transfer = PlannedSampleDilution.get_entity( volume, self.DILUTION_INFO, rack_pos_96) ptfs.append(planned_transfer) worklist = PlannedWorklist(label, TRANSFER_TYPES.SAMPLE_DILUTION, get_pipetting_specs_biomek(), planned_liquid_transfers=ptfs) self.__worklist_series.add_worklist(self.__last_worklist_index, worklist) self.__last_worklist_index += 1 def __create_prep_to_aliquot_worklist(self): # There is one rack transfer for each sector (many-to-one transfer). # Each transfer is executed once per aliquot plate. self.add_debug('Add worklist for transfer into aliquot plates ...') volume = DEFAULT_ALIQUOT_PLATE_VOLUME / VOLUME_CONVERSION_FACTOR rack_transfers = [] for sector_index in self.__sector_layouts.keys(): rack_transfer = PlannedRackSampleTransfer.get_entity( volume, NUMBER_SECTORS, 0, sector_index) rack_transfers.append(rack_transfer) label = self.PREP_TO_ALIQUOT_TRANSFER_WORKLIST_LABEL % ( self.library_name) worklist = PlannedWorklist(label, TRANSFER_TYPES.RACK_SAMPLE_TRANSFER, get_pipetting_specs_cybio(), planned_liquid_transfers=rack_transfers) self.__worklist_series.add_worklist(self.__last_worklist_index, worklist) self.__last_worklist_index += 1
def __create_mastermix_series(self): # Creates the worklist series for the mastermix preparation. self.add_debug('Create mastermix preparation worklist series ...') self.__design_series = WorklistSeries() self.__generate_optimem_worklist() self.__generate_reagent_worklist()
class StockSampleCreationWorklistGenerator(BaseTool): """ Creates the worklist series containing the worklists involved in pool stock sample creation. This comprises only one worklist which deals with the addition of buffer to the future pool stock tubes. The tool will create planned sample dilutions for all positions of a 8x12 rack shape. **Return Value:** worklist series (:class:`thelma.entities.liquidtransfer.WorklistSeries`). """ NAME = 'Pool Creation Worklist Generator' #: The index for the buffer worklist within the series. BUFFER_WORKLIST_INDEX = 0 def __init__(self, volume_calculator, iso_request_label, parent=None): """ Constructor. :param volume_calculator: Determines transfer and dilution volumes for pool stock sample ISO requests. :type volume_calculator: :class:`VolumeCalculator` :param int number_designs: The number of single molecule designs for each pool (positive number). :param int target_volume: The final volume for the new pool stock tubes in ul (positive number). :param int target_concentration: The final pool concentration for the new pool stock tubes in nM (positive number). :param str iso_request_label: The plate set label of the ISO request to be created - will be used as part of the worklist name. """ BaseTool.__init__(self, parent=parent) #: The plate set label of the ISO request to be created - will be used #: as part of the worklist name. self.iso_request_label = iso_request_label #: The :class:`VolumeCalculator` determines transfer and buffer volumes #: and might also adjust the target volume for the ISO request. self.volume_calculator = volume_calculator #: The worklist series for the ISO request. self.__worklist_series = None def reset(self): BaseTool.reset(self) self.__worklist_series = WorklistSeries() def run(self): self.reset() self.add_info('Start worklist series generation ...') self.__check_input() if not self.has_errors(): self.__create_transfers() if not self.has_errors(): self.return_value = self.__worklist_series self.add_info('Worklist series generation completed.') def __check_input(self): """ Checks the initialisation values. """ self.add_debug('Check input ...') self._check_input_class('volume calculator', self.volume_calculator, VolumeCalculator) self._check_input_class('ISO request label', self.iso_request_label, basestring) def __create_transfers(self): # Creates a :class:`PlannedSampleDilution` for each rack position # in a 8x12 rack shape. self.add_debug('Create transfers ...') self._run_and_record_error(self.volume_calculator.calculate, 'Error when trying to determine buffer volume: ', ValueError) buffer_volume = self.volume_calculator.get_buffer_volume() if buffer_volume is not None: volume = buffer_volume / VOLUME_CONVERSION_FACTOR wl_label = LABELS.create_buffer_worklist_label( self.iso_request_label) wl = PlannedWorklist(wl_label, TRANSFER_TYPES.SAMPLE_DILUTION, get_pipetting_specs_cybio()) for rack_pos in get_positions_for_shape(RACK_SHAPE_NAMES.SHAPE_96): psd = PlannedSampleDilution.get_entity(volume=volume, target_position=rack_pos, diluent_info=DILUENT_INFO) wl.planned_liquid_transfers.append(psd) self.__worklist_series.add_worklist(self.BUFFER_WORKLIST_INDEX, wl)
class LibraryCreationWorklistGenerator(BaseTool): """ Creates the worklist series for containing the worklists involved in library creation. Preparation route with pool stock racks (default): 1. Buffer addition into the pool stock racks (1 for each quadrant) 2. Buffer addition into preparation plates (1 for each quadrant) 3. Rack transfers from normal stock racks into pool stock racks (the take out worklists are not stored as part of this worklist series but at the sample stock racks as container transfer worklist to allow for container tracking) 4. Rack transfer from stock rack to preparation plate 5. Rack transfer from preparation plates to aliquot plate. For the preparation route without pool stock racks, steps 1 and 3 are left out and the stock transfer is made directly into the preparation plate (with the buffer volume adjusted accordingly). **Return Value:** worklist series (:class:`thelma.entities.liquidtransfer.WorklistSeries`). """ NAME = "Library Creation Worklist Generator" #: Name pattern for the worklists that add annealing buffer to the pool #: stock racks. The placeholders will contain the library name and the #: quadrant sector. LIBRARY_STOCK_BUFFER_WORKLIST_LABEL = "%s_stock_buffer_Q%i" #: Name pattern for the worklists that add annealing buffer to the pool #: preparation plates. The placeholders will contain the library name and #: the quadrant sector. LIBRARY_PREP_BUFFER_WORKLIST_LABEL = "%s_prep_buffer_Q%i" #: Name pattern for the worklists that transfers the pool from the pool #: stock rack to the preparation plate. The placeholder will contain the #: library name. STOCK_TO_PREP_TRANSFER_WORKLIST_LABEL = "%s_stock_to_prep" #: Name pattern for the worklists that transfers the pool from the #: preparation plate to the final library aliqut plate. The placeholder will #: contain the library name. PREP_TO_ALIQUOT_TRANSFER_WORKLIST_LABEL = "%s_prep_to_aliquot" #: The dilution info for the dilution worklists. DILUTION_INFO = "annealing buffer" def __init__( self, base_layout, stock_concentration, library_name, preparation_buffer_volume, pool_buffer_volume=None, parent=None, ): """ Constructor. :param base_layout: Layout defining which positions of the layout are allowed to take up library samples. :type base_layout: :class:`LibraryBaseLayout` :param int stock_concentration: Concentration of the single source molecule designs in the stock in nM (positive number). :param str library_name: Name of the library to be created. :param float preparation_buffer_volume: Buffer volume for preparation plates in ul. :param float pool_buffer_volume: Buffer volume for pool plates in ul. May be `None` if no pool racks are created. """ BaseTool.__init__(self, parent=parent) self.base_layout = base_layout self.stock_concentration = stock_concentration self.library_name = library_name self.preparation_buffer_volume = preparation_buffer_volume self.pool_buffer_volume = pool_buffer_volume #: The worklist series for the ISO request. self.__worklist_series = None #: The last used worklist index (within the series). self.__last_worklist_index = None #: The base layout for each sector. self.__sector_layouts = None def reset(self): BaseTool.reset(self) self.__worklist_series = None self.__last_worklist_index = 0 self.__sector_layouts = dict() def run(self): self.reset() self.add_info("Start worklist generation ...") self.__check_input() if not self.has_errors(): self.__sort_into_sectors() if not self.has_errors(): self.__worklist_series = WorklistSeries() if not self.pool_buffer_volume is None: self.__create_pool_rack_buffer_worklists() if not self.has_errors(): self.__create_preparation_plate_buffer_worklists() if not self.has_errors(): self.__create_prep_to_aliquot_worklist() if not self.has_errors(): self.return_value = self.__worklist_series self.add_info("Worklist generation completed.") def __check_input(self): # Checks the initialisation values. self.add_debug("Checking input.") self._check_input_class("base library layout", self.base_layout, LibraryBaseLayout) self._check_input_class("library name", self.library_name, basestring) if not is_valid_number(self.stock_concentration): msg = ( "The stock concentration for the single source molecules " "must be a positive number (obtained: %s)." % (self.stock_concentration) ) self.add_error(msg) def __sort_into_sectors(self): # Create a rack layout for each quadrant. self.add_debug("Sorting positions into sectors.") self.__sector_layouts = get_sector_layouts_for_384_layout(self.base_layout, LibraryBaseLayout) if len(self.__sector_layouts) < NUMBER_SECTORS: missing_sector_labels = [str(si + 1) for si in range(NUMBER_SECTORS) if not si in self.__sector_layouts] msg = "Some rack sectors are empty. You do not require stock " "racks for them: %s!" % ", ".join( missing_sector_labels ) self.add_warning(msg) def __create_pool_rack_buffer_worklists(self): # These worklists are responsible for the addition of annealing buffer # to the pool rack. There is 1 worklist for each quadrant. self.add_debug("Creating pool buffer worklists.") for sector_index, sector_layout in self.__sector_layouts.iteritems(): label = self.LIBRARY_STOCK_BUFFER_WORKLIST_LABEL % (self.library_name, sector_index + 1) self.__create_buffer_worklist(sector_layout, self.pool_buffer_volume, label) def __create_preparation_plate_buffer_worklists(self): # These worklists are responsible for the addition of annealing # buffer to the pool preparation plate. There is 1 worklist for each # quadrant. self.add_debug("Creating preparation plate buffer worklists.") for sector_index, sector_layout in self.__sector_layouts.iteritems(): label = self.LIBRARY_PREP_BUFFER_WORKLIST_LABEL % (self.library_name, (sector_index + 1)) self.__create_buffer_worklist(sector_layout, self.preparation_buffer_volume, label) def __create_buffer_worklist(self, sector_layout, buffer_volume, label): # Creates the buffer dilution worklist for a particular quadrant # and adds it to the worklist series. volume = buffer_volume / VOLUME_CONVERSION_FACTOR ptfs = [] for rack_pos_96 in sector_layout.get_positions(): planned_transfer = PlannedSampleDilution.get_entity(volume, self.DILUTION_INFO, rack_pos_96) ptfs.append(planned_transfer) worklist = PlannedWorklist( label, TRANSFER_TYPES.SAMPLE_DILUTION, get_pipetting_specs_biomek(), planned_liquid_transfers=ptfs ) self.__worklist_series.add_worklist(self.__last_worklist_index, worklist) self.__last_worklist_index += 1 def __create_prep_to_aliquot_worklist(self): # There is one rack transfer for each sector (many-to-one transfer). # Each transfer is executed once per aliquot plate. self.add_debug("Add worklist for transfer into aliquot plates ...") volume = DEFAULT_ALIQUOT_PLATE_VOLUME / VOLUME_CONVERSION_FACTOR rack_transfers = [] for sector_index in self.__sector_layouts.keys(): rack_transfer = PlannedRackSampleTransfer.get_entity(volume, NUMBER_SECTORS, 0, sector_index) rack_transfers.append(rack_transfer) label = self.PREP_TO_ALIQUOT_TRANSFER_WORKLIST_LABEL % (self.library_name) worklist = PlannedWorklist( label, TRANSFER_TYPES.RACK_SAMPLE_TRANSFER, get_pipetting_specs_cybio(), planned_liquid_transfers=rack_transfers, ) self.__worklist_series.add_worklist(self.__last_worklist_index, worklist) self.__last_worklist_index += 1
def reset(self): BaseTool.reset(self) self.__worklist_series = WorklistSeries()