app/models/labware_creators/pooled_tubes_by_sample.rb
# frozen_string_literal: true
require_dependency 'labware_creators/base'
module LabwareCreators
##
# Pools from a plate into tubes, grouping together wells that contain the same sample
# NB. Currently this is specific to the Cardinal usage of FluidX tubes
##
class PooledTubesBySample < PooledTubesBase # rubocop:todo Metrics/ClassLength
include SupportParent::PlateOnly
include LabwareCreators::CustomPage
self.page = 'tube_creation/pooled_tubes_by_sample'
self.attributes += [:file]
attr_accessor :file
# delegate method to return well values to csv file handler class
delegate :well_details, to: :csv_file
validates :file, presence: true # Don't create the tubes until the file has been uploaded
validates_nested :csv_file, if: :file # Don't create the tubes until the file has been validated
validate :must_have_enough_tubes_for_pools # Don't create the tubes if there aren't enough available for our needs
PARENT_PLATE_INCLUDES = 'wells.aliquots,wells.aliquots.sample,wells.aliquots.sample.sample_metadata'
def save
super && upload_file && true
end
def parent
@parent ||= Sequencescape::Api::V2.plate_with_custom_includes(PARENT_PLATE_INCLUDES, uuid: parent_uuid)
end
def parent_v1
@parent_v1 ||= api.plate.find(parent_uuid)
end
# TODO: QUESTIONS:
#
# Should we pre-filter wells, based on whether they have been failed, or based on what request they have?
# -> Should check this general strategy with team, as labware creators are inconsistent.
#
# Are these pool identifiers recorded in the db? SS transfer_request.rb mentions 'the pool_id attribute on well'...?
#
# Have we made this class so cardinal-specific (e.g. lookup of ancestor vac tubes) that it cannot be re-used?
def create_child_stock_tubes
api
.specific_tube_creation
.create!(
user: user_uuid,
parent: parent_uuid,
child_purposes: [purpose_uuid] * pool_uuids.length,
tube_attributes: tube_attributes
)
.children
.index_by(&:name)
end
def name_for_details(pool_identifier)
{
source_tube_barcode: pools_with_extra_details[pool_identifier][:source_tube_barcode],
destination_tube_posn: pools_with_extra_details[pool_identifier][:destination_tube_posn]
}
end
def transfer_request_attributes
pools.each_with_object([]) do |(pool_identifier, pool), transfer_requests|
pool.each do |location|
transfer_requests <<
request_hash(
well_locations.fetch(location).uuid,
child_stock_tubes.fetch(name_for(name_for_details(pool_identifier))).uuid,
pool_identifier
)
end
end
end
def pools
@pools ||= determine_pools
end
private
#
# Create the tube attributes to send for the tubes creation in Sequencescape.
# Passes the name for each tube.
# Passes the foreign barcode extracted from the tube rack scan upload for each tube,
# which on the Sequencescape side sets that barcode as the primary.
#
# returns [Array of hashes] e.g.
# [
# {
# name: NT11111111:A1,
# foreign_barcode: FD11111111
# },
# {
# name: NT22222222:B1,
# foreign_barcode: FD22222222
# },
# etc.
# ]
# Assumption: pools are already in column order (by first sample instance appearance in
# the source plate)
#
def tube_attributes
# fetch the available tube positions (i.e. locations of scanned tubes for which we
# have the barcodes) e.g. ["A1", "B1", "D1"]
available_tube_positions = csv_file.position_details.keys
pools_with_extra_details.values.each_with_index.map do |pool_details, pool_index|
tube_posn = available_tube_positions[pool_index]
# set tube position in pools_with_extra_details for use later in transfer requests
pool_details[:destination_tube_posn] = tube_posn
name_for_details = { source_tube_barcode: pool_details[:source_tube_barcode], destination_tube_posn: tube_posn }
{ name: name_for(name_for_details), foreign_barcode: csv_file.position_details[tube_posn]['tube_barcode'] }
end
end
#
# Generates a name for the destination tube.
# Comprises the ancestor source (stock) tube barcode and the destination tube position
# return [String] e.g. 'NT12345678:A1'
#
def name_for(name_for_details)
"#{name_for_details[:source_tube_barcode]}:#{name_for_details[:destination_tube_posn]}"
end
#
# Upload the csv file onto the plate via api v1
#
def upload_file
parent_v1.qc_files.create_from_file!(file, 'tube_rack_scan_file.csv')
end
#
# Create class that will parse and validate the uploaded file
#
def csv_file
@csv_file ||= CommonFileHandling::CsvFileForTubeRack.new(file) if file
end
#
# Validate that we have identified enough destination tube barcodes from the rack scan csv for
# the number of pools that we have to transfer.
# @return [void]
#
def must_have_enough_tubes_for_pools
return if pools.blank?
return if csv_file.blank?
num_pools = pools.count
num_tubes = csv_file.position_details.count
return unless num_pools > num_tubes
# TODO: test this
errors.add(
:csv_file,
"contains #{num_tubes} tubes, whereas we need #{num_pools} to match the number of unique samples"
)
end
def pools_with_extra_details
@pools_with_extra_details ||= extract_pools_with_extra_details
end
def ancestor_stock_tubes
@ancestor_stock_tubes ||= locate_ancestor_tubes
end
#
# Identify the originally supplied ancestor source tubes from their purpose name
# and create a hash with sample uuid as the key
# @return [Hash] e.g.
# {
# <sample 1 uuid>: <tube 1>,
# <sample 2 uuid>: <tube 2>,
# etc.
# }
def locate_ancestor_tubes
purpose_name = purpose_config[:ancestor_stock_tube_purpose_name]
ancestor_results = parent.ancestors.where(purpose_name: purpose_name)
return {} if ancestor_results.blank?
ancestor_results.each_with_object({}) do |ancestor_result, tube_list|
tube = Sequencescape::Api::V2::Tube.find_by(uuid: ancestor_result.uuid)
tube_sample_uuid = tube.aliquots.first.sample.uuid
tube_list[tube_sample_uuid] = tube if tube_sample_uuid.present?
end
end
# get the original supplier (ancestor) tube barcode (if not already set on this pool)
def add_sample_ancestor_tube_barcode(extra_details, sample_uuid)
sample_ancestor_tube_barcode = ancestor_stock_tubes[sample_uuid]
if sample_ancestor_tube_barcode.blank?
raise StandardError, "Failed to identify ancestor (supplier) source tube for sample uuid #{sample_uuid}"
end
extra_details[sample_uuid][:source_tube_barcode] = sample_ancestor_tube_barcode.labware_barcode.human
end
#
# Builds pools_with_extra_details hash, based on which wells contain the same sample.
# Uses the sample uuid as the key for the pool.
#
# @return [Hash of Hashes] e.g.
# {
# "a1aa0101-16e1-11ec-80e2-acde48001121" = {
# 'locations' => ["A1", "B1"],
# 'source_tube_barcode' => 'NT12345678'
# }
# }
# where 'A1' and 'B1' are the coordinates of the source wells to go into that pool
#
# rubocop:disable Metrics/AbcSize
def extract_pools_with_extra_details
extra_details = Hash.new { |hash, pool_name| hash[pool_name] = { locations: [] } }
parent.wells_in_columns.each do |well|
next if well.empty?
sample_uuid = well.aliquots.first.sample.uuid
# the same sample may be present in more than one well
extra_details[sample_uuid][:locations] << well.location
next if extra_details[sample_uuid].key?(:source_tube_barcode)
add_sample_ancestor_tube_barcode(extra_details, sample_uuid)
end
extra_details
end
# rubocop:enable Metrics/AbcSize
#
# Builds pools hash, based on which wells contain the same sample.
# Uses the sample uuid as the key for the pool.
#
# @return [Hash] e.g. { "a1aa0101-16e1-11ec-80e2-acde48001121" => ["A1", "B1"] }
# where 'A1' and 'B1' are the coordinates of the source wells to go into that pool
#
def determine_pools
pools = Hash.new { |hash, pool_name| hash[pool_name] = [] }
pools_with_extra_details.each_key do |sample_uuid|
pools[sample_uuid] = pools_with_extra_details[sample_uuid][:locations]
end
pools
end
def request_hash(source, target, submission)
{
'source_asset' => source,
'target_asset' => target,
'submission' => submission,
'merge_equivalent_aliquots' => true
}
end
end
end