Class: Plate

Overview

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 Classes: Creator, CreatorParameters, 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 Metadata

has_metadata, required_tags

Methods included from QcFile::Associations

has_qc_files

Methods included from PlateCreation::CreationChild

included

Methods included from SubmissionPool::Association::Plate

included

Methods included from FluidigmBehaviour

#apply_fluidigm_data, included, #retrieve_fluidigm_data

Methods included from Asset::Ownership::Owned

#change_owner_to, included

Methods included from Barcode::Barcodeable

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

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

#display_name, #generate_name, #human_barcode, #labware, #labwhere_location, #scanned_in_date, #storage_location

Methods included from SharedBehaviour::Named

included

Methods included from AssetLink::Associations

included

Methods included from Uuid::Uuidable

included, #unsaved_uuid!, #uuid

Methods included from LabwareAssociations

#child, #parent

Methods inherited from Asset

#add_parent, #asset_type_for_request_types, #assign_relationships, #barcode_number, #contained_samples, #external_identifier, #get_qc_result_value_for, #has_stock_asset?, #label, #label=, #original_stock_plates, #prefix, #printable?, #printable_target, #register_stock!, #request_types, #role, #spiked_in_buffer, #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

convert_labware_to_receptacle_for, find_by_id_or_name, find_by_id_or_name!

Methods included from Warren::BroadcastMessages

#broadcast, included, #queue_associated_for_broadcast, #queue_for_broadcast, #warren

Class Method Details

.create_with_barcode!(*args, &block) ⇒ Object


356
357
358
359
360
# File 'app/models/plate.rb', line 356

def self.create_with_barcode!(*args, &block)
  attributes = args.extract_options!
  attributes[:sanger_barcode] = safe_sanger_barcode(attributes[:sanger_barcode] || {})
  create!(attributes, &block)
end

.plate_ids_from_requests(requests) ⇒ Object


330
331
332
# File 'app/models/plate.rb', line 330

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

.safe_sanger_barcode(sanger_barcode) ⇒ Object


362
363
364
365
366
367
368
# File 'app/models/plate.rb', line 362

def self.safe_sanger_barcode(sanger_barcode)
  if sanger_barcode[:number].blank? || Barcode.sanger_barcode(sanger_barcode[:prefix], sanger_barcode[:number]).exists?
    { number: PlateBarcode.create.barcode, prefix: sanger_barcode[:prefix] }
  else
    sanger_barcode
  end
end

.search_for_plates(params) ⇒ Object


266
267
268
269
270
271
272
273
# File 'app/models/plate.rb', line 266

def self.search_for_plates(params)
  with_faculty_sponsor_ids(params[:faculty_sponsor_ids] || nil)
    .with_study_id(params[:study_id] || nil)
    .with_plate_purpose_ids(params[:plate_purpose_ids] || nil)
    .created_between(params[:start_date], params[:end_date])
    .filter_by_barcode(params[:barcodes] || nil)
    .distinct
end

Instance Method Details

#after_comment_addition(comment) ⇒ Object


473
474
475
# File 'app/models/plate.rb', line 473

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


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

def all_submission_ids
  submission_ids_as_source.presence || submission_ids
end

#ancestor_of_purpose(ancestor_purpose_id) ⇒ Object


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

def ancestor_of_purpose(ancestor_purpose_id)
  return self if plate_purpose_id == ancestor_purpose_id

  ancestors.order(created_at: :desc).find_by(plate_purpose_id: ancestor_purpose_id)
end

#ancestors_of_purpose(ancestor_purpose_id) ⇒ Object


350
351
352
353
354
# File 'app/models/plate.rb', line 350

def ancestors_of_purpose(ancestor_purpose_id)
  return [self] if plate_purpose_id == ancestor_purpose_id

  ancestors.order(created_at: :desc).where(plate_purpose_id: ancestor_purpose_id)
end

#buffer_required?Boolean

Returns:

  • (Boolean)

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

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)

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

def cherrypick_completed
  plate_purpose.cherrypick_completed(self)
end

#commentsObject


213
214
215
# File 'app/models/plate.rb', line 213

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

#compatible_purposesObject


425
426
427
# File 'app/models/plate.rb', line 425

def compatible_purposes
  PlatePurpose.compatible_with_purpose(purpose)
end

#convert_to(new_purpose) ⇒ Object


421
422
423
# File 'app/models/plate.rb', line 421

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

#detailsObject


322
323
324
# File 'app/models/plate.rb', line 322

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

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


297
298
299
300
301
302
303
# File 'app/models/plate.rb', line 297

def find_well_by_name(well_name)
  if wells.loaded?
    wells.indexed_by_location[well_name]
  else
    wells.located_at_position(well_name).first
  end
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)

465
466
467
# File 'app/models/plate.rb', line 465

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


398
399
400
# File 'app/models/plate.rb', line 398

def height
  asset_shape.plate_height(size)
end

#iterationObject


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

def iteration
  iter = siblings # assets sharing the same parent
         .where(plate_purpose_id: plate_purpose_id, sti_type: 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

#labware_typeObject


88
89
90
# File 'app/models/plate.rb', line 88

def labware_type
  super || PlateType.plate_default_type
end

#mapsObject


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

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

#name_for_labelObject


390
391
392
# File 'app/models/plate.rb', line 390

def name_for_label
  name
end

#number_of_blank_samplesObject


370
371
372
# File 'app/models/plate.rb', line 370

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


123
124
125
# File 'app/models/plate.rb', line 123

def occupied_well_count
  wells.with_contents.count
end

#plate_columnsObject


310
311
312
# File 'app/models/plate.rb', line 310

def plate_columns
  (1..width)
end

#plate_rowsObject


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

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

#plate_typeObject


314
315
316
# File 'app/models/plate.rb', line 314

def plate_type
  labware_type.name
end

#plate_type=(plate_type) ⇒ Object


318
319
320
# File 'app/models/plate.rb', line 318

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

#priorityObject


217
218
219
220
221
# File 'app/models/plate.rb', line 217

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

477
478
479
# File 'app/models/plate.rb', line 477

def related_studies
  studies
end

#sanger_barcode=(attributes) ⇒ Object


469
470
471
# File 'app/models/plate.rb', line 469

def sanger_barcode=(attributes)
  barcodes << Barcode.build_sanger_code39(attributes)
end

#scored?Boolean

Returns:

  • (Boolean)

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

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

#source_plateObject


145
146
147
# File 'app/models/plate.rb', line 145

def source_plate
  purpose&.source_plate(self)
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


103
104
105
# File 'app/models/plate.rb', line 103

def state
  plate_purpose.state_of(self)
end

#stock_plateObject


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

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

#stock_plate?Boolean

Returns:

  • (Boolean)

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

def stock_plate?
  return true if plate_purpose.nil?

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

#stock_roleObject


326
327
328
# File 'app/models/plate.rb', line 326

def stock_role
  well_requests_as_source.first&.role
end

#stock_wellsObject

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


407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'app/models/plate.rb', line 407

def stock_wells
  # Optimisation: if the plate is a stock plate then it's wells are it's stock wells!]
  if stock_plate?
    wells.with_pool_id.each_with_object({}) { |w, store| store[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 do |stock_wells_hash|
      raise "No stock plate associated with #{id}" if stock_wells_hash.empty?
    end
  end
end

#subject_typeObject


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

def subject_type
  'plate'
end

#submission_idsObject


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

def submission_ids
  @submission_ids ||= in_progress_submissions.ids
end

#submission_ids_as_sourceObject


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

def submission_ids_as_source
  @submission_ids_as_source ||= waiting_submissions.ids
end

#submissionsObject


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

def submissions
  waiting_submissions.presence || in_progress_submissions
end

#summary_hashObject


127
128
129
130
131
132
133
# File 'app/models/plate.rb', line 127

def summary_hash
  {
    asset_id: id,
    barcode: { ean13_barcode: ean13_barcode, human_readable: human_barcode },
    occupied_wells: wells.with_aliquots.include_map.map(&:map_description)
  }
end

#teamObject


447
448
449
450
451
452
453
454
# File 'app/models/plate.rb', line 447

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

#transition_to(state, user, contents = nil, customer_accepts_responsibility = false) ⇒ Void

Delegate the change of state to our plate purpose.

Parameters:

  • state (String)

    The desired target state

  • user (User)

    The person to associate with the action (Will take ownership of the plate)

  • contents (nil, Array) (defaults to: nil)

    Array of well locations to update, leave nil for ALL wells

  • customer_accepts_responsibility (Boolean) (defaults to: false)

    The customer proceeded against advice and will still be charged in the the event of a failure

Returns:

  • (Void)

209
210
211
# File 'app/models/plate.rb', line 209

def transition_to(state, user, contents = nil, customer_accepts_responsibility = false)
  purpose.transition_to(self, state, user, contents, customer_accepts_responsibility)
end

#update_qc_values_with_parser(parser) ⇒ Object


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

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)

112
113
114
115
116
# File 'app/models/plate.rb', line 112

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

#valid_positions?(positions) ⇒ Boolean

Returns:

  • (Boolean)

382
383
384
385
386
387
388
# File 'app/models/plate.rb', line 382

def valid_positions?(positions)
  unique_positions_from_caller = positions.sort.uniq
  unique_positions_on_plate = maps.where_description(unique_positions_from_caller)
                                  .distinct
                                  .pluck(:description).sort
  unique_positions_on_plate == unique_positions_from_caller
end

#well_hashObject


429
430
431
# File 'app/models/plate.rb', line 429

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

#wells_in_column_orderObject


489
490
491
492
493
494
495
# File 'app/models/plate.rb', line 489

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

#wells_in_row_orderObject


481
482
483
484
485
486
487
# File 'app/models/plate.rb', line 481

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

#widthObject


402
403
404
# File 'app/models/plate.rb', line 402

def width
  asset_shape.plate_width(size)
end