Class: Plate

Overview

https://github.com/sanger/sequencescape/raw/master/docs/images/plate.jpg

A plate is a piece of labware made up of a number of wells. This class represents the physical piece of plastic.

  • PlatePurpose: describes the role a plate has in the lab. In some cases a plate's purpose may change as it gets processed.
  • Well: Plates can have multiple wells (most often 96 or 384) each of which can contain multiple samples.
  • PlateType: Identifies the plates form factor, typically provided to robots to ensure tips are positioned correctly.

Defined Under Namespace

Modules: FluidigmBehaviour, PoolingMetadata Classes: Creator, CreatorParameters, QuadCreator, SampleTubeFactory

Constant Summary

Constants included from Metadata

Metadata::SECTION_FIELDS

Instance Attribute Summary

Attributes inherited from Labware

#storage_location_service

Class Method Summary collapse

Instance Method Summary collapse

Methods included from QcFile::Associations

has_qc_files

Methods included from Metadata

has_metadata

Methods included from SubmissionPool::Association::Plate

included

Methods included from Barcode::Barcodeable

#any_barcode_matching?, #barcode_format, #barcode_number, #cgap_barcode, #cgap_barcode=, #external_barcode, #external_barcode=, #external_identifier, #fluidigm_barcode, #fluidigm_barcode=, #foreign_barcode=, included, #infinium_barcode, #infinium_barcode=, #prefix, #primary_barcode, #printable_target, #sanger_barcode

Methods included from PlateCreation::CreationChild

included

Methods included from Asset::Ownership::Owned

#change_owner_to, included

Methods included from Transfer::State::PlateState

included

Methods included from Transfer::Associations

included

Methods included from Api::PlateIo::Extensions

included, #json_root

Methods inherited from Labware

#ancestor_of_purpose, #ancestors_of_purpose, #child, #display_name, #external_identifier, find_by_barcode, find_from_any_barcode, #generate_name, #labware, #labwhere_location, labwhere_locations, map_retention_instructions, #parent, #poly_metadata, #received_date, #retention_instructions, #role, #scanned_in_date, search_for_count_of_labware, search_for_labware, #source_plate, #source_plates, #spiked_in_buffer, #storage_location

Methods included from SharedBehaviour::Named

included

Methods included from AssetLink::Associations

included

Methods included from Uuid::Uuidable

included, #unsaved_uuid!, #uuid

Methods inherited from Asset

#ancestor_of_purpose, #asset_type_for_request_types, #barcode_number, #contained_samples, #get_qc_result_value_for, #has_stock_asset?, #label, #label=, #original_stock_plates, #prefix, #printable?, #printable_target, #register_stock!, #request_types, #type, #update_from_qc

Methods included from EventfulRecord

#has_many_events, #has_many_lab_events, #has_one_event_with_family

Methods included from Event::PlateEvents

#event_date, #fluidigm_stamp_date, #gel_qc_date, #pico_date, #qc_started_date, #sequenom_stamp_date

Methods inherited from ApplicationRecord

alias_association, convert_labware_to_receptacle_for, find_by_id_or_name, find_by_id_or_name!

Methods included from Squishify

extended

Methods included from Warren::Callback::ExceptExternallyManaged

#broadcast_except_externally_managed, included

Class Method Details

.create_with_barcode!(*args) ⇒ Object



334
335
336
337
338
# File 'app/models/plate.rb', line 334

def self.create_with_barcode!(*args, &)
  attributes = args.extract_options!
  attributes[:sanger_barcode] ||= PlateBarcode.create_barcode
  create!(attributes, &)
end

.plate_ids_from_requests(requests) ⇒ Object



302
303
304
# File 'app/models/plate.rb', line 302

def self.plate_ids_from_requests(requests)
  with_requests(requests).pluck(:id)
end

Instance Method Details

#after_comment_addition(comment) ⇒ Object



467
468
469
# File 'app/models/plate.rb', line 467

def after_comment_addition(comment)
  comments.add_comment_to_submissions(comment)
end

#all_submission_idsObject

Prioritised the submissions that have been made from the plate then falls back onto the ones under which the plate was made



182
183
184
# File 'app/models/plate.rb', line 182

def all_submission_ids
  submission_ids_as_source.presence || submission_ids
end

#buffer_required?Boolean

Returns:

  • (Boolean)


348
349
350
# File 'app/models/plate.rb', line 348

def buffer_required?
  wells.any?(&:buffer_required?)
end

#cherrypick_completedVoid

Called when cherrypicking is completed to allow the plate to trigger any callbacks, such as broadcasting Fluidigm plates to the warehouse. This behaviour varies based on the PlatePurpose

Returns:

  • (Void)


159
160
161
# File 'app/models/plate.rb', line 159

def cherrypick_completed
  plate_purpose.cherrypick_completed(self)
end

#commentsObject



200
201
202
# File 'app/models/plate.rb', line 200

def comments
  @comments ||= CommentsProxy::Plate.new(self)
end

#compatible_purposesObject



404
405
406
# File 'app/models/plate.rb', line 404

def compatible_purposes
  PlatePurpose.compatible_with_purpose(purpose)
end

#convert_to(new_purpose) ⇒ Object



400
401
402
# File 'app/models/plate.rb', line 400

def convert_to(new_purpose)
  update!(plate_purpose: new_purpose)
end

#detailsObject



298
299
300
# File 'app/models/plate.rb', line 298

def details
  purpose.try(:name) || 'Unknown plate purpose'
end

#find_well_by_name(well_name) ⇒ Object Also known as: find_well_by_map_description



277
278
279
# File 'app/models/plate.rb', line 277

def find_well_by_name(well_name)
  wells.loaded? ? wells.indexed_by_location[well_name] : wells.located_at_position(well_name).first
end

#generate_barcodeObject

Plates use a different counter to tubes, and prior to the foreign barcodes update this method would have fallen back to Barcodable#generate tubes, and potentially generated an invalid plate barcode. In the future we probably want to scrap this approach entirely, and generate all barcodes in the plate style. (That is, as part of the factory on, eg. plate purpose)

Raises:

  • (StandardError)


458
459
460
461
# File 'app/models/plate.rb', line 458

def generate_barcode
  raise StandardError,
        "#generate_barcode has been called on plate, which wasn't supposed to happen, and probably indicates a bug."
end

#heightObject



375
376
377
# File 'app/models/plate.rb', line 375

def height
  asset_shape&.plate_height(size)
end

#invalid_positions(positions) ⇒ Array

Given a list of well map_descriptions (eg. A1), returns those not present on the plate

Parameters:

  • positions (Array)

    Array of positions to test

Returns:

  • (Array)

    Array of invalid positions



359
360
361
# File 'app/models/plate.rb', line 359

def invalid_positions(positions)
  (positions.uniq - unique_positions_on_plate).sort
end

#iterationObject



190
191
192
193
194
195
196
197
198
# File 'app/models/plate.rb', line 190

def iteration
  iter =
    siblings # assets sharing the same parent
      .where(plate_purpose_id:, sti_type:) # of the same purpose and type
      .where("#{self.class.table_name}.created_at <= ?", created_at) # created before or at the same time
      .count(:id) # count the siblings.

  iter.zero? ? nil : iter # Maintains compatibility with legacy version
end

#mapsObject



273
274
275
# File 'app/models/plate.rb', line 273

def maps
  Map.where_plate_size(size).where_plate_shape(asset_shape)
end

#name_for_labelObject



367
368
369
# File 'app/models/plate.rb', line 367

def name_for_label
  name
end

#number_of_blank_samplesObject



340
341
342
# File 'app/models/plate.rb', line 340

def number_of_blank_samples
  wells.with_blank_samples.count
end

#occupied_well_countInteger

Note:

Does not take into account the Sample#empty_supplier_sample_name flag on older samples

Counts the number of wells containing one or more aliquots.

Returns:

  • (Integer)

    The number of wells with samples



149
150
151
# File 'app/models/plate.rb', line 149

def occupied_well_count
  wells.with_contents.count
end

#pick_as_control?false

When Cherrypicking, especially on the Hamilton, control plates get placed on a seperate bed. ControlPlates overide this.

Returns:

  • (false)


486
487
488
# File 'app/models/plate.rb', line 486

def pick_as_control?
  false
end

#plate_columnsObject



286
287
288
# File 'app/models/plate.rb', line 286

def plate_columns
  (1..width)
end

#plate_rowsObject



282
283
284
# File 'app/models/plate.rb', line 282

def plate_rows
  ('A'..('A'.getbyte(0) + height - 1).chr.to_s).to_a
end

#plate_typeObject



290
291
292
# File 'app/models/plate.rb', line 290

def plate_type
  labware_type&.name || Sequencescape::Application.config.plate_default_type
end

#plate_type=(plate_type) ⇒ Object



294
295
296
# File 'app/models/plate.rb', line 294

def plate_type=(plate_type)
  self.labware_type = PlateType.find_by(name: plate_type)
end

#pre_cap_groupsObject

Adds pre-capture pooling information, we need to delegate this to the stock plate, as we need all the wells Currently used in Transfer::BetweenPlates to set submission id, we should switch to doing this directly via Limber with transfer request collections



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'app/models/plate.rb', line 207

def pre_cap_groups # rubocop:todo Metrics/AbcSize
  Request
    .
    .for_pre_cap_grouping_of(self)
    .each_with_object({}) do |request, groups|
    next if request.group_id.nil?

    groups[request.group_id] = { wells: request.group_into.split(',') }.tap do |pool_information|
      pool_information[:pre_capture_plex_level] ||= request..pre_capture_plex_level

      # We supply the submission id to assist with correctly tagging transfer requests later
      pool_information[:submission_id] ||= request.submission_id
    end
  end
end

#priorityObject



223
224
225
# File 'app/models/plate.rb', line 223

def priority
  waiting_submissions.maximum(:priority) || in_progress_submissions.maximum(:priority) || 0
end

#receptacles_with_positionObject

Used to unify interface with TubeRacks. Returns a list of all receptacles wells with position information included for aid performance



116
117
118
# File 'app/models/plate.rb', line 116

def receptacles_with_position
  wells.includes(:map)
end


471
472
473
# File 'app/models/plate.rb', line 471

def related_studies
  studies
end

#sanger_barcode=(barcode) ⇒ Object



463
464
465
# File 'app/models/plate.rb', line 463

def sanger_barcode=(barcode)
  barcodes << barcode
end

#scored?Boolean

Returns:

  • (Boolean)


344
345
346
# File 'app/models/plate.rb', line 344

def scored?
  wells.any?(&:get_gel_pass)
end

#stateString

The state of a plate loosely defines what has happened to it. In most cases it is determined by aggregating the state of transfer requests into the wells, although exact behaviour is determined by the PlatePurpose. State typically only works for pipeline application plates. In general:

  • pending: The plate has been registered, but it empty.
  • started: The plate contains samples, but required further processing
  • passed: Work on the plate is complete, and it can be transferred to another target
  • failed: The plate failed QC and can not be progressed further
  • cancelled: The plate is no longer required and should be ignored.

Returns:

  • (String)

    Name of the state the plate is in



131
132
133
# File 'app/models/plate.rb', line 131

def state
  plate_purpose&.state_of(self)
end

#stock_platePlate?

Deprecated.

Do not use this for new behaviour.

Attempts to find the 'stock_plate' for the plate. However this is a fairly nebulous concept. Often it means the plate that first entered a pipeline, but in other cases it can be the XP plate part way through the process. Further complication comes from tubes which pool across multiple plates, where identifying a single stock plate is meaningless. In other scenarios, you split plates out again and the asset link graph is insufficient.

JG: 2021-02-11: See https://github.com/sanger/sequencescape/issues/3040 for more information

Returns:

  • (Plate, nil)

    The stock plate if found



328
329
330
# File 'app/models/plate.rb', line 328

def stock_plate
  @stock_plate ||= stock_plate? ? self : lookup_stock_plate
end

#stock_plate?Boolean

Returns:

  • (Boolean)


306
307
308
309
310
# File 'app/models/plate.rb', line 306

def stock_plate?
  return true if plate_purpose.nil?

  plate_purpose.stock_plate? && plate_purpose.attached?(self)
end

#stock_wellsObject

This method returns a map from the wells on the plate to their stock well.



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'app/models/plate.rb', line 384

def stock_wells # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
  # Optimisation: if the plate is a stock plate then it's wells are it's stock wells!]
  if stock_plate?
    wells.with_pool_id.index_with { |w| [w] }
  else
    wells
      .include_stock_wells
      .with_pool_id
      .each_with_object({}) do |w, store|
        storted_stock_wells = w.stock_wells.sort_by { |sw| sw.map.column_order }
        store[w] = storted_stock_wells unless storted_stock_wells.empty?
      end
      .tap { |stock_wells_hash| raise "No stock plate associated with #{id}" if stock_wells_hash.empty? }
  end
end

#subject_typeObject



450
451
452
# File 'app/models/plate.rb', line 450

def subject_type
  'plate'
end

#submission_idsObject



172
173
174
# File 'app/models/plate.rb', line 172

def submission_ids
  @submission_ids ||= in_progress_submissions.ids
end

#submission_ids_as_sourceObject



176
177
178
# File 'app/models/plate.rb', line 176

def submission_ids_as_source
  @submission_ids_as_source ||= waiting_submissions.ids
end

#submissionsObject



186
187
188
# File 'app/models/plate.rb', line 186

def submissions
  waiting_submissions.presence || in_progress_submissions
end

#teamObject

Finds the product line (= team) of the requests coming out of this plate's 'stock plate'. Written at a time when requests weren't recorded on the aliquot, so could be re-written in a less convoluted way.



435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'app/models/plate.rb', line 435

def team
  ProductLine
    .joins(
      [
        'INNER JOIN request_types ON request_types.product_line_id = product_lines.id',
        'INNER JOIN requests ON requests.request_type_id = request_types.id',
        'INNER JOIN well_links ON well_links.source_well_id = requests.asset_id AND well_links.type = "stock"',
        'INNER JOIN receptacles AS re ON re.id = well_links.target_well_id'
      ]
    )
    .find_by(['re.labware_id = ?', id])
    .try(:name) || 'UNKNOWN'
end

#unique_positions_on_plateObject



363
364
365
# File 'app/models/plate.rb', line 363

def unique_positions_on_plate
  maps.distinct.pluck(:description)
end

#update_qc_values_with_parser(parser) ⇒ Object



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'app/models/plate.rb', line 412

def update_qc_values_with_parser(parser)
  ActiveRecord::Base.transaction do
    qc_assay = QcAssay.new
    parser.each_well_and_parameters do |position, well_updates|
      # We might have a nil well if a plate was only partially cherrypicked
      well = well_hash[position] || next
      well_updates.each do |attribute, value|
        QcResult.create!(
          asset: well,
          key: attribute,
          unit_value: value,
          assay_type: parser.assay_type,
          assay_version: parser.assay_version,
          qc_assay: qc_assay
        )
      end
    end
  end
  true
end

#update_volume(volume_change) ⇒ Void

Modifies the recorded volume information of all wells on a plate by volume_change

Parameters:

  • volume_change (Numeric)

    The adjustment to apply to all wells (in ul). Negative values reduce the target volume, positive values increase it.

Returns:

  • (Void)


140
141
142
# File 'app/models/plate.rb', line 140

def update_volume(volume_change)
  ActiveRecord::Base.transaction { wells.each { |well| well.update_volume(volume_change) } }
end

#well_hashObject



408
409
410
# File 'app/models/plate.rb', line 408

def well_hash
  @well_hash ||= wells.include_map.includes(:well_attribute).index_by(&:map_description)
end

#wells_in_column_orderObject



479
480
481
# File 'app/models/plate.rb', line 479

def wells_in_column_order
  wells.loaded? ? wells.sort_by(&:column_order) : wells.in_column_major_order
end

#wells_in_row_orderObject



475
476
477
# File 'app/models/plate.rb', line 475

def wells_in_row_order
  wells.loaded? ? wells.sort_by(&:row_order) : wells.in_row_major_order
end

#widthObject



379
380
381
# File 'app/models/plate.rb', line 379

def width
  asset_shape&.plate_width(size)
end