sanger/sequencescape

View on GitHub
app/models/cherrypick_task/pick_target.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
95%
# frozen_string_literal: true

# An instance of this class represents the target plate being picked onto.  It can have a template
# and be a partial plate, and so when wells are picked into it we need to ensure that we don't hit
# the template/partial wells.
class CherrypickTask::PickTarget
  def self.for(plate_purpose)
    cherrypick_direction = plate_purpose.nil? ? 'column' : plate_purpose.cherrypick_direction
    const_get("by_#{cherrypick_direction}".classify)
  end

  # Base class for different pick target beha
  class Base # rubocop:todo Metrics/ClassLength
    def initialize(template, asset_shape = nil, partial = nil)
      @wells = []
      @size = template.size
      @shape = asset_shape || AssetShape.default
      initialize_already_occupied_wells_from(template, partial)
      add_any_wells_from_template_or_partial(@wells)
    end

    def empty?
      @wells.empty?
    end

    def content
      @wells
    end

    attr_reader :size

    def full?
      @wells.size == @size
    end

    # Creates control requests for the control assets provided and adds them to the batch
    def create_control_requests!(batch, control_assets)
      control_requests =
        control_assets.map do |control_asset|
          CherrypickRequest.create(
            asset: control_asset,
            target_asset: Well.new,
            submission: batch.requests.first.submission,
            request_type: batch.requests.first.request_type,
            request_purpose: 'standard'
          )
        end
      batch.requests << control_requests
      control_requests
    end

    # Creates a new control request for the control_asset and adds it into the current_destination_plate plate
    def add_control_request(batch, control_asset)
      control_request = create_control_requests!(batch, [control_asset]).first
      control_request.start!
      push(control_request.id, control_request.asset.plate.human_barcode, control_request.asset.map_description)
    end

    # Adds any consecutive list of control requests into the current_destination_plate
    def add_any_consecutive_control_requests(control_posns, batch, control_assets)
      # find the index of the well we are filling right now
      current_well_index = content.length

      # check if this well should contain a control
      # add it if so, and any consecutive ones by looping
      while control_posns.include?(current_well_index)
        control_asset = control_assets[control_posns.find_index(current_well_index)]
        add_control_request(batch, control_asset)

        # above adds to current_destination_plate, so current_well_index should be recalculated
        current_well_index = content.length
      end
    end

    # Adds any remaining control requests not already added, into the current_destination_plate plate
    def add_remaining_control_requests(control_posns, batch, control_assets)
      control_posns.each_with_index do |pos, idx|
        if pos >= content.length
          control_asset = control_assets[idx]
          add_control_request(batch, control_asset)
        end
      end
    end

    def push(request_id, plate_barcode, well_location)
      @wells << [request_id, plate_barcode, well_location]

      add_any_wells_from_template_or_partial(@wells)
      self
    end

    # includes control wells and template / partial wells that are yet to be added
    def remaining_wells(control_posns)
      remaining_controls = control_posns.select { |c| c > @wells.length }
      remaining_used_wells = @used_wells.keys.select { |c| c > @wells.length }
      remaining_controls.concat(remaining_used_wells).flatten
    end

    # rubocop:todo Metrics/ParameterLists
    def push_with_controls(request_id, plate_barcode, well_location, control_posns, batch, control_assets)
      # rubocop:enable Metrics/ParameterLists
      @wells << [request_id, plate_barcode, well_location]
      if control_posns
        # would be nil if no control plate selected
        add_any_consecutive_control_requests(control_posns, batch, control_assets)

        # This assumes that the template wells will fall at the end of the plate
        if (@wells.length + remaining_wells(control_posns).length) == @size
          add_remaining_control_requests(control_posns, batch, control_assets)
        end
      end
      add_any_wells_from_template_or_partial(@wells)
      self
    end

    # Completes the given well array such that it looks like the plate has been completely picked.
    def complete(wells)
      until wells.size >= @size
        add_empty_well(wells)

        add_any_wells_from_template_or_partial(wells)
      end
    end
    private :complete

    # Determines the wells that are already occupied on the template or the partial plate.  This is
    # then used in add_any_wells_from_template_or_partial to fill them in as wells are added by the
    # pick.
    def initialize_already_occupied_wells_from(template, partial)
      @used_wells =
        {}.tap do |wells|
          [partial, template].compact.each do |plate|
            plate.wells.each { |w| wells[w.map.horizontal_plate_position] = w.map.description }
          end
        end
    end
    private :initialize_already_occupied_wells_from

    # Every time a well is added to the pick we need to make sure that the template and partial are
    # checked to see if subsequent wells are already taken.  In other words, after calling this method
    # the next position on the pick plate is known to be empty.
    def add_any_wells_from_template_or_partial(wells)
      wells << CherrypickTask::TEMPLATE_EMPTY_WELL until (wells.size >= @size) || @used_wells[well_position(wells)].nil?
    end
    private :add_any_wells_from_template_or_partial

    def add_empty_well(wells)
      wells << CherrypickTask::EMPTY_WELL
    end
    private :add_empty_well

    # When starting a new plate, it writes all control requests from the beginning of the plate
    def add_any_initial_control_requests(control_posns, batch, control_assets)
      current_well_index = content.length
      control_posns
        .select { |c| c <= current_well_index }
        .each do |control_well_index|
          control_asset = control_assets[control_posns.find_index(control_well_index)]
          add_control_request(batch, control_asset)
        end
      add_any_consecutive_control_requests(control_posns, batch, control_assets)
    end
  end

  # Deals with generating the pick plate by travelling in a row direction, so A1, A2, A3 ...
  class ByRow < Base
    def well_position(wells)
      (wells.size + 1) > @size ? nil : wells.size + 1
    end
    private :well_position

    def completed_view
      @wells
        .dup
        .tap { |wells| complete(wells) }
        .each_with_index
        .inject([]) do |wells, (well, index)|
          wells.tap { wells[@shape.horizontal_to_vertical(index + 1, @size)] = well }
        end
        .compact
    end
  end

  # Deals with generating the pick plate by travelling in a column direction, so A1, B1, C1 ...
  class ByColumn < Base
    def well_position(wells)
      @shape.vertical_to_horizontal(wells.size + 1, @size)
    end
    private :well_position

    def completed_view
      @wells.dup.tap { |wells| complete(wells) }
    end
  end

  # Deals with generating the pick plate by travelling in an interlaced column direction, so A1, C1, E1 ...
  class ByInterlacedColumn < Base
    def well_position(wells)
      @shape.interlaced_vertical_to_horizontal(wells.size + 1, @size)
    end
    private :well_position

    def completed_view
      @wells
        .dup
        .tap { |wells| complete(wells) }
        .each_with_index
        .inject([]) do |wells, (well, index)|
          wells.tap { wells[@shape.vertical_to_interlaced_vertical(index + 1, @size)] = well }
        end
        .compact
    end
  end
end