Class: Batch

Overview

A Batch groups 1 or more requests together to enable processing in a Pipeline. All requests in a batch get usually processed together, although it is possible for requests to get removed from a batch in a handful of cases.

Defined Under Namespace

Modules: PipelineBehaviour, PolyMetadataBehaviour, RequestBehaviour, StateMachineBehaviour Classes: RequestFailAndRemover

Constant Summary collapse

DEFAULT_VOLUME =

The three states of Batch Also @see SequencingQcBatch

13

Constants included from UnderRepWellCommentsToBroadcast

UnderRepWellCommentsToBroadcast::UNDER_REPRESENTED_KEY

Constants included from StandardNamedScopes

StandardNamedScopes::SORT_FIELDS, StandardNamedScopes::SORT_ORDERS

Constants included from SequencingQcBatch

SequencingQcBatch::VALID_QC_STATES

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from EventfulRecord

has_many_events, has_many_lab_events, has_one_event_with_family

Methods included from PolyMetadataBehaviour

#automatic_buffer_addition?, #buffer_volume_for_empty_wells, #plate_template_for_buffer_addition

Methods included from HasPolyMetadata

#get_poly_metadata, #set_poly_metadata

Methods included from UnderRepWellCommentsToBroadcast

#comments, #request_with_under_represented_wells, #under_represented_well_comments

Methods included from StateMachineBehaviour

#complete_with_user!, #editable?, #finished?, included, #release_with_user!, #start_with_user!

Methods included from PipelineBehaviour

#has_item_limit?, included, #last_completed_task

Methods included from StandardNamedScopes

included

Methods included from Uuid::Uuidable

included, #unsaved_uuid!, #uuid

Methods included from Commentable

#after_comment_addition

Methods included from SequencingQcBatch

adjacent_state_helper, included, #processing_in_manual_qc?, #qc_manual_in_progress, #qc_previous_state!, #qc_states, state_transition_helper

Methods included from Api::BatchIo::Extensions

included

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

Instance Attribute Details

#production_stateObject

Also referenced in StateMachineBehaviour. Either nil, or fail. This is updated in Batch#fail_requests and Batch#fail. The former is used via BatchesController#fail_items, the latter seems to be unused. Is intended to take precedence over both other states to track failures in-spite of QC results.



34
# File 'app/models/batch.rb', line 34

DEFAULT_VOLUME = 13

#qc_stateObject

Primarily for sequencing batches. See SequencingQcBatch. Holds the sequencing QC state



34
# File 'app/models/batch.rb', line 34

DEFAULT_VOLUME = 13

#stateObject

The main state machine, used to track the batch through the pipeline. Handled by StateMachineBehaviour



34
# File 'app/models/batch.rb', line 34

DEFAULT_VOLUME = 13

Class Method Details

.barcode_without_pick_number(code) ⇒ Object



546
547
548
# File 'app/models/batch.rb', line 546

def self.barcode_without_pick_number(code)
  code.split('-').first
end

.extract_pick_number(code) ⇒ Object



550
551
552
553
554
555
556
557
# File 'app/models/batch.rb', line 550

def self.extract_pick_number(code)
  # expecting format 550000555760-1 with pick number at end
  split_code = code.split('-')
  return Integer(split_code.last) if split_code.size > 1

  # default to 1 if the pick number is not present
  1
end

.find_by_barcode(code) ⇒ Object Also known as: find_from_barcode



560
561
562
563
564
565
566
567
# File 'app/models/batch.rb', line 560

def find_by_barcode(code)
  split_code = barcode_without_pick_number(code)
  human_batch_barcode = Barcode.number_to_human(split_code)
  batch = Batch.find_by(barcode: human_batch_barcode)
  batch ||= Batch.find_by(id: human_batch_barcode)

  batch
end

.prefixObject



529
530
531
# File 'app/models/batch.rb', line 529

def self.prefix
  'BA'
end

.valid_barcode?(code) ⇒ Boolean

Returns:

  • (Boolean)


533
534
535
536
537
538
539
540
541
542
543
544
# File 'app/models/batch.rb', line 533

def self.valid_barcode?(code)
  begin
    split_code = barcode_without_pick_number(code)
    Barcode.barcode_to_human!(split_code, prefix)
  rescue StandardError
    return false
  end

  return false if find_from_barcode(code).nil?

  true
end

Instance Method Details

#all_requests_are_ready?Boolean

Returns:

  • (Boolean)


120
121
122
123
124
# File 'app/models/batch.rb', line 120

def all_requests_are_ready?
  # Checks that SequencingRequests have at least one LibraryCreationRequest in passed status before being processed
  # (as referred by #75102998)
  errors.add :base, 'All requests must be ready to be added to a batch' unless requests.all?(&:ready?)
end

#assign_positions_to_requests!(request_ids_in_position_order) ⇒ Object

Sets the position of the requests in the batch to their index in the supplied array.

Raises:

  • (StandardError)


217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'app/models/batch.rb', line 217

def assign_positions_to_requests!(request_ids_in_position_order)
  request_ids_in_batch = batch_requests.map(&:request_id)
  # checking for both missing and extra requests
  missing_requests = request_ids_in_batch.any? { |id| request_ids_in_position_order.exclude?(id) }
  extra_requests = request_ids_in_position_order.any? { |id| request_ids_in_batch.exclude?(id) }
  raise StandardError, 'Can only sort all the requests in the batch at once' if missing_requests || extra_requests

  BatchRequest.transaction do
    batch_requests.each do |batch_request|
      batch_request.move_to_position!(request_ids_in_position_order.index(batch_request.request_id) + 1)
    end
  end
end

#assigned_userObject



233
234
235
# File 'app/models/batch.rb', line 233

def assigned_user
  assignee.try(:login) || ''
end

#batch_id_matches(scanned_batch_id) ⇒ Object



377
378
379
# File 'app/models/batch.rb', line 377

def batch_id_matches(scanned_batch_id)
  scanned_batch_id == id.to_s
end

#controlObject



201
202
203
# File 'app/models/batch.rb', line 201

def control
  requests.detect { |request| request.try(:asset).try(:resource?) }
end

#detach_request(request, current_user = nil) ⇒ Object

Remove a request from the batch and reset it to a point where it can be put back into the pending queue.



402
403
404
405
406
407
408
409
# File 'app/models/batch.rb', line 402

def detach_request(request, current_user = nil)
  ActiveRecord::Base.transaction do
    unless current_user.nil?
      request.add_comment("Used to belong to Batch #{id} removed at #{Time.zone.now}", current_user)
    end
    pipeline.detach_request_from_batch(self, request)
  end
end

#displayed_statusObject

Summarise the state encapsulated by state and production_state Essentially a 'fail' production_state over-rides the 'state' We don't use production_state directly as it it 'fail' rather than ' failed' qc_state it kept separate as its a fairly distinct concept and is summarised elsewhere in the interface.



602
603
604
# File 'app/models/batch.rb', line 602

def displayed_status
  failed? ? 'failed' : state
end

#downstream_requests_needing_asset(request) {|next_requests_needing_asset| ... } ⇒ Object

Yields:

  • (next_requests_needing_asset)


583
584
585
586
# File 'app/models/batch.rb', line 583

def downstream_requests_needing_asset(request)
  next_requests_needing_asset = request.next_requests.select { |r| r.asset_id.blank? }
  yield(next_requests_needing_asset) if next_requests_needing_asset.present?
end

#event_with_description(name) ⇒ Object



189
190
191
# File 'app/models/batch.rb', line 189

def event_with_description(name)
  lab_events.order(id: :desc).find_by(description: name)
end

#eventful_studiesObject



130
131
132
# File 'app/models/batch.rb', line 130

def eventful_studies
  requests.reduce([]) { |studies, request| studies.concat(request.eventful_studies) }.uniq
end

#fail(reason, comment, ignore_requests = false) ⇒ Object

Fail was removed from State Machine (as a state) to allow the addition of qc_state column and features

Raises:

  • (StandardError)


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'app/models/batch.rb', line 139

def fail(reason, comment, ignore_requests = false)
  # We've deprecated the ability to fail a batch but not its requests.
  # Keep this check here until we're sure we haven't missed anything.
  raise StandardError, 'Can not fail batch without failing requests' if ignore_requests

  # create failures
  failures.create(reason: reason, comment: comment, notify_remote: false)

  requests.each do |request|
    request.failures.create(reason: reason, comment: comment, notify_remote: true)
    EventSender.send_fail_event(request, reason, comment, id) unless request.asset && request.asset.resource?
  end

  self.production_state = 'fail'
  save!
end

#fail_requests(requests_to_fail, reason, comment, fail_but_charge = false) ⇒ Object

Fail specific requests on this batch



157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'app/models/batch.rb', line 157

def fail_requests(requests_to_fail, reason, comment, fail_but_charge = false) # rubocop:todo Metrics/MethodLength
  ActiveRecord::Base.transaction do
    requests
      .find(requests_to_fail)
      .each do |request|
        logger.debug "SENDING FAIL FOR REQUEST #{request.id}, BATCH #{id}, WITH REASON #{reason}"

        request.customer_accepts_responsibility! if fail_but_charge
        request.failures.create(reason: reason, comment: comment, notify_remote: true)
        EventSender.send_fail_event(request, reason, comment, id)
      end
    update_batch_state(reason, comment)
  end
end

#failed?Boolean

Returns:

  • (Boolean)


180
181
182
# File 'app/models/batch.rb', line 180

def failed?
  production_state == 'fail'
end

#first_output_plateObject



276
277
278
# File 'app/models/batch.rb', line 276

def first_output_plate
  Plate.output_by_batch(self).with_wells_and_requests.first
end

#flowcellObject



134
135
136
# File 'app/models/batch.rb', line 134

def flowcell
  self if sequencing?
end

#has_control?Boolean

Returns:

  • (Boolean)


205
206
207
# File 'app/models/batch.rb', line 205

def has_control?
  control.present?
end

#has_event(event_name) ⇒ Object

Tests whether this Batch has any associated LabEvents



185
186
187
# File 'app/models/batch.rb', line 185

def has_event(event_name)
  lab_events.any? { |event| event_name.downcase == event.description.try(:downcase) }
end

#id_dupObject



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

def id_dup
  id
end

#input_labware_reportLabware::ActiveRecord_Relation

Returns a list of input labware including their barcodes, purposes, and a count of the number of requests associated with the batch. Output depends on Pipeline. Some pipelines return an empty relationship

Returns:

  • (Labware::ActiveRecord_Relation)

    The associated labware



246
247
248
# File 'app/models/batch.rb', line 246

def input_labware_report
  pipeline.input_labware requests
end

#input_plate_groupObject



259
260
261
# File 'app/models/batch.rb', line 259

def input_plate_group
  source_assets.group_by(&:plate)
end

#npg_set_stateObject



575
576
577
578
579
580
581
# File 'app/models/batch.rb', line 575

def npg_set_state
  if all_requests_qced?
    self.state = 'released'
    qc_complete
    save!
  end
end

#output_labware_reportLabware::ActiveRecord_Relation

Returns a list of output labware including their barcodes, purposes, and a count of the number of requests associated with the batch. Output depends on Pipeline. Some pipelines return an empty relationship

Returns:

  • (Labware::ActiveRecord_Relation)

    The associated labware



255
256
257
# File 'app/models/batch.rb', line 255

def output_labware_report
  pipeline.output_labware requests.with_target
end

#output_plate_groupObject

This looks odd. Why would a request have the same asset as target asset? Why are we filtering them out here?



264
265
266
# File 'app/models/batch.rb', line 264

def output_plate_group
  requests.select { |r| r.target_asset != r.asset }.map(&:target_asset).select(&:present?).group_by(&:plate)
end

#output_plate_purposeObject



280
281
282
# File 'app/models/batch.rb', line 280

def output_plate_purpose
  output_plates[0].plate_purpose unless output_plates[0].nil?
end

#output_plate_roleObject



284
285
286
# File 'app/models/batch.rb', line 284

def output_plate_role
  requests.first.try(:role)
end

#output_platesObject



268
269
270
271
272
273
274
# File 'app/models/batch.rb', line 268

def output_plates
  # We use re-order here as batch_requests applies a default sort order to
  # the relationship, which takes preference, even though we're has_many throughing
  return output_labware.sort_by(&:id) if output_labware.loaded?

  output_labware.reorder(id: :asc)
end

#pick_information?Boolean

Returns:

  • (Boolean)


592
593
594
# File 'app/models/batch.rb', line 592

def pick_information?
  pipeline.pick_information?(self)
end

#plate_barcode(barcode) ⇒ Object



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

def plate_barcode(barcode)
  barcode.presence || requests.first.target_asset.plate.human_barcode
end

#plate_group_barcodesObject



288
289
290
291
292
# File 'app/models/batch.rb', line 288

def plate_group_barcodes
  return nil unless pipeline.group_by_parent || requests.first.target_asset.is_a?(Well)

  output_plate_group.presence || input_plate_group
end

#plate_ids_in_study(study) ⇒ Object

rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity



506
507
508
# File 'app/models/batch.rb', line 506

def plate_ids_in_study(study)
  Plate.plate_ids_from_requests(requests.for_studies(study))
end

#rebroadcastObject



588
589
590
# File 'app/models/batch.rb', line 588

def rebroadcast
  messengers.each(&:resend)
end

#release_pending_requestsObject



381
382
383
384
385
# File 'app/models/batch.rb', line 381

def release_pending_requests
  # We set the unused requests to pending.
  # this is to allow unused well to be cherry-picked again
  requests.each { |request| detach_request(request) if request.started? }
end

#remove_request_ids(request_ids, reason = nil, comment = nil) ⇒ Object

Remove the request from the batch and remove asset information



388
389
390
391
392
393
394
395
396
397
398
# File 'app/models/batch.rb', line 388

def remove_request_ids(request_ids, reason = nil, comment = nil)
  ActiveRecord::Base.transaction do
    Request
      .find(request_ids)
      .each do |request|
        request.failures.create(reason: reason, comment: comment, notify_remote: true)
        detach_request(request)
      end
    update_batch_state(reason, comment)
  end
end

#request_countObject



571
572
573
# File 'app/models/batch.rb', line 571

def request_count
  requests.count
end

#reset!(current_user) ⇒ Object

rubocop:todo Metrics/MethodLength



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'app/models/batch.rb', line 424

def reset!(current_user) # rubocop:todo Metrics/AbcSize
  ActiveRecord::Base.transaction do
    discard!

    requests.each do |request|
      request.batch = nil
      return_request_to_inbox(request, current_user)
    end

    if requests.last.submission_id.present?
      Request
        .where(submission_id: requests.last.submission_id, state: 'pending')
        .where.not(request_type_id: pipeline.request_type_ids)
        .find_each do |request|
          request.asset_id = nil
          request.save!
        end
    end
  end
end

#return_request_to_inbox(request, current_user = nil) ⇒ Object



411
412
413
414
415
416
417
418
419
420
421
# File 'app/models/batch.rb', line 411

def return_request_to_inbox(request, current_user = nil)
  ActiveRecord::Base.transaction do
    unless current_user.nil?
      request.add_comment(
        "Used to belong to Batch #{id} returned to inbox unstarted at #{Time.zone.now}",
        current_user
      )
    end
    request.return_pending_to_inbox!
  end
end

#robot_idObject



193
194
195
# File 'app/models/batch.rb', line 193

def robot_id
  event_with_description('Cherrypick Layout Set')&.descriptor_value('robot_id')
end

#robot_verified!(user_id) ⇒ Object



518
519
520
521
522
523
524
525
526
527
# File 'app/models/batch.rb', line 518

def robot_verified!(user_id)
  return if has_event('robot verified')

  pipeline.robot_verified!(self)
  lab_events.create(
    description: 'Robot verified',
    message: 'Robot verification completed and source volumes updated.',
    user_id: user_id
  )
end

#set_position_based_on_asset_barcodeObject

Sets the position of the requests in the batch based on their asset barcodes. This was done at Lab request to make it easier to order the tubes in the batch.



211
212
213
214
# File 'app/models/batch.rb', line 211

def set_position_based_on_asset_barcode
  request_ids_in_position_order = requests.sort_by { |r| r.asset.human_barcode }.map(&:id)
  assign_positions_to_requests!(request_ids_in_position_order)
end

#source_labwareObject

Source Labware returns the physical pieces of labware (ie. a plate for wells, but tubes for tubes)



303
304
305
# File 'app/models/batch.rb', line 303

def source_labware
  input_labware
end

#start_requestsObject



237
238
239
# File 'app/models/batch.rb', line 237

def start_requests
  requests.with_assets_for_starting_requests.not_failed.map(&:start!)
end

#subject_typeObject



126
127
128
# File 'app/models/batch.rb', line 126

def subject_type
  sequencing? ? 'flowcell' : 'batch'
end

#swap(current_user, batch_info = {}) ⇒ Object

rubocop:todo Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize



448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# File 'app/models/batch.rb', line 448

def swap(current_user, batch_info = {}) # rubocop:todo Metrics/CyclomaticComplexity
  return false if batch_info.empty?

  # Find the two lanes that are to be swapped
  batch_request_left =
    BatchRequest.find_by(batch_id: batch_info['batch_1']['id'], position: batch_info['batch_1']['lane']) or
    errors.add('Swap: ', 'The first lane cannot be found')
  batch_request_right =
    BatchRequest.find_by(batch_id: batch_info['batch_2']['id'], position: batch_info['batch_2']['lane']) or
    errors.add('Swap: ', 'The second lane cannot be found')
  return unless batch_request_left.present? && batch_request_right.present?

  ActiveRecord::Base.transaction do
    # Update the lab events for the request so that they reference the batch that the request is moving to
    batch_request_left.request.lab_events.each do |event|
      event.update!(batch_id: batch_request_right.batch_id) if event.batch_id == batch_request_left.batch_id
    end
    batch_request_right.request.lab_events.each do |event|
      event.update!(batch_id: batch_request_left.batch_id) if event.batch_id == batch_request_right.batch_id
    end

    # Swap the two batch requests so that they are correct.  This involves swapping both the batch and the lane but
    # ensuring that the two requests don't clash on position by removing one of them.
    original_left_batch_id, original_left_position, original_right_request_id =
      batch_request_left.batch_id,
      batch_request_left.position,
      batch_request_right.request_id
    batch_request_right.destroy
    batch_request_left.update!(batch_id: batch_request_right.batch_id, position: batch_request_right.position)
    batch_request_right =
      BatchRequest.create!(
        batch_id: original_left_batch_id,
        position: original_left_position,
        request_id: original_right_request_id
      )

    # Finally record the fact that the batch was swapped
    batch_request_left.batch.lab_events.create!(
      description: 'Lane swap',
      message:
        "Lane #{batch_request_right.position} moved to #{batch_request_left.batch_id} " \
        "lane #{batch_request_left.position}",
      user_id: current_user.id
    )
    batch_request_right.batch.lab_events.create!(
      description: 'Lane swap',
      message:
        "Lane #{batch_request_left.position} moved to #{batch_request_right.batch_id} " \
        "lane #{batch_request_right.position}",
      user_id: current_user.id
    )
  end

  true
end

#total_volume_to_cherrypickObject



510
511
512
513
514
515
516
# File 'app/models/batch.rb', line 510

def total_volume_to_cherrypick
  request = requests.first
  return DEFAULT_VOLUME unless request.asset.is_a?(Well)
  return DEFAULT_VOLUME unless request.target_asset.is_a?(Well)

  request.target_asset.get_requested_volume
end

#tube_barcode_matches(request, scanned_barcode) ⇒ Object



373
374
375
# File 'app/models/batch.rb', line 373

def tube_barcode_matches(request, scanned_barcode)
  scanned_barcode == request.asset.machine_barcode || scanned_barcode == request.asset.human_barcode
end

#underrunObject



197
198
199
# File 'app/models/batch.rb', line 197

def underrun
  has_limit? ? (item_limit - batch_requests.size) : 0
end

#update_batch_state(reason, comment) ⇒ Object



172
173
174
175
176
177
178
# File 'app/models/batch.rb', line 172

def update_batch_state(reason, comment)
  if requests.all?(&:terminated?)
    failures.create(reason: reason, comment: comment, notify_remote: false)
    self.production_state = 'fail'
    save!
  end
end

#verify_amp_plate_layout(barcodes, user = nil) ⇒ Bool

Used in the Ultima sequencing pipelines to check AMP plates are in the correct position.

Verifies that provided barcodes are in the correct locations according to the request 'position' within the batch. Logs an event if the layout is correct.

Parameters:

  • barcodes (Array<Integer>)

    An array of AMP plate barcodes, assumed ordered by position

  • user (User) (defaults to: nil)

    The user validating the barcode layout

Returns:

  • (Bool)

    true if the layout is correct, false otherwise



351
352
353
354
355
356
357
358
359
360
# File 'app/models/batch.rb', line 351

def verify_amp_plate_layout(barcodes, user = nil)
  requests.each { |request| verify_amp_plate_position(request, barcodes) }

  if errors.empty?
    lab_events.create(description: 'AMP plate layout verified', user: user)
    true
  else
    false
  end
end

#verify_amp_plate_position(request, barcodes) ⇒ Object



362
363
364
365
366
367
368
369
370
371
# File 'app/models/batch.rb', line 362

def verify_amp_plate_position(request, barcodes)
  divider = '-'
  scanned_barcode = barcodes[request.position - 1]
  scanned_batch_id, tube_barcode = scanned_barcode.split(divider)

  unless batch_id_matches(scanned_batch_id) && tube_barcode_matches(request, tube_barcode)
    expected_barcode = "#{id}#{divider}#{request.asset.human_barcode}"
    errors.add(:base, "The barcode at position #{request.position} is incorrect: expected #{expected_barcode}.")
  end
end

#verify_tube_layout(barcodes, user = nil) ⇒ Bool

Used in Sequencing pipelines to check tubes are in the correct position on the flowcell.

Verifies that provided barcodes are in the correct locations according to the request 'position' within the batch. Logs an event if the layout is correct.

Parameters:

  • barcodes (Array<Integer>)

    An array of 1-7 digit long barcodes, assumed ordered by position

  • user (User) (defaults to: nil)

    The user validating the barcode layout

Returns:

  • (Bool)

    true if the layout is correct, false otherwise



319
320
321
322
323
324
325
326
327
328
# File 'app/models/batch.rb', line 319

def verify_tube_layout(barcodes, user = nil)
  requests.each { |request| verify_tube_position(request, barcodes) }

  if errors.empty?
    lab_events.create(description: 'Tube layout verified', user: user)
    true
  else
    false
  end
end

#verify_tube_position(request, barcodes) ⇒ Object



330
331
332
333
334
335
336
337
# File 'app/models/batch.rb', line 330

def verify_tube_position(request, barcodes)
  scanned_barcode = barcodes[request.position - 1]

  unless tube_barcode_matches(request, scanned_barcode)
    expected_barcode = request.asset.human_barcode
    errors.add(:base, "The tube at position #{request.position} is incorrect: expected #{expected_barcode}.")
  end
end