From 7f5b6b78712ca8a815b8400d64296e21773d80c5 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 12 Oct 2023 23:55:48 +0100 Subject: [PATCH 01/52] Added bed numbers to config --- config/robots.rb | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/config/robots.rb b/config/robots.rb index 6d46be4b8..c63f66eeb 100644 --- a/config/robots.rb +++ b/config/robots.rb @@ -3061,4 +3061,27 @@ } } ) + + # LRC Hamilton Star bed verification + # LRC PBMC Bank to LRC Bank Seq and LRC Bank Spare + custom_robot( + 'hamilton-lrc-pbmc-bank-to-lrc-bank-seq-and-lrc-bank-spare', + name: 'Hamilton LRC PBMC Bank => LRC Bank Seq and LRC Bank Spare', + beds: { + bed(12).barcode => { + purpose: 'LRC PBMC Bank', + states: ['passed'], + label: 'Bed 12' + }, + bed(15).barcode => { + purpose: 'LRC Bank Seq', + states: ['pending'], + label: 'Bed 15', + }, + bed(14).barcode => { + purpose: 'LRC Bank Spare', + states: ['pending'], + label: 'Bed 14', + } + } end From 6fb48d9409c6fa3ad9753fa12ac92e1ef2bfe4b9 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 12 Oct 2023 23:59:40 +0100 Subject: [PATCH 02/52] Fixed syntax in config --- config/robots.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/robots.rb b/config/robots.rb index c63f66eeb..e24887680 100644 --- a/config/robots.rb +++ b/config/robots.rb @@ -3076,12 +3076,13 @@ bed(15).barcode => { purpose: 'LRC Bank Seq', states: ['pending'], - label: 'Bed 15', + label: 'Bed 15' }, bed(14).barcode => { purpose: 'LRC Bank Spare', states: ['pending'], - label: 'Bed 14', + label: 'Bed 14' } } + } end From c739e896b1a2f0b26df9abad99ea6c00d81c41d0 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 13 Oct 2023 00:02:51 +0100 Subject: [PATCH 03/52] Fixed syntax in config 2 --- config/robots.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/robots.rb b/config/robots.rb index e24887680..b82f8b198 100644 --- a/config/robots.rb +++ b/config/robots.rb @@ -3084,5 +3084,5 @@ label: 'Bed 14' } } - } + ) end From 220fcf2cc3026139e8c5b0b61bf8a8b4b4012ff8 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 16 Oct 2023 15:25:52 +0100 Subject: [PATCH 04/52] Removed the relationships section from plate to tube racks robot config --- config/robots.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/config/robots.rb b/config/robots.rb index b82f8b198..3b9ddac79 100644 --- a/config/robots.rb +++ b/config/robots.rb @@ -3076,12 +3076,16 @@ bed(15).barcode => { purpose: 'LRC Bank Seq', states: ['pending'], - label: 'Bed 15' + label: 'Bed 15', + parent: bed(12).barcode, + target_state: 'passed' }, bed(14).barcode => { purpose: 'LRC Bank Spare', states: ['pending'], - label: 'Bed 14' + label: 'Bed 14', + parent: bed(12).barcode, + target_state: 'passed' } } ) From 5ec11f913d516165e353df916feb95a56fc653e3 Mon Sep 17 00:00:00 2001 From: yoldas Date: Wed, 18 Oct 2023 17:39:54 +0100 Subject: [PATCH 05/52] Added relationships back as it allows specifying multiple children from parent --- config/robots.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/config/robots.rb b/config/robots.rb index 3b9ddac79..a67faeb7b 100644 --- a/config/robots.rb +++ b/config/robots.rb @@ -3077,16 +3077,24 @@ purpose: 'LRC Bank Seq', states: ['pending'], label: 'Bed 15', - parent: bed(12).barcode, target_state: 'passed' }, bed(14).barcode => { purpose: 'LRC Bank Spare', states: ['pending'], label: 'Bed 14', - parent: bed(12).barcode, target_state: 'passed' } - } + }, + class: 'Robots::PlateToTubeRacksRobot', + relationships: [ + { + 'type' => 'LRC PBMBC Bank to LRC Bank Seq/Spare', + 'options' => { + 'parent' => bed(12).barcode, + 'children' => [bed(15).barcode, bed(14).barcode] + } + } + ] ) end From 6c56bb39941aea28295275bc5efcead6fdf98c63 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 20 Oct 2023 09:59:38 +0100 Subject: [PATCH 06/52] Fixed typo in robot config --- config/robots.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/robots.rb b/config/robots.rb index a67faeb7b..8538acedb 100644 --- a/config/robots.rb +++ b/config/robots.rb @@ -3089,7 +3089,7 @@ class: 'Robots::PlateToTubeRacksRobot', relationships: [ { - 'type' => 'LRC PBMBC Bank to LRC Bank Seq/Spare', + 'type' => 'LRC PBMC Bank to LRC Bank Seq/Spare', 'options' => { 'parent' => bed(12).barcode, 'children' => [bed(15).barcode, bed(14).barcode] From 79faa2951d86b393f8cff07b86a609ced3137cf0 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 20 Oct 2023 10:00:22 +0100 Subject: [PATCH 07/52] Adding tube rack wrapper to reuse existing validations --- .../robots/bed/plate_to_tube_racks_bed.rb | 41 +++++++++++++++++++ app/models/robots/bed/tube_rack_wrapper.rb | 16 ++++++++ .../robots/plate_to_tube_racks_robot.rb | 36 ++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 app/models/robots/bed/plate_to_tube_racks_bed.rb create mode 100644 app/models/robots/bed/tube_rack_wrapper.rb create mode 100644 app/models/robots/plate_to_tube_racks_robot.rb diff --git a/app/models/robots/bed/plate_to_tube_racks_bed.rb b/app/models/robots/bed/plate_to_tube_racks_bed.rb new file mode 100644 index 000000000..8cececd6b --- /dev/null +++ b/app/models/robots/bed/plate_to_tube_racks_bed.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Robots::Bed + # Plate to tube racks robot beds + class PlateToTubeRacksBed < Robots::Bed::Base + # TODO: Do I need this accessor? + attr_accessor :parents + + PLATE_INCLUDES = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' + + def find_all_labware + Sequencescape::Api::V2::Plate.find_all({ barcode: @barcodes }, includes: PLATE_INCLUDES) + end + + def load_labware_from_parents(parents) + return if labware.present? + @labware = parents.flat_map(&:child_labware).select { |labware| labware.barcode == barcode } + end + + def child_labware + return [] if labware.nil? + + @child_labware ||= child_labware_of_plate + end + + def child_labware_of_plate + labware + .wells + .sort_by(&well_order) + .each_with_object([]) do |well, racks| + next if well.downstream_tubes.empty? + + well.downstream_tubes.each do |tube| + barcode = tube.custom_metadatum_collection.metadata[:tube_rack_barcode] + rack = racks.detect { |rack| rack.barcode == barcode } || racks.push(TubeRackWrapper.new(barcode)).last + rack.tubes << tube + end + end + end + end +end diff --git a/app/models/robots/bed/tube_rack_wrapper.rb b/app/models/robots/bed/tube_rack_wrapper.rb new file mode 100644 index 000000000..784c0426d --- /dev/null +++ b/app/models/robots/bed/tube_rack_wrapper.rb @@ -0,0 +1,16 @@ +module Robots::Bed + # Tube rack info from tube metadata + class TubeRackWrapper + attr_accessor :barcode, :tubes + delegate :purpose_name, :state, :uuid, to: :first_tube, allow_nil: true + + def initialize(barcode, tubes: []) + @barcode = barcode + @tubes = tubes + end + + def first_tube + @tubes.first + end + end +end diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb new file mode 100644 index 000000000..58a2737c2 --- /dev/null +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Robots + class PlateToTubeRacksRobot < Robots::Robot + attr_writer :relationships + + def bed_class + Robots::Bed::PlateToTubeRacksBed + end + + def bed_labwares=(bed_labwares) + super + parents = beds.values.select { |bed| bed.labware && bed.child_labware } + beds.values.each { |bed| bed.load_labware_from_parents(parents) if bed.labware.blank? } + end + + def valid_relationships # rubocop:todo Metrics/AbcSize + raise StandardError, "Relationships for #{name} are empty" if @relationships.empty? + + @relationships.each_with_object({}) do |relationship, validations| + parent_bed = relationship.dig('options', 'parent') + child_beds = relationship.dig('options', 'children') + + validations[parent_bed] = beds[parent_bed].child_labware.present? + error(beds[parent_bed], 'should not be empty.') if beds[parent_bed].empty? + error(beds[parent_bed], 'should have children.') if beds[parent_bed].child_labware.empty? + + expected_children = beds[parent_bed].child_labware + expected_children.each_with_index do |expected_child, index| + child_bed = child_beds[index] + validations[child_bed] = check_labware_identity([expected_child], child_bed) + end + end + end + end +end From e10f3ba122292c69454b1e695e392716d5620625 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 22 Oct 2023 03:31:15 +0100 Subject: [PATCH 08/52] Fixed error message for created_with_robot metadata --- app/controllers/robots_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/robots_controller.rb b/app/controllers/robots_controller.rb index bd716e6d8..c956f825b 100644 --- a/app/controllers/robots_controller.rb +++ b/app/controllers/robots_controller.rb @@ -34,7 +34,7 @@ def start # rubocop:todo Metrics/AbcSize .update!(created_with_robot: params[:robot_barcode]) rescue Sequencescape::Api::ResourceNotFound respond_to do |format| - format.html { redirect_to robot_path(id: @robot.id), notice: "Plate #{plate_barcode} not found." } + format.html { redirect_to robot_path(id: @robot.id), notice: "Labware #{labware_barcode} not found." } end end end From 5e0561b58d56c3574757cd012e819d28c07e2313 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 23 Oct 2023 10:21:01 +0100 Subject: [PATCH 09/52] Changes for bed verification with tube racks so far --- app/controllers/robots_controller.rb | 10 +++++++--- app/models/robots/bed/plate_to_tube_racks_bed.rb | 10 ++++++++-- app/models/robots/bed/tube_rack_wrapper.rb | 3 ++- app/models/robots/plate_to_tube_racks_robot.rb | 6 ++++-- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/controllers/robots_controller.rb b/app/controllers/robots_controller.rb index c956f825b..cdfbef04e 100644 --- a/app/controllers/robots_controller.rb +++ b/app/controllers/robots_controller.rb @@ -29,9 +29,13 @@ def start # rubocop:todo Metrics/AbcSize labware_barcode = bed.labware.barcode.machine begin - LabwareMetadata - .new(api: api, user: current_user_uuid, barcode: labware_barcode) - .update!(created_with_robot: params[:robot_barcode]) + if bed.respond_to?(:set_created_with_robot) + bed.set_created_with_robot(params[:robot_barcode]) + else + LabwareMetadata + .new(api: api, user: current_user_uuid, barcode: labware_barcode) + .update!(created_with_robot: params[:robot_barcode]) + end rescue Sequencescape::Api::ResourceNotFound respond_to do |format| format.html { redirect_to robot_path(id: @robot.id), notice: "Labware #{labware_barcode} not found." } diff --git a/app/models/robots/bed/plate_to_tube_racks_bed.rb b/app/models/robots/bed/plate_to_tube_racks_bed.rb index 8cececd6b..887b124e9 100644 --- a/app/models/robots/bed/plate_to_tube_racks_bed.rb +++ b/app/models/robots/bed/plate_to_tube_racks_bed.rb @@ -8,13 +8,15 @@ class PlateToTubeRacksBed < Robots::Bed::Base PLATE_INCLUDES = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' + def set_created_with_robot(robot_barcode); end + def find_all_labware Sequencescape::Api::V2::Plate.find_all({ barcode: @barcodes }, includes: PLATE_INCLUDES) end def load_labware_from_parents(parents) return if labware.present? - @labware = parents.flat_map(&:child_labware).select { |labware| labware.barcode == barcode } + @labware = parents.flat_map(&:child_labware).select { |labware| labware.barcode.human == barcode } end def child_labware @@ -32,7 +34,11 @@ def child_labware_of_plate well.downstream_tubes.each do |tube| barcode = tube.custom_metadatum_collection.metadata[:tube_rack_barcode] - rack = racks.detect { |rack| rack.barcode == barcode } || racks.push(TubeRackWrapper.new(barcode)).last + rack = racks.detect { |rack| rack.barcode.human == barcode } + if rack.nil? + labware_barcode = LabwareBarcode.new(human: barcode, machine: barcode) + rack = racks.push(TubeRackWrapper.new(labware_barcode)).last + end rack.tubes << tube end end diff --git a/app/models/robots/bed/tube_rack_wrapper.rb b/app/models/robots/bed/tube_rack_wrapper.rb index 784c0426d..34bd1b089 100644 --- a/app/models/robots/bed/tube_rack_wrapper.rb +++ b/app/models/robots/bed/tube_rack_wrapper.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true module Robots::Bed # Tube rack info from tube metadata class TubeRackWrapper attr_accessor :barcode, :tubes - delegate :purpose_name, :state, :uuid, to: :first_tube, allow_nil: true + delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :first_tube, allow_nil: true def initialize(barcode, tubes: []) @barcode = barcode diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 58a2737c2..d3f5a01e5 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -10,8 +10,10 @@ def bed_class def bed_labwares=(bed_labwares) super - parents = beds.values.select { |bed| bed.labware && bed.child_labware } - beds.values.each { |bed| bed.load_labware_from_parents(parents) if bed.labware.blank? } + parents = beds.values.select { |bed| bed.respond_to?(:labware) && bed.labware && bed.child_labware } + beds.each_value do |bed| + bed.load_labware_from_parents(parents) if bed.respond_to?(:labware) && bed.labware.blank? + end end def valid_relationships # rubocop:todo Metrics/AbcSize From 918d0f2fa0b99d39059f9dc1451235ff4c11f684 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 26 Oct 2023 02:10:59 +0100 Subject: [PATCH 10/52] robot controller start action created_with_robot metadata --- app/controllers/robots_controller.rb | 43 ++++++++++--------- .../robots/{bed => }/tube_rack_wrapper.rb | 0 2 files changed, 23 insertions(+), 20 deletions(-) rename app/models/robots/{bed => }/tube_rack_wrapper.rb (100%) diff --git a/app/controllers/robots_controller.rb b/app/controllers/robots_controller.rb index cdfbef04e..f3ed8a2f3 100644 --- a/app/controllers/robots_controller.rb +++ b/app/controllers/robots_controller.rb @@ -23,26 +23,7 @@ def show def start # rubocop:todo Metrics/AbcSize @robot.perform_transfer(stripped_beds) - if params[:robot_barcode].present? - @robot.beds.each_value do |bed| - next unless bed.transitions? && bed.labware - - labware_barcode = bed.labware.barcode.machine - begin - if bed.respond_to?(:set_created_with_robot) - bed.set_created_with_robot(params[:robot_barcode]) - else - LabwareMetadata - .new(api: api, user: current_user_uuid, barcode: labware_barcode) - .update!(created_with_robot: params[:robot_barcode]) - end - rescue Sequencescape::Api::ResourceNotFound - respond_to do |format| - format.html { redirect_to robot_path(id: @robot.id), notice: "Labware #{labware_barcode} not found." } - end - end - end - end + update_labware_metadata(params[:robot_barcode]) if params[:robot_barcode].present? respond_to { |format| format.html { redirect_to search_path, notice: "Robot #{@robot.name} has been started." } } rescue Robots::Bed::BedError => e # Our beds complained, nothing has happened. @@ -51,6 +32,28 @@ def start # rubocop:todo Metrics/AbcSize end end + def update_labware_metadata(robot_barcode) + @robot.beds.each_value do |bed| + next unless bed.transitions? && bed.labware + labware_barcode = bed.labware.barcode.machine + if bed.respond_to?(:labware_created_with_robot) + bed.labware_created_with_robot(robot_barcode) + else + labware_created_with_robot(labware_barcode, robot_barcode) + end + rescue Sequencescape::Api::ResourceNotFound + respond_to do |format| + format.html { redirect_to robot_path(id: @robot.id), notice: "Labware #{labware_barcode} not found." } + end + end + end + + def labware_created_with_robot(labware_barcode, robot_barcode) + LabwareMetadata + .new(api: api, user: current_user_uuid, barcode: labware_barcode) + .update!(created_with_robot: robot_barcode) + end + def verify render(json: @robot.verify(robot_params)) end diff --git a/app/models/robots/bed/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb similarity index 100% rename from app/models/robots/bed/tube_rack_wrapper.rb rename to app/models/robots/tube_rack_wrapper.rb From 775799d73bdd3356191b2a24216898f1da9debf2 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 26 Oct 2023 02:12:42 +0100 Subject: [PATCH 11/52] Robot finds labware for beds --- .../robots/bed/plate_to_tube_racks_bed.rb | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/app/models/robots/bed/plate_to_tube_racks_bed.rb b/app/models/robots/bed/plate_to_tube_racks_bed.rb index 887b124e9..3fddd151b 100644 --- a/app/models/robots/bed/plate_to_tube_racks_bed.rb +++ b/app/models/robots/bed/plate_to_tube_racks_bed.rb @@ -6,42 +6,23 @@ class PlateToTubeRacksBed < Robots::Bed::Base # TODO: Do I need this accessor? attr_accessor :parents - PLATE_INCLUDES = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' - - def set_created_with_robot(robot_barcode); end - - def find_all_labware - Sequencescape::Api::V2::Plate.find_all({ barcode: @barcodes }, includes: PLATE_INCLUDES) - end - - def load_labware_from_parents(parents) - return if labware.present? - @labware = parents.flat_map(&:child_labware).select { |labware| labware.barcode.human == barcode } + def labware_created_with_robot(robot_barcode) + labware.tubes.each do |tube| + LabwareMetadata + .new(api: api, user: user_uuid, barcode: tube.barcode.machine) + .update!(created_with_robot: robot_barcode) + end end def child_labware - return [] if labware.nil? + return [] if labware.blank? - @child_labware ||= child_labware_of_plate + @child_labware ||= robot.child_labware(labware) end - def child_labware_of_plate - labware - .wells - .sort_by(&well_order) - .each_with_object([]) do |well, racks| - next if well.downstream_tubes.empty? - - well.downstream_tubes.each do |tube| - barcode = tube.custom_metadatum_collection.metadata[:tube_rack_barcode] - rack = racks.detect { |rack| rack.barcode.human == barcode } - if rack.nil? - labware_barcode = LabwareBarcode.new(human: barcode, machine: barcode) - rack = racks.push(TubeRackWrapper.new(labware_barcode)).last - end - rack.tubes << tube - end - end + def load(barcodes) + @barcodes = Array(barcodes).filter_map(&:strip).uniq + @labware = @barcodes.present? ? robot.find_bed_labware(@barcodes) : [] end end end From cdf9d09049ce7d6a45cfded63996c2740859f2b5 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 26 Oct 2023 02:14:00 +0100 Subject: [PATCH 12/52] Initialise labware store in robot and find labware for beds --- .../robots/plate_to_tube_racks_robot.rb | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index d3f5a01e5..a279c53ad 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -4,16 +4,20 @@ module Robots class PlateToTubeRacksRobot < Robots::Robot attr_writer :relationships + PLATE_INCLUDES = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' + def bed_class Robots::Bed::PlateToTubeRacksBed end - def bed_labwares=(bed_labwares) + def perform_transfer(bed_settings) + init_labware_store(bed_settings) + super + end + + def verify(params) + init_labware_store(params[:bed_labwares]) super - parents = beds.values.select { |bed| bed.respond_to?(:labware) && bed.labware && bed.child_labware } - beds.each_value do |bed| - bed.load_labware_from_parents(parents) if bed.respond_to?(:labware) && bed.labware.blank? - end end def valid_relationships # rubocop:todo Metrics/AbcSize @@ -34,5 +38,61 @@ def valid_relationships # rubocop:todo Metrics/AbcSize end end end + + def find_bed_labware(barcodes) + barcodes.filter_map { |barcode| labware_store[barcode] } + end + + def child_labware(plate) + labware_store.values.select { |labware| labware.respond_to?(:parent) && labware.parent.uuid == plate.uuid } + end + + def init_labware_store(bed_labwares) + return if labware_store.present? + stripped_barcodes(bed_labwares).each do |barcode| + plate = find_plate(barcode) + next if plate.blank? + add_plate_to_labware_store(plate) + add_tube_racks_to_labware_store(plate) + end + end + + def stripped_barcodes(bed_labwares) + bed_labwares.values.flatten.filter_map(&:strip).uniq + end + + def add_plate_to_labware_store(plate) + labware_store[plate.barcode.machine] = plate + end + + def add_tube_racks_to_labware_store(plate) + find_tube_racks(plate).each { |rack| labware_store[rack.barcode.machine] = rack } + end + + def labware_store + @labware_store ||= {} + end + + def find_plate(barcode) + Sequencescape::Api::V2::Plate.find_all({ barcode: barcode }, includes: PLATE_INCLUDES).first + end + + def find_tube_racks(plate) + plate + .wells + .sort_by(&well_order) + .each_with_object([]) do |well, racks| + next if well.downstream_tubes.empty? + well.downstream_tubes.each do |tube| + barcode = tube.custom_metadatum_collection.metadata[:tube_rack_barcode] + rack = racks.detect { |tube_rack| tube_rack.barcode.machine == barcode } + if rack.nil? + labware_barcode = LabwareBarcode.new(human: barcode, machine: barcode) + rack = racks.push(TubeRackWrapper.new(labware_barcode, plate)).last + end + rack.tubes << tube + end + end + end end end From d96124d97759feeffa18028e5c1aa2d16b0082bb Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 26 Oct 2023 02:14:42 +0100 Subject: [PATCH 13/52] Added parent field to tube rack wrapper --- app/models/robots/tube_rack_wrapper.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index 34bd1b089..165a4a3b5 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -module Robots::Bed +module Robots # Tube rack info from tube metadata class TubeRackWrapper - attr_accessor :barcode, :tubes + attr_accessor :barcode, :parent, :tubes delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :first_tube, allow_nil: true - def initialize(barcode, tubes: []) + def initialize(barcode, parent, tubes: []) @barcode = barcode + @parent = parent @tubes = tubes end From b9e55a1c91d60e3daa59ec56680444d4024acbb0 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 26 Oct 2023 18:26:12 +0100 Subject: [PATCH 14/52] Added code comments to plate to tube racks robot --- .../robots/plate_to_tube_racks_robot.rb | 118 ++++++++++++++++-- 1 file changed, 110 insertions(+), 8 deletions(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index a279c53ad..d3301b7bd 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -1,20 +1,58 @@ # frozen_string_literal: true module Robots + # This plate to tube racks robot takes one parent plate, and transfers it to + # its child tubes that are on multiple tube racks. The tube racks handled by + # this robot are not actual recorded labware. Their barcodes are extracted + # from the metadata of the tubes and they are accessed using wrapper objects. + # When robot controller calls the robot's verify or perform_transfer actions, + # the robot will first initialize its labware store with the plate and tube + # rack objects. The plate information comes from the Sequencescape API call, + # however the tube rack information comes from the metadata of the downstream + # tubes included in the same API response. Therefore, the bed verification of + # the tube racks depend on the verification of the plate. + # + # The destination tubes racks are distinguished by their barcodes. We assume + # that the tubes on the same tube rack have the same labware purpose. We also + # assume that there cannot be two tube racks with the tubes of the same + # labware purpose. For bed verification, only the etched barcode of the tube + # racks are scanned, not the individual tubes. The number of tube racks to be + # verified not only depends on the robot's configured relationships but also + # whether the plate has children with those purposes. + # class PlateToTubeRacksRobot < Robots::Robot - attr_writer :relationships + attr_writer :relationships # Hash from robot config into @relationships + # Option for including downstream tubes and metadata in Plate API response. PLATE_INCLUDES = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' + # Returns the bed class for this robot. + # + # @return [Class] the bed class def bed_class Robots::Bed::PlateToTubeRacksBed end + # Performs the transfer between plate and tube racks. This method is called + # by the robot controller when the user clicks the start robot button. The + # method first initializes the labware store with the plate and tube racks. + # + # @param [Hash] bed_settings bed_labwares hash from request parameters + # @return [void] + # def perform_transfer(bed_settings) init_labware_store(bed_settings) super end + # Performs the bed verification of plate and tube racks. This method is + # called by the robot controller when the user clicks the validate layout + # button. The method first initializes the labware store with the plate + # and tube racks. + # + # @param [Hash] params request parameters + # @return [void] + # def verify(params) init_labware_store(params[:bed_labwares]) super @@ -39,14 +77,34 @@ def valid_relationships # rubocop:todo Metrics/AbcSize end end + # Returns an array of labware from the robot's labware store for barcodes. + # This method is called by the robot's beds when they need to find their + # labware. The labware returned can be Plate objects or labware-like + # wrapper objects for tube racks. + # + # @param [Array] barcodes array of barcodes + # @return [Array] + # def find_bed_labware(barcodes) barcodes.filter_map { |barcode| labware_store[barcode] } end + # Returns an array of child labware from the robot's labware store for + # the given Plate. + # + # @param [Plate] plate the parent plate + # @return [Array] array of tube rack wrapper objects + # def child_labware(plate) labware_store.values.select { |labware| labware.respond_to?(:parent) && labware.parent.uuid == plate.uuid } end + # Prepares the labware store before handling robot actions. This method is + # called before the robot's bed verification and perform transfer actions. + # + # @param [Hash] bed_labwares hash from request parameters + # @return [void] + # def init_labware_store(bed_labwares) return if labware_store.present? stripped_barcodes(bed_labwares).each do |barcode| @@ -57,42 +115,86 @@ def init_labware_store(bed_labwares) end end + # Returns an array of sanitised barcodes from the bed_labwares hash from + # request parameters. + # + # @param [Hash] bed_labwares hash from request parameters + # @return [Array] array of barcodes + # def stripped_barcodes(bed_labwares) bed_labwares.values.flatten.filter_map(&:strip).uniq end + # Adds the plate to the robot's labware store. + # + # @param [Plate] plate the parent plate + # @return [void] + # def add_plate_to_labware_store(plate) labware_store[plate.barcode.machine] = plate end + # Adds the tube racks wrappers from plate includes to the labware store. + # + # @param [Plate] plate the parent plate + # @return [void] + # def add_tube_racks_to_labware_store(plate) find_tube_racks(plate).each { |rack| labware_store[rack.barcode.machine] = rack } end + # Returns the labware store. The hash is indexed by the labware barcode. + # The values are either Plate objects or labware-like wrapper objects for + # tube racks. + # + # @return [Hash] the labware store + # def labware_store @labware_store ||= {} end + # Returns the Plate for the given barcode from the Sequencescape API. + # The call includes downstream tubes and their metadata as well. + # + # @param [String] barcode the barcode of the plate + # @return [Plate] the plate + # def find_plate(barcode) Sequencescape::Api::V2::Plate.find_all({ barcode: barcode }, includes: PLATE_INCLUDES).first end + # Returns an array of tube rack wrapper objects that from the downstream tubes + # of the given plate. + # + # @param [Plate] plate the parent plate + # @return [Array] array of tube rack wrapper objects + # def find_tube_racks(plate) plate .wells .sort_by(&well_order) .each_with_object([]) do |well, racks| - next if well.downstream_tubes.empty? + next if well.downstream_tubes.blank? well.downstream_tubes.each do |tube| barcode = tube.custom_metadatum_collection.metadata[:tube_rack_barcode] - rack = racks.detect { |tube_rack| tube_rack.barcode.machine == barcode } - if rack.nil? - labware_barcode = LabwareBarcode.new(human: barcode, machine: barcode) - rack = racks.push(TubeRackWrapper.new(labware_barcode, plate)).last - end - rack.tubes << tube + find_or_create_tube_rack(racks, barcode, plate).tubes.push(tube) end end end + + # Returns an existing or new tube rack wrapper object. + # + # @param [Array] racks the tube racks found so far + # @param [String] barcode the barcode of the tube rack + # @param [Plate] plate the parent plate + # + # @return [TubeRackWrapper] the tube rack wrapper object + # + def find_or_create_tube_rack(racks, barcode, plate) + rack = racks.detect { |tube_rack| tube_rack.barcode.machine == barcode } + return rack if rack.present? + labware_barcode = LabwareBarcode.new(human: barcode, machine: barcode) + racks.push(TubeRackWrapper.new(labware_barcode, plate)).last + end end end From 83b0ca2db42eb6c5f3cd7f459c16a095276a0ddc Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 26 Oct 2023 20:26:50 +0100 Subject: [PATCH 15/52] Removed parents attr_accessor from bed --- .../robots/bed/plate_to_tube_racks_bed.rb | 24 +++++++++++++++---- .../robots/plate_to_tube_racks_robot.rb | 13 ++++++---- app/models/robots/tube_rack_wrapper.rb | 15 +++++++++++- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/app/models/robots/bed/plate_to_tube_racks_bed.rb b/app/models/robots/bed/plate_to_tube_racks_bed.rb index 3fddd151b..89c0881c7 100644 --- a/app/models/robots/bed/plate_to_tube_racks_bed.rb +++ b/app/models/robots/bed/plate_to_tube_racks_bed.rb @@ -1,11 +1,18 @@ # frozen_string_literal: true module Robots::Bed - # Plate to tube racks robot beds + # This bed hosts the parent plate or a tube-rack. It uses the robot to find + # its labware and child labware. When it is used the source bed, it should + # host the Plate. When it is used as a destination bed, it should host a + # tube-rack wrapper. class PlateToTubeRacksBed < Robots::Bed::Base - # TODO: Do I need this accessor? - attr_accessor :parents - + # Updates the metadata of the labware with the robot barcode. + # This method is called inside the robot controller's start action for + # tube-rack wrappers and it sets the created_with_robot metadata field. + # + # @param [String] robot_barcode the robot barcode + # @return [void] + # def labware_created_with_robot(robot_barcode) labware.tubes.each do |tube| LabwareMetadata @@ -14,12 +21,21 @@ def labware_created_with_robot(robot_barcode) end end + # Returns an array of labware from the robot's labware store for barcodes. + # + # @return [Array] child tube-rack wrappers + # def child_labware return [] if labware.blank? @child_labware ||= robot.child_labware(labware) end + # Loads labware into this bed. + # + # @param [Array] barcodes array containing the barcode of the labware + # @return [void] + # def load(barcodes) @barcodes = Array(barcodes).filter_map(&:strip).uniq @labware = @barcodes.present? ? robot.find_bed_labware(@barcodes) : [] diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index d3301b7bd..12121ba12 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -12,13 +12,14 @@ module Robots # tubes included in the same API response. Therefore, the bed verification of # the tube racks depend on the verification of the plate. # - # The destination tubes racks are distinguished by their barcodes. We assume + # The destination tube racks are distinguished by their barcodes. We assume # that the tubes on the same tube rack have the same labware purpose. We also # assume that there cannot be two tube racks with the tubes of the same - # labware purpose. For bed verification, only the etched barcode of the tube - # racks are scanned, not the individual tubes. The number of tube racks to be - # verified not only depends on the robot's configured relationships but also - # whether the plate has children with those purposes. + # labware purpose on the robot at the same time. For bed verification, only + # the etched barcode of the tube racks are scanned, not the individual tubes. + # The number of tube racks to be verified not only depends on the robot's + # configured relationships but also whether the plate has children with those + # purposes. # class PlateToTubeRacksRobot < Robots::Robot attr_writer :relationships # Hash from robot config into @relationships @@ -115,6 +116,8 @@ def init_labware_store(bed_labwares) end end + private + # Returns an array of sanitised barcodes from the bed_labwares hash from # request parameters. # diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index 165a4a3b5..ed14a71a1 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -1,16 +1,29 @@ # frozen_string_literal: true module Robots - # Tube rack info from tube metadata + # This wrapper class is for tube racks that are not actual recorded labware. + # The instances acts as a labware wrapper and provides access to the tubes. + # Labware methods are delegated to the first tube on the tube rack. + # class TubeRackWrapper attr_accessor :barcode, :parent, :tubes delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :first_tube, allow_nil: true + # Initializes a new instance of the class. + # + # @param [LabwareBarcode] barcode the barcode object of the tube rack + # @param [Plate] parent the parent plate + # @tubes [Array] the tubes on the tube rack + # def initialize(barcode, parent, tubes: []) @barcode = barcode @parent = parent @tubes = tubes end + # Returns the first tube on the tube rack. This method used for delegating + # certain methods to make it behave like a like a labware object. + # + # @return [Tube] the first tube def first_tube @tubes.first end From ddd94e49d4dc8f32cbc9f5eb8381f5c17049d4dd Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 26 Oct 2023 20:30:54 +0100 Subject: [PATCH 16/52] Updated bed code comments --- app/models/robots/bed/plate_to_tube_racks_bed.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/robots/bed/plate_to_tube_racks_bed.rb b/app/models/robots/bed/plate_to_tube_racks_bed.rb index 89c0881c7..1c22fca72 100644 --- a/app/models/robots/bed/plate_to_tube_racks_bed.rb +++ b/app/models/robots/bed/plate_to_tube_racks_bed.rb @@ -2,9 +2,10 @@ module Robots::Bed # This bed hosts the parent plate or a tube-rack. It uses the robot to find - # its labware and child labware. When it is used the source bed, it should + # its labware and child labware. When it is used as a source bed, it should # host the Plate. When it is used as a destination bed, it should host a # tube-rack wrapper. + # class PlateToTubeRacksBed < Robots::Bed::Base # Updates the metadata of the labware with the robot barcode. # This method is called inside the robot controller's start action for @@ -31,7 +32,7 @@ def child_labware @child_labware ||= robot.child_labware(labware) end - # Loads labware into this bed. + # Loads labware into this bed from the robot's labware store. # # @param [Array] barcodes array containing the barcode of the labware # @return [void] From 3d7b17228ab72eecaa211cdde6c8db0dc58006bd Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 27 Oct 2023 20:14:17 +0100 Subject: [PATCH 17/52] Comment about using machine barcode for setting robot_barcode in metadata --- app/models/robots/bed/plate_to_tube_racks_bed.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/robots/bed/plate_to_tube_racks_bed.rb b/app/models/robots/bed/plate_to_tube_racks_bed.rb index 1c22fca72..2a12dd16b 100644 --- a/app/models/robots/bed/plate_to_tube_racks_bed.rb +++ b/app/models/robots/bed/plate_to_tube_racks_bed.rb @@ -15,6 +15,7 @@ class PlateToTubeRacksBed < Robots::Bed::Base # @return [void] # def labware_created_with_robot(robot_barcode) + # RobotController uses machine barcode for initialising LabwareMetadata labware.tubes.each do |tube| LabwareMetadata .new(api: api, user: user_uuid, barcode: tube.barcode.machine) From 1c5bdc64def249671890b85d2d77bb37a5a40d93 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 27 Oct 2023 20:15:23 +0100 Subject: [PATCH 18/52] Switched to human barcode for indexing --- app/models/robots/plate_to_tube_racks_robot.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 12121ba12..339e3e05e 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -52,14 +52,14 @@ def perform_transfer(bed_settings) # and tube racks. # # @param [Hash] params request parameters - # @return [void] + # @return [Report] # def verify(params) init_labware_store(params[:bed_labwares]) super end - def valid_relationships # rubocop:todo Metrics/AbcSize + def valid_relationships raise StandardError, "Relationships for #{name} are empty" if @relationships.empty? @relationships.each_with_object({}) do |relationship, validations| @@ -134,7 +134,7 @@ def stripped_barcodes(bed_labwares) # @return [void] # def add_plate_to_labware_store(plate) - labware_store[plate.barcode.machine] = plate + labware_store[plate.barcode.human] = plate end # Adds the tube racks wrappers from plate includes to the labware store. @@ -143,7 +143,7 @@ def add_plate_to_labware_store(plate) # @return [void] # def add_tube_racks_to_labware_store(plate) - find_tube_racks(plate).each { |rack| labware_store[rack.barcode.machine] = rack } + find_tube_racks(plate).each { |rack| labware_store[rack.barcode.human] = rack } end # Returns the labware store. The hash is indexed by the labware barcode. @@ -163,7 +163,7 @@ def labware_store # @return [Plate] the plate # def find_plate(barcode) - Sequencescape::Api::V2::Plate.find_all({ barcode: barcode }, includes: PLATE_INCLUDES).first + Sequencescape::Api::V2::Plate.find_all({ barcode: [barcode] }, includes: PLATE_INCLUDES).first end # Returns an array of tube rack wrapper objects that from the downstream tubes @@ -194,7 +194,7 @@ def find_tube_racks(plate) # @return [TubeRackWrapper] the tube rack wrapper object # def find_or_create_tube_rack(racks, barcode, plate) - rack = racks.detect { |tube_rack| tube_rack.barcode.machine == barcode } + rack = racks.detect { |tube_rack| tube_rack.barcode.human == barcode } return rack if rack.present? labware_barcode = LabwareBarcode.new(human: barcode, machine: barcode) racks.push(TubeRackWrapper.new(labware_barcode, plate)).last From fa6e2cbcdad8f375743c856eedc703d847337f87 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 27 Oct 2023 20:16:33 +0100 Subject: [PATCH 19/52] Delegate tube-rack wrapper attributes to the last tube --- app/models/robots/tube_rack_wrapper.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index ed14a71a1..9c21f83eb 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -6,7 +6,7 @@ module Robots # class TubeRackWrapper attr_accessor :barcode, :parent, :tubes - delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :first_tube, allow_nil: true + delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :last_tube, allow_nil: true # Initializes a new instance of the class. # @@ -20,12 +20,12 @@ def initialize(barcode, parent, tubes: []) @tubes = tubes end - # Returns the first tube on the tube rack. This method used for delegating + # Returns the last tube on the tube rack. This method used for delegating # certain methods to make it behave like a like a labware object. # - # @return [Tube] the first tube - def first_tube - @tubes.first + # @return [Tube] the last tube + def last_tube + @tubes.last end end end From d985cdb8c1875e125a280242336577d908b01c84 Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 27 Oct 2023 20:17:28 +0100 Subject: [PATCH 20/52] Added test for a valid scanned layout --- .../robots/plate_to_tube_racks_robot_spec.rb | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 spec/models/robots/plate_to_tube_racks_robot_spec.rb diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb new file mode 100644 index 000000000..3c3ba767c --- /dev/null +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -0,0 +1,220 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Robots::PlateToTubeRacksRobot, robot: true do + include FeatureHelpers # Include methods for stubbing Sequencescape API requests. + include RobotHelpers # Include methods for stubbing bed labware lookups. + has_a_working_api # Add a mock Sequencescape API to the test context. + + describe '#verify' do + # The robot will receive the scanned layout as bed_labwares from request parameters + # and return a report object showing the validity of each bed and robot. + subject { robot.verify(bed_labwares: scanned_layout) } + + # user_uuid + let(:user_uuid) { 'user_uuid' } + + # tube rack barcodes + let(:tube_rack1_barcode) { 'TR00000001' } + let(:tube_rack2_barcode) { 'TR00000002' } + + # tube metadata + let(:tube1_metadata) { { 'tube_rack_barcode' => tube_rack1_barcode, 'tube_rack_position' => 'A1' } } + let(:tube2_metadata) { { 'tube_rack_barcode' => tube_rack1_barcode, 'tube_rack_position' => 'B1' } } + let(:tube3_metadata) { { 'tube_rack_barcode' => tube_rack1_barcode, 'tube_rack_position' => 'C1' } } + let(:tube4_metadata) { { 'tube_rack_barcode' => tube_rack2_barcode, 'tube_rack_position' => 'A1' } } + let(:tube5_metadata) { { 'tube_rack_barcode' => tube_rack2_barcode, 'tube_rack_position' => 'B1' } } + let(:tube6_metadata) { { 'tube_rack_barcode' => tube_rack2_barcode, 'tube_rack_position' => 'C1' } } + + # tube custom metadata collections + let(:tube1_custom_metadata) { create(:custom_metadatum_collection, metadata: tube1_metadata) } + let(:tube2_custom_metadata) { create(:custom_metadatum_collection, metadata: tube2_metadata) } + let(:tube3_custom_metadata) { create(:custom_metadatum_collection, metadata: tube3_metadata) } + let(:tube4_custom_metadata) { create(:custom_metadatum_collection, metadata: tube4_metadata) } + let(:tube5_custom_metadata) { create(:custom_metadatum_collection, metadata: tube5_metadata) } + let(:tube6_custom_metadata) { create(:custom_metadatum_collection, metadata: tube6_metadata) } + + # tube uuids + let(:tube1_uuid) { 'tube1_uuid' } + let(:tube2_uuid) { 'tube2_uuid' } + let(:tube3_uuid) { 'tube3_uuid' } + let(:tube4_uuid) { 'tube4_uuid' } + let(:tube5_uuid) { 'tube5_uuid' } + let(:tube6_uuid) { 'tube6_uuid' } + + # tube purpose uuids + let(:tube_purpose1_uuid) { 'tube_purpose1_uuid' } + let(:tube_purpose2_uuid) { 'tube_purpose2_uuid' } + + # tube purpose names + let(:tube_purpose1_name) { 'tube_purpose1_name' } + let(:tube_purpose2_name) { 'tube_purpose2_name' } + + # tube purposes + let(:tube_purpose1) { create(:v2_purpose, name: tube_purpose1_name, uuid: tube_purpose1_uuid) } + let(:tube_purpose2) { create(:v2_purpose, name: tube_purpose2_name, uuid: tube_purpose2_uuid) } + + # tube states + let(:tube1_state) { 'pending' } + let(:tube2_state) { 'pending' } + let(:tube3_state) { 'pending' } + let(:tube4_state) { 'pending' } + let(:tube5_state) { 'pending' } + let(:tube6_state) { 'pending' } + + # tubes + let(:tube1) do + create( + :v2_tube_with_metadata, + uuid: tube1_uuid, + barcode_prefix: 'FX', + barcode_number: 4, + custom_metadatum_collection: tube1_custom_metadata, + purpose: tube_purpose1, + state: tube1_state + ) + end + let(:tube2) do + create( + :v2_tube_with_metadata, + uuid: tube2_uuid, + barcode_prefix: 'FX', + barcode_number: 5, + custom_metadatum_collection: tube2_custom_metadata, + purpose: tube_purpose1, + state: tube2_state + ) + end + let(:tube3) do + create( + :v2_tube_with_metadata, + uuid: tube3_uuid, + barcode_prefix: 'FX', + barcode_number: 6, + custom_metadatum_collection: tube3_custom_metadata, + purpose: tube_purpose1, + state: tube3_state + ) + end + let(:tube4) do + create( + :v2_tube_with_metadata, + uuid: tube4_uuid, + barcode_prefix: 'FX', + barcode_number: 7, + custom_metadatum_collection: tube4_custom_metadata, + purpose: tube_purpose2, + state: tube4_state + ) + end + let(:tube5) do + create( + :v2_tube_with_metadata, + uuid: tube5_uuid, + barcode_prefix: 'FX', + barcode_number: 8, + custom_metadatum_collection: tube5_custom_metadata, + purpose: tube_purpose2, + state: tube5_state + ) + end + let(:tube6) do + create( + :v2_tube_with_metadata, + uuid: tube6_uuid, + barcode_prefix: 'FX', + barcode_number: 9, + custom_metadatum_collection: tube6_custom_metadata, + purpose: tube_purpose2, + state: tube6_state + ) + end + + # wells + let(:well1) { create(:v2_well, location: 'A1', downstream_tubes: [tube1, tube4]) } + let(:well2) { create(:v2_well, location: 'B1', downstream_tubes: [tube2, tube5]) } + let(:well3) { create(:v2_well, location: 'C1', downstream_tubes: [tube3, tube6]) } + + # plate purpose uuid + let(:plate_purpose_uuid) { 'plate_purpose_uuid' } + + # plate purpose name + let(:plate_purpose_name) { 'plate_purpose' } + + # plate purpose + let(:plate_purpose) { create(:v2_purpose, name: plate_purpose_name, uuid: plate_purpose_uuid) } + + # plate uuid + let(:plate_uuid) { 'plate_uuid' } + + # plate state + let(:plate_state) { 'passed' } + + # plate + let(:plate) do + create(:v2_plate, wells: [well1, well2, well3], barcode_number: 3, purpose: plate_purpose, state: plate_state) + end + + let(:bed1_barcode) { 'bed1_barcode' } + let(:bed2_barcode) { 'bed2_barcode' } + let(:bed3_barcode) { 'bed3_barcode' } + + let(:robot_config) do + { + name: 'robot_name', + beds: { + bed1_barcode => { + purpose: plate_purpose_name, + states: ['passed'], + label: 'Bed 1' + }, + bed2_barcode => { + purpose: tube_purpose1_name, + states: ['pending'], + label: 'Bed 2', + target_state: 'passed' + }, + bed3_barcode => { + purpose: tube_purpose2_name, + states: ['pending'], + label: 'Bed 3', + target_state: 'passed' + } + }, + relationships: [ + { + 'type' => 'relationship_type', + 'options' => { + 'parent' => bed1_barcode, + 'children' => [bed2_barcode, bed3_barcode] + } + } + ] + } + end + + let(:robot) { described_class.new(robot_config.merge(api: api, user_uuid: user_uuid)) } + + before do + # Stub the robot requests to the Sequencescape API to look up labware by + # barcode. It returns the plate with its wells and downstream tubes. + includes = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' + bed_plate_lookup_with_barcode(plate.barcode.human, [plate], includes) + bed_plate_lookup_with_barcode(tube_rack1_barcode, [], includes) + bed_plate_lookup_with_barcode(tube_rack2_barcode, [], includes) + end + + context 'with a valid scanned layout' do + let(:scanned_layout) do + { + bed1_barcode => [plate.human_barcode], + bed2_barcode => [tube_rack1_barcode], + bed3_barcode => [tube_rack2_barcode] + } + end + + it { is_expected.to be_valid } + end + end +end From 6a994a7492520e78a5c2366841be562910710c62 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sat, 28 Oct 2023 01:22:31 +0100 Subject: [PATCH 21/52] Reduced method sizes for updating labware metadata --- app/controllers/robots_controller.rb | 37 ++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/app/controllers/robots_controller.rb b/app/controllers/robots_controller.rb index f3ed8a2f3..d87016d83 100644 --- a/app/controllers/robots_controller.rb +++ b/app/controllers/robots_controller.rb @@ -23,7 +23,7 @@ def show def start # rubocop:todo Metrics/AbcSize @robot.perform_transfer(stripped_beds) - update_labware_metadata(params[:robot_barcode]) if params[:robot_barcode].present? + update_all_labware_metadata(params[:robot_barcode]) if params[:robot_barcode].present? respond_to { |format| format.html { redirect_to search_path, notice: "Robot #{@robot.name} has been started." } } rescue Robots::Bed::BedError => e # Our beds complained, nothing has happened. @@ -32,22 +32,43 @@ def start # rubocop:todo Metrics/AbcSize end end - def update_labware_metadata(robot_barcode) + # Updates all bed labware metadata with robot barcode. + # Beds with no transitions and labware are ignored. + # + # @param [String] robot_barcode + # @raise [Sequencescape::Api::ResourceNotFound] if the labware cannot be found + # + def update_all_labware_metadata(robot_barcode) @robot.beds.each_value do |bed| next unless bed.transitions? && bed.labware - labware_barcode = bed.labware.barcode.machine - if bed.respond_to?(:labware_created_with_robot) - bed.labware_created_with_robot(robot_barcode) - else - labware_created_with_robot(labware_barcode, robot_barcode) - end + update_bed_labware_metadata(bed, robot_barcode) rescue Sequencescape::Api::ResourceNotFound + labware_barcode = bed.labware.barcode.machine respond_to do |format| format.html { redirect_to robot_path(id: @robot.id), notice: "Labware #{labware_barcode} not found." } end end end + # Updates bed labware metadata with robot barcode. If the bed has its own + # method for updating, use that, otherwise use the method of this controller. + # + # @param [String] labware_barcode + # @param [String] robot_barcode + # @raise [Sequencescape::Api::ResourceNotFound] if the labware cannot be found + # + def update_bed_labware_metadata(bed, robot_barcode) + return bed.labware_created_with_robot(robot_barcode) if bed.respond_to?(:labware_created_with_robot) + labware_barcode = bed.labware.barcode.machine + labware_created_with_robot(labware_barcode, robot_barcode) + end + + # Updates labware metadata with robot barcode. + # + # @param [String] labware_barcode + # @param [String] robot_barcode + # @raise [Sequencescape::Api::ResourceNotFound] if the labware cannot be found + # def labware_created_with_robot(labware_barcode, robot_barcode) LabwareMetadata .new(api: api, user: current_user_uuid, barcode: labware_barcode) From 0930176ef96425209b4e1175825db925b7ed75c2 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sat, 28 Oct 2023 01:24:06 +0100 Subject: [PATCH 22/52] Disabled valid_relationships rubocop metrics for now --- app/models/robots/plate_to_tube_racks_robot.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 339e3e05e..466bef5f9 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -59,6 +59,8 @@ def verify(params) super end + # rubocop:todo Metrics/AbcSize + # rubocop:todo Metrics/MethodLength def valid_relationships raise StandardError, "Relationships for #{name} are empty" if @relationships.empty? @@ -78,6 +80,9 @@ def valid_relationships end end + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize + # Returns an array of labware from the robot's labware store for barcodes. # This method is called by the robot's beds when they need to find their # labware. The labware returned can be Plate objects or labware-like @@ -180,7 +185,7 @@ def find_tube_racks(plate) next if well.downstream_tubes.blank? well.downstream_tubes.each do |tube| barcode = tube.custom_metadatum_collection.metadata[:tube_rack_barcode] - find_or_create_tube_rack(racks, barcode, plate).tubes.push(tube) + find_or_create_tube_rack(racks, barcode, plate).push(tube) end end end @@ -190,7 +195,6 @@ def find_tube_racks(plate) # @param [Array] racks the tube racks found so far # @param [String] barcode the barcode of the tube rack # @param [Plate] plate the parent plate - # # @return [TubeRackWrapper] the tube rack wrapper object # def find_or_create_tube_rack(racks, barcode, plate) From 2581134b7f9276c2d05d5c8679404d2ecdf86fdf Mon Sep 17 00:00:00 2001 From: yoldas Date: Sat, 28 Oct 2023 01:25:12 +0100 Subject: [PATCH 23/52] Updated tube rack wrapper to handle duplicate tubes --- app/models/robots/tube_rack_wrapper.rb | 36 +++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index 9c21f83eb..10cca93e3 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -6,7 +6,7 @@ module Robots # class TubeRackWrapper attr_accessor :barcode, :parent, :tubes - delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :last_tube, allow_nil: true + delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :last, allow_nil: true # Initializes a new instance of the class. # @@ -17,15 +17,45 @@ class TubeRackWrapper def initialize(barcode, parent, tubes: []) @barcode = barcode @parent = parent - @tubes = tubes + @tubes = [] + @tube_positions = {} # Keep track of tube positions for push performance + tubes.each { |tube| push(tube) } # Eliminate duplicate tubes by position end # Returns the last tube on the tube rack. This method used for delegating # certain methods to make it behave like a like a labware object. # # @return [Tube] the last tube - def last_tube + def last @tubes.last end + + # Appends a tube to the tube rack. If there is an existing tube with the + # same position, the tube with the latest creation date is kept. The + # instance is returned for method chaining. + # + # @param [Tube] tube the tube to be added + # @return [TubeRackWrapper] the instance of the class + # + def push(tube) + index = @tube_positions[tube_rack_position(tube)] + if index.present? + @tubes[index] = tube if tube.created_at >= @tubes[index].created_at + else + @tubes.push(tube) + @tube_positions[tube_rack_position(tube)] = @tubes.length - 1 + end + end + + private + + # Returns the tube rack position of a tube from its metadata + # + # @param [Tube] tube the tube + # @return [String] the tube rack position + # + def tube_rack_position(tube) + tube.custom_metadatum_collection.metadata[:tube_rack_position] + end end end From ffdcdd5ffbbb9151f9d7bc969f1d87cc0d4c6f56 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 01:14:34 +0000 Subject: [PATCH 24/52] Renamed wrapper methods --- .../robots/plate_to_tube_racks_robot.rb | 29 ++++++++++--------- app/models/robots/tube_rack_wrapper.rb | 14 ++++----- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 466bef5f9..4d236a2cc 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -13,13 +13,16 @@ module Robots # the tube racks depend on the verification of the plate. # # The destination tube racks are distinguished by their barcodes. We assume - # that the tubes on the same tube rack have the same labware purpose. We also - # assume that there cannot be two tube racks with the tubes of the same - # labware purpose on the robot at the same time. For bed verification, only - # the etched barcode of the tube racks are scanned, not the individual tubes. - # The number of tube racks to be verified not only depends on the robot's - # configured relationships but also whether the plate has children with those - # purposes. + # that the tubes on the same tube rack have the same labware purpose. When + # multiple tubes of the same purpose and the same position are found, the + # latest tube is assumed to be on the tube rack and the other tubes are + # ignored. We also assume that there cannot be multiple tube racks with the + # tubes of the same labware purpose on the robot at the same time. + # + # For bed verification, only the etched barcode of the tube racks are scanned, + # not the individual tubes. The number of tube racks to be verified not only + # depends on the robot's configured relationships but also whether the plate + # has children with those purposes. # class PlateToTubeRacksRobot < Robots::Robot attr_writer :relationships # Hash from robot config into @relationships @@ -38,11 +41,11 @@ def bed_class # by the robot controller when the user clicks the start robot button. The # method first initializes the labware store with the plate and tube racks. # - # @param [Hash] bed_settings bed_labwares hash from request parameters + # @param [Hash] bed_labwares the bed_labwares hash from request parameters # @return [void] # - def perform_transfer(bed_settings) - init_labware_store(bed_settings) + def perform_transfer(bed_labwares) + init_labware_store(bed_labwares) super end @@ -105,6 +108,8 @@ def child_labware(plate) labware_store.values.select { |labware| labware.respond_to?(:parent) && labware.parent.uuid == plate.uuid } end + private + # Prepares the labware store before handling robot actions. This method is # called before the robot's bed verification and perform transfer actions. # @@ -121,8 +126,6 @@ def init_labware_store(bed_labwares) end end - private - # Returns an array of sanitised barcodes from the bed_labwares hash from # request parameters. # @@ -185,7 +188,7 @@ def find_tube_racks(plate) next if well.downstream_tubes.blank? well.downstream_tubes.each do |tube| barcode = tube.custom_metadatum_collection.metadata[:tube_rack_barcode] - find_or_create_tube_rack(racks, barcode, plate).push(tube) + find_or_create_tube_rack(racks, barcode, plate).push_tube(tube) end end end diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index 10cca93e3..bf93bc069 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -6,7 +6,7 @@ module Robots # class TubeRackWrapper attr_accessor :barcode, :parent, :tubes - delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :last, allow_nil: true + delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :last_tube, allow_nil: true # Initializes a new instance of the class. # @@ -26,18 +26,18 @@ def initialize(barcode, parent, tubes: []) # certain methods to make it behave like a like a labware object. # # @return [Tube] the last tube - def last + def last_tube @tubes.last end - # Appends a tube to the tube rack. If there is an existing tube with the - # same position, the tube with the latest creation date is kept. The - # instance is returned for method chaining. + # Appends a tube to the tube rack or replaces an existing tube. If there + # is an existing tube with the same position, the tube with the latest + # creation date is kept. # # @param [Tube] tube the tube to be added - # @return [TubeRackWrapper] the instance of the class + # @return [Void] # - def push(tube) + def push_tube(tube) index = @tube_positions[tube_rack_position(tube)] if index.present? @tubes[index] = tube if tube.created_at >= @tubes[index].created_at From ad670128860d6b5c989d91216de75af1425a9205 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 01:15:07 +0000 Subject: [PATCH 25/52] Added initial test for contingency-only bed verification --- .../robots/plate_to_tube_racks_robot_spec.rb | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb index 3c3ba767c..9865c8bda 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -197,7 +197,7 @@ let(:robot) { described_class.new(robot_config.merge(api: api, user_uuid: user_uuid)) } before do - # Stub the robot requests to the Sequencescape API to look up labware by + # Stub robot requests to the Sequencescape API to look up plate by # barcode. It returns the plate with its wells and downstream tubes. includes = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' bed_plate_lookup_with_barcode(plate.barcode.human, [plate], includes) @@ -205,16 +205,35 @@ bed_plate_lookup_with_barcode(tube_rack2_barcode, [], includes) end - context 'with a valid scanned layout' do - let(:scanned_layout) do - { - bed1_barcode => [plate.human_barcode], - bed2_barcode => [tube_rack1_barcode], - bed3_barcode => [tube_rack2_barcode] - } + context 'with two destination purposes' do + # Parent plate has two child tube-racks. We validate parent bed and two child beds. + context 'with a valid scanned layout' do + let(:scanned_layout) do + { + bed1_barcode => [plate.human_barcode], + bed2_barcode => [tube_rack1_barcode], + bed3_barcode => [tube_rack2_barcode] + } + end + + it { is_expected.to be_valid } end + end - it { is_expected.to be_valid } + context 'with one destination purpose' do + # Parent plate has only one child tube-rack. We validate parent bed and one child bed. + # wells + let(:well1) { create(:v2_well, location: 'A1', downstream_tubes: [tube4]) } + let(:well2) { create(:v2_well, location: 'B1', downstream_tubes: [tube5]) } + let(:well3) { create(:v2_well, location: 'C1', downstream_tubes: [tube6]) } + + context 'with a valid scanned layout' do + let(:scanned_layout) { { bed1_barcode => [plate.human_barcode], bed2_barcode => [tube_rack1_barcode] } } + + it 'test' do + is_expected.to be_valid + end + end end end end From c68f0bb69426a2e6b0a0cd8675820125954d7788 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 15:15:45 +0000 Subject: [PATCH 26/52] Prepare robot for contingency-only verification --- .../robots/plate_to_tube_racks_robot.rb | 74 ++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 4d236a2cc..0a185fc6d 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -38,32 +38,29 @@ def bed_class end # Performs the transfer between plate and tube racks. This method is called - # by the robot controller when the user clicks the start robot button. The - # method first initializes the labware store with the plate and tube racks. + # by the robot controller when the user clicks the start robot button. # # @param [Hash] bed_labwares the bed_labwares hash from request parameters # @return [void] # def perform_transfer(bed_labwares) - init_labware_store(bed_labwares) + prepare_robot(bed_labwares) super end # Performs the bed verification of plate and tube racks. This method is # called by the robot controller when the user clicks the validate layout - # button. The method first initializes the labware store with the plate - # and tube racks. + # button. # # @param [Hash] params request parameters # @return [Report] # def verify(params) - init_labware_store(params[:bed_labwares]) + prepare_robot(params[:bed_labwares]) super end # rubocop:todo Metrics/AbcSize - # rubocop:todo Metrics/MethodLength def valid_relationships raise StandardError, "Relationships for #{name} are empty" if @relationships.empty? @@ -83,7 +80,6 @@ def valid_relationships end end - # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/AbcSize # Returns an array of labware from the robot's labware store for barcodes. @@ -110,13 +106,22 @@ def child_labware(plate) private + # Prepares the robot before handling actions. + # + # @param [Hash] bed_labwares hash from request parameters + # @return [void] + def prepare_robot(bed_labwares) + prepare_labware_store(bed_labwares) + prepare_beds + end + # Prepares the labware store before handling robot actions. This method is # called before the robot's bed verification and perform transfer actions. # # @param [Hash] bed_labwares hash from request parameters # @return [void] # - def init_labware_store(bed_labwares) + def prepare_labware_store(bed_labwares) return if labware_store.present? stripped_barcodes(bed_labwares).each do |barcode| plate = find_plate(barcode) @@ -126,6 +131,57 @@ def init_labware_store(bed_labwares) end end + # Prepares the beds before handling robot actions. This method is called + # after preparing the labware store and before assigning bed_labwares + # request parameter to beds. It is simply modifying the config loaded + # into the robot (beds and relationships). + # + # There are two reasons we need to prepare the beds. 1) If parent labware + # cannot be found, we cannot find the child labware, hence we cannot + # validate child beds separately. The bed verification will fail because of + # the parent bed in this case. 2) If the parent labware can be found, but + # the parent does not have a child labware of one of the purposes, we should + # not validate the bed for that purpose. The bed verification will continue + # with the expected labware. + # + # This method relies on the bed_labwares specified in request parameters, + # that were already recorded by the prepare_labware_store method. We + # override the bed configuration based on availability of labware here. + # + # NB. The child labware are tube-rack wrapper objects, not actual labware. + # The information about tube-racks for are found using the metadata of the + # downstream tubes, included in the Sequencescape API response. + # + # @ return [void] + # + def prepare_beds + @relationships.each do |relationship| + relationship_children = relationship.dig('options', 'children') + labware_store_purposes = labware_store.values.map(&:purpose_name) + + bed_barcodes_to_remove = + relationship_children.select { |barcode| labware_store_purposes.exclude?(beds[barcode].purpose) } + + delete_beds(bed_barcodes_to_remove, relationship_children) + end + end + + # Deletes the beds and their relationships from the robot's configuration. + # This method is called by the prepare_beds method after finding which + # beds should not be verified. For scRNA, this means either we need to + # verify the parent bed first as it has a problem, or we have to remove + # the sequencing tube-rack from the robot's config as the parent has only + # contingency-only tube rack to be verified. + # + # @param [Array] barcodes array of barcodes to be removed + # @param [Array] relationship_children array of child barcodes + # @return [void] + # + def delete_beds(barcodes, relationship_children) + beds.delete_if { |barcode, _bed| barcodes.include?(barcode) } + relationship_children.delete_if { |barcode| barcodes.include?(barcode) } + end + # Returns an array of sanitised barcodes from the bed_labwares hash from # request parameters. # From 0ae1fbe0bc81b82e1df2e00e028cd20ef42e5a75 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 15:17:34 +0000 Subject: [PATCH 27/52] Added test for a contingency-only valid scanned layout --- spec/models/robots/plate_to_tube_racks_robot_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb index 9865c8bda..aca9424ad 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -206,7 +206,8 @@ end context 'with two destination purposes' do - # Parent plate has two child tube-racks. We validate parent bed and two child beds. + # Parent plate has two child tube-racks. We validate parent bed (bed1) + # and two child beds (bed2 and bed3). context 'with a valid scanned layout' do let(:scanned_layout) do { @@ -221,18 +222,17 @@ end context 'with one destination purpose' do - # Parent plate has only one child tube-rack. We validate parent bed and one child bed. + # Parent plate has only one child tube-rack. We validate parent bed + # (bed1) and one child bed (bed3). # wells let(:well1) { create(:v2_well, location: 'A1', downstream_tubes: [tube4]) } let(:well2) { create(:v2_well, location: 'B1', downstream_tubes: [tube5]) } let(:well3) { create(:v2_well, location: 'C1', downstream_tubes: [tube6]) } context 'with a valid scanned layout' do - let(:scanned_layout) { { bed1_barcode => [plate.human_barcode], bed2_barcode => [tube_rack1_barcode] } } + let(:scanned_layout) { { bed1_barcode => [plate.human_barcode], bed3_barcode => [tube_rack2_barcode] } } - it 'test' do - is_expected.to be_valid - end + it { is_expected.to be_valid } end end end From 4b7bd86fb21955feed91c157de8ff4ba90fe7a7f Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 15:43:29 +0000 Subject: [PATCH 28/52] Added test for a plate on an unknown bed --- .../robots/plate_to_tube_racks_robot_spec.rb | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb index aca9424ad..305e36778 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -219,6 +219,31 @@ it { is_expected.to be_valid } end + + context 'with plate on an unknown bed' do + # Plate is on a bed we are not supposed to use for any labware. + let(:bed_barcode) { 'unknown_bed_barcode' } + let(:scanned_layout) do + { + bed_barcode => [plate.human_barcode], + bed2_barcode => [tube_rack1_barcode], + bed3_barcode => [tube_rack2_barcode] + } + end + + it { is_expected.not_to be_valid } + + it 'has error messages' do + errors = [ + "#{bed_barcode} does not appear to be a valid bed barcode.", + "Bed 1: should not be empty.", + "Bed 1: should have children." + ] + errors.each do |error| + expect(subject.message).to include(error) + end + end + end end context 'with one destination purpose' do From 53f8f8f5026cc660a30ab0e42ceeddfee272f2e4 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 15:50:25 +0000 Subject: [PATCH 29/52] Added test for a child tube-rack missing --- .../robots/plate_to_tube_racks_robot_spec.rb | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb index 305e36778..58f5208f6 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -239,11 +239,29 @@ "Bed 1: should not be empty.", "Bed 1: should have children." ] - errors.each do |error| - expect(subject.message).to include(error) - end + errors.each { |error| expect(subject.message).to include(error) } end end + + context 'with a child tube-rack missing' do + # We forgot to scan one of the child tube-racks. + let(:scanned_layout) do + { + bed1_barcode => [plate.human_barcode], + bed2_barcode => [tube_rack1_barcode], + } + end + it { is_expected.not_to be_valid } + + it 'has error messages' do + errors = [ + "Bed 3: Was expected to contain labware barcode FX9G but nothing was scanned (empty)." + ] + errors.each { |error| expect(subject.message).to include(error) } + end + + end + end context 'with one destination purpose' do From 1022ba7f6ca58d487750c7ff67ca417d5b5c9f3f Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 16:14:53 +0000 Subject: [PATCH 30/52] Fixed incorrect tube-rack barcode in error message --- app/models/robots/tube_rack_wrapper.rb | 10 +++++++++- spec/models/robots/plate_to_tube_racks_robot_spec.rb | 11 ++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index bf93bc069..fc1f18c0c 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -6,7 +6,7 @@ module Robots # class TubeRackWrapper attr_accessor :barcode, :parent, :tubes - delegate :purpose_name, :purpose, :human_barcode, :state, :uuid, to: :last_tube, allow_nil: true + delegate :purpose_name, :purpose, :state, :uuid, to: :last_tube, allow_nil: true # Initializes a new instance of the class. # @@ -47,6 +47,14 @@ def push_tube(tube) end end + # Returns the human readable barcode of the tube rack. + # + # @return [String] the human readable barcode + # + def human_barcode + barcode.human + end + private # Returns the tube rack position of a tube from its metadata diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb index 58f5208f6..0439f4c27 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -233,7 +233,7 @@ it { is_expected.not_to be_valid } - it 'has error messages' do + it 'has correct messages' do errors = [ "#{bed_barcode} does not appear to be a valid bed barcode.", "Bed 1: should not be empty.", @@ -244,7 +244,7 @@ end context 'with a child tube-rack missing' do - # We forgot to scan one of the child tube-racks. + # We forgot to scan the second tube-rack. let(:scanned_layout) do { bed1_barcode => [plate.human_barcode], @@ -253,15 +253,16 @@ end it { is_expected.not_to be_valid } - it 'has error messages' do + it 'has correct messages' do errors = [ - "Bed 3: Was expected to contain labware barcode FX9G but nothing was scanned (empty)." + "Bed 3: Was expected to contain labware barcode #{tube_rack2_barcode} but nothing was scanned (empty)." ] errors.each { |error| expect(subject.message).to include(error) } end - end + + end context 'with one destination purpose' do From d3527e9c83bfdc112627ac04ca77123994d4aec2 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 16:15:33 +0000 Subject: [PATCH 31/52] Added test for all child tube-racks missing --- .../robots/plate_to_tube_racks_robot_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb index 0439f4c27..f19f61ada 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -261,7 +261,23 @@ end end + context 'with all child tube-racks missing' do + # We forgot to scan all tube-racks. + let(:scanned_layout) do + { + bed1_barcode => [plate.human_barcode] + } + end + it { is_expected.not_to be_valid } + it 'has correct messages' do + errors = [ + "Bed 2: Was expected to contain labware barcode #{tube_rack1_barcode} but nothing was scanned (empty).", + "Bed 3: Was expected to contain labware barcode #{tube_rack2_barcode} but nothing was scanned (empty)." + ] + errors.each { |error| expect(subject.message).to include(error) } + end + end end From 07d948c4fb9b809bb815379c96d94921ea47f45a Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 16:25:45 +0000 Subject: [PATCH 32/52] Added test for a tube-rack on an unknown bed --- .../robots/plate_to_tube_racks_robot_spec.rb | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb index f19f61ada..e297c4f84 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -222,10 +222,10 @@ context 'with plate on an unknown bed' do # Plate is on a bed we are not supposed to use for any labware. - let(:bed_barcode) { 'unknown_bed_barcode' } + let(:unknown_bed_barcode) { 'unknown_bed_barcode' } let(:scanned_layout) do { - bed_barcode => [plate.human_barcode], + unknown_bed_barcode => [plate.human_barcode], bed2_barcode => [tube_rack1_barcode], bed3_barcode => [tube_rack2_barcode] } @@ -235,9 +235,9 @@ it 'has correct messages' do errors = [ - "#{bed_barcode} does not appear to be a valid bed barcode.", - "Bed 1: should not be empty.", - "Bed 1: should have children." + "#{unknown_bed_barcode} does not appear to be a valid bed barcode.", + 'Bed 1: should not be empty.', + 'Bed 1: should have children.' ] errors.each { |error| expect(subject.message).to include(error) } end @@ -245,12 +245,7 @@ context 'with a child tube-rack missing' do # We forgot to scan the second tube-rack. - let(:scanned_layout) do - { - bed1_barcode => [plate.human_barcode], - bed2_barcode => [tube_rack1_barcode], - } - end + let(:scanned_layout) { { bed1_barcode => [plate.human_barcode], bed2_barcode => [tube_rack1_barcode] } } it { is_expected.not_to be_valid } it 'has correct messages' do @@ -261,24 +256,41 @@ end end - context 'with all child tube-racks missing' do + context 'with all child tube-racks missing' do # We forgot to scan all tube-racks. + let(:scanned_layout) { { bed1_barcode => [plate.human_barcode] } } + it { is_expected.not_to be_valid } + + it 'has correct messages' do + errors = [ + "Bed 2: Was expected to contain labware barcode #{tube_rack1_barcode} but nothing was scanned (empty).", + "Bed 3: Was expected to contain labware barcode #{tube_rack2_barcode} but nothing was scanned (empty)." + ] + errors.each { |error| expect(subject.message).to include(error) } + end + end + + context 'with a tube-rack on an unknown bed' do + # The second tube-rack is on a bed we are not supposed to use for any labware. + let(:unknown_bed_barcode) { 'unknown_bed_barcode' } let(:scanned_layout) do { - bed1_barcode => [plate.human_barcode] + bed1_barcode => [plate.human_barcode], + bed2_barcode => [tube_rack1_barcode], + unknown_bed_barcode => [tube_rack2_barcode] } end - it { is_expected.not_to be_valid } + + it { is_expected.not_to be_valid } it 'has correct messages' do errors = [ - "Bed 2: Was expected to contain labware barcode #{tube_rack1_barcode} but nothing was scanned (empty).", + "#{unknown_bed_barcode} does not appear to be a valid bed barcode.", "Bed 3: Was expected to contain labware barcode #{tube_rack2_barcode} but nothing was scanned (empty)." ] errors.each { |error| expect(subject.message).to include(error) } end end - end context 'with one destination purpose' do From ac3cfa4edb7d5d3a27b736b266de65d1d5cebe92 Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 23:34:07 +0000 Subject: [PATCH 33/52] Added state transition for all tubes of the tube-rack on the bed --- .../robots/bed/plate_to_tube_racks_bed.rb | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/models/robots/bed/plate_to_tube_racks_bed.rb b/app/models/robots/bed/plate_to_tube_racks_bed.rb index 2a12dd16b..6c8cd05e3 100644 --- a/app/models/robots/bed/plate_to_tube_racks_bed.rb +++ b/app/models/robots/bed/plate_to_tube_racks_bed.rb @@ -42,5 +42,25 @@ def load(barcodes) @barcodes = Array(barcodes).filter_map(&:strip).uniq @labware = @barcodes.present? ? robot.find_bed_labware(@barcodes) : [] end + + # Changes the state of the labware to the target state. It will change the + # state of all tubes of the tube-rack on this bed. + # + # @return [void] + # + def transition + return if target_state.nil? || labware.nil? # We have nothing to do + + labware.tubes.each { |tube| change_tube_state(tube) } + end + + # Changes the state of one tube to the target state. This method is called + # by the transition method. + # + # @param [Tube] tube the tube + def change_tube_state(tube) + state_changer = StateChangers.lookup_for(tube.purpose.uuid) + state_changer.new(api, tube.uuid, user_uuid).move_to!(target_state, "Robot #{robot.name} started") + end end end From 8e4168469bf33f97608af897a13aae24fae00d3b Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 23:35:41 +0000 Subject: [PATCH 34/52] Subclass SplittingRobot for validating relationships --- .../robots/plate_to_tube_racks_robot.rb | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 0a185fc6d..24d8d4ca7 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -24,7 +24,7 @@ module Robots # depends on the robot's configured relationships but also whether the plate # has children with those purposes. # - class PlateToTubeRacksRobot < Robots::Robot + class PlateToTubeRacksRobot < Robots::SplittingRobot attr_writer :relationships # Hash from robot config into @relationships # Option for including downstream tubes and metadata in Plate API response. @@ -60,28 +60,6 @@ def verify(params) super end - # rubocop:todo Metrics/AbcSize - def valid_relationships - raise StandardError, "Relationships for #{name} are empty" if @relationships.empty? - - @relationships.each_with_object({}) do |relationship, validations| - parent_bed = relationship.dig('options', 'parent') - child_beds = relationship.dig('options', 'children') - - validations[parent_bed] = beds[parent_bed].child_labware.present? - error(beds[parent_bed], 'should not be empty.') if beds[parent_bed].empty? - error(beds[parent_bed], 'should have children.') if beds[parent_bed].child_labware.empty? - - expected_children = beds[parent_bed].child_labware - expected_children.each_with_index do |expected_child, index| - child_bed = child_beds[index] - validations[child_bed] = check_labware_identity([expected_child], child_bed) - end - end - end - - # rubocop:enable Metrics/AbcSize - # Returns an array of labware from the robot's labware store for barcodes. # This method is called by the robot's beds when they need to find their # labware. The labware returned can be Plate objects or labware-like From 140ebe6f69bc81e5df55f3e255a69f55b86e430f Mon Sep 17 00:00:00 2001 From: yoldas Date: Sun, 29 Oct 2023 23:37:44 +0000 Subject: [PATCH 35/52] Added test for state changing for all tubes of tube-racks --- .../robots/plate_to_tube_racks_robot_spec.rb | 475 ++++++++++-------- 1 file changed, 278 insertions(+), 197 deletions(-) diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb index e297c4f84..25aa43bf2 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -7,216 +7,227 @@ include RobotHelpers # Include methods for stubbing bed labware lookups. has_a_working_api # Add a mock Sequencescape API to the test context. - describe '#verify' do - # The robot will receive the scanned layout as bed_labwares from request parameters - # and return a report object showing the validity of each bed and robot. - subject { robot.verify(bed_labwares: scanned_layout) } - - # user_uuid - let(:user_uuid) { 'user_uuid' } - - # tube rack barcodes - let(:tube_rack1_barcode) { 'TR00000001' } - let(:tube_rack2_barcode) { 'TR00000002' } - - # tube metadata - let(:tube1_metadata) { { 'tube_rack_barcode' => tube_rack1_barcode, 'tube_rack_position' => 'A1' } } - let(:tube2_metadata) { { 'tube_rack_barcode' => tube_rack1_barcode, 'tube_rack_position' => 'B1' } } - let(:tube3_metadata) { { 'tube_rack_barcode' => tube_rack1_barcode, 'tube_rack_position' => 'C1' } } - let(:tube4_metadata) { { 'tube_rack_barcode' => tube_rack2_barcode, 'tube_rack_position' => 'A1' } } - let(:tube5_metadata) { { 'tube_rack_barcode' => tube_rack2_barcode, 'tube_rack_position' => 'B1' } } - let(:tube6_metadata) { { 'tube_rack_barcode' => tube_rack2_barcode, 'tube_rack_position' => 'C1' } } - - # tube custom metadata collections - let(:tube1_custom_metadata) { create(:custom_metadatum_collection, metadata: tube1_metadata) } - let(:tube2_custom_metadata) { create(:custom_metadatum_collection, metadata: tube2_metadata) } - let(:tube3_custom_metadata) { create(:custom_metadatum_collection, metadata: tube3_metadata) } - let(:tube4_custom_metadata) { create(:custom_metadatum_collection, metadata: tube4_metadata) } - let(:tube5_custom_metadata) { create(:custom_metadatum_collection, metadata: tube5_metadata) } - let(:tube6_custom_metadata) { create(:custom_metadatum_collection, metadata: tube6_metadata) } - - # tube uuids - let(:tube1_uuid) { 'tube1_uuid' } - let(:tube2_uuid) { 'tube2_uuid' } - let(:tube3_uuid) { 'tube3_uuid' } - let(:tube4_uuid) { 'tube4_uuid' } - let(:tube5_uuid) { 'tube5_uuid' } - let(:tube6_uuid) { 'tube6_uuid' } - - # tube purpose uuids - let(:tube_purpose1_uuid) { 'tube_purpose1_uuid' } - let(:tube_purpose2_uuid) { 'tube_purpose2_uuid' } - - # tube purpose names - let(:tube_purpose1_name) { 'tube_purpose1_name' } - let(:tube_purpose2_name) { 'tube_purpose2_name' } - - # tube purposes - let(:tube_purpose1) { create(:v2_purpose, name: tube_purpose1_name, uuid: tube_purpose1_uuid) } - let(:tube_purpose2) { create(:v2_purpose, name: tube_purpose2_name, uuid: tube_purpose2_uuid) } - - # tube states - let(:tube1_state) { 'pending' } - let(:tube2_state) { 'pending' } - let(:tube3_state) { 'pending' } - let(:tube4_state) { 'pending' } - let(:tube5_state) { 'pending' } - let(:tube6_state) { 'pending' } - - # tubes - let(:tube1) do - create( - :v2_tube_with_metadata, - uuid: tube1_uuid, - barcode_prefix: 'FX', - barcode_number: 4, - custom_metadatum_collection: tube1_custom_metadata, - purpose: tube_purpose1, - state: tube1_state - ) - end - let(:tube2) do - create( - :v2_tube_with_metadata, - uuid: tube2_uuid, - barcode_prefix: 'FX', - barcode_number: 5, - custom_metadatum_collection: tube2_custom_metadata, - purpose: tube_purpose1, - state: tube2_state - ) - end - let(:tube3) do - create( - :v2_tube_with_metadata, - uuid: tube3_uuid, - barcode_prefix: 'FX', - barcode_number: 6, - custom_metadatum_collection: tube3_custom_metadata, - purpose: tube_purpose1, - state: tube3_state - ) - end - let(:tube4) do - create( - :v2_tube_with_metadata, - uuid: tube4_uuid, - barcode_prefix: 'FX', - barcode_number: 7, - custom_metadatum_collection: tube4_custom_metadata, - purpose: tube_purpose2, - state: tube4_state - ) - end - let(:tube5) do - create( - :v2_tube_with_metadata, - uuid: tube5_uuid, - barcode_prefix: 'FX', - barcode_number: 8, - custom_metadatum_collection: tube5_custom_metadata, - purpose: tube_purpose2, - state: tube5_state - ) - end - let(:tube6) do - create( - :v2_tube_with_metadata, - uuid: tube6_uuid, - barcode_prefix: 'FX', - barcode_number: 9, - custom_metadatum_collection: tube6_custom_metadata, - purpose: tube_purpose2, - state: tube6_state - ) - end + # user_uuid + let(:user_uuid) { 'user_uuid' } + + # tube rack barcodes + let(:tube_rack1_barcode) { 'TR00000001' } + let(:tube_rack2_barcode) { 'TR00000002' } + + # tube metadata + let(:tube1_metadata) { { 'tube_rack_barcode' => tube_rack1_barcode, 'tube_rack_position' => 'A1' } } + let(:tube2_metadata) { { 'tube_rack_barcode' => tube_rack1_barcode, 'tube_rack_position' => 'B1' } } + let(:tube3_metadata) { { 'tube_rack_barcode' => tube_rack1_barcode, 'tube_rack_position' => 'C1' } } + let(:tube4_metadata) { { 'tube_rack_barcode' => tube_rack2_barcode, 'tube_rack_position' => 'A1' } } + let(:tube5_metadata) { { 'tube_rack_barcode' => tube_rack2_barcode, 'tube_rack_position' => 'B1' } } + let(:tube6_metadata) { { 'tube_rack_barcode' => tube_rack2_barcode, 'tube_rack_position' => 'C1' } } + + # tube custom metadata collections + let(:tube1_custom_metadata) { create(:custom_metadatum_collection, metadata: tube1_metadata) } + let(:tube2_custom_metadata) { create(:custom_metadatum_collection, metadata: tube2_metadata) } + let(:tube3_custom_metadata) { create(:custom_metadatum_collection, metadata: tube3_metadata) } + let(:tube4_custom_metadata) { create(:custom_metadatum_collection, metadata: tube4_metadata) } + let(:tube5_custom_metadata) { create(:custom_metadatum_collection, metadata: tube5_metadata) } + let(:tube6_custom_metadata) { create(:custom_metadatum_collection, metadata: tube6_metadata) } + + # tube uuids + let(:tube1_uuid) { 'tube1_uuid' } + let(:tube2_uuid) { 'tube2_uuid' } + let(:tube3_uuid) { 'tube3_uuid' } + let(:tube4_uuid) { 'tube4_uuid' } + let(:tube5_uuid) { 'tube5_uuid' } + let(:tube6_uuid) { 'tube6_uuid' } + + let(:tube_uuids) { [tube1_uuid, tube2_uuid, tube3_uuid, tube4_uuid, tube5_uuid, tube6_uuid] } + + # tube purpose uuids + let(:tube_purpose1_uuid) { 'tube_purpose1_uuid' } + let(:tube_purpose2_uuid) { 'tube_purpose2_uuid' } + + # tube purpose names + let(:tube_purpose1_name) { 'tube_purpose1_name' } + let(:tube_purpose2_name) { 'tube_purpose2_name' } + + # tube purposes + let(:tube_purpose1) { create(:v2_purpose, name: tube_purpose1_name, uuid: tube_purpose1_uuid) } + let(:tube_purpose2) { create(:v2_purpose, name: tube_purpose2_name, uuid: tube_purpose2_uuid) } + + # tube states + let(:tube1_state) { 'pending' } + let(:tube2_state) { 'pending' } + let(:tube3_state) { 'pending' } + let(:tube4_state) { 'pending' } + let(:tube5_state) { 'pending' } + let(:tube6_state) { 'pending' } + + # tubes + let(:tube1) do + create( + :v2_tube_with_metadata, + uuid: tube1_uuid, + barcode_prefix: 'FX', + barcode_number: 4, + custom_metadatum_collection: tube1_custom_metadata, + purpose: tube_purpose1, + state: tube1_state + ) + end + let(:tube2) do + create( + :v2_tube_with_metadata, + uuid: tube2_uuid, + barcode_prefix: 'FX', + barcode_number: 5, + custom_metadatum_collection: tube2_custom_metadata, + purpose: tube_purpose1, + state: tube2_state + ) + end + let(:tube3) do + create( + :v2_tube_with_metadata, + uuid: tube3_uuid, + barcode_prefix: 'FX', + barcode_number: 6, + custom_metadatum_collection: tube3_custom_metadata, + purpose: tube_purpose1, + state: tube3_state + ) + end + let(:tube4) do + create( + :v2_tube_with_metadata, + uuid: tube4_uuid, + barcode_prefix: 'FX', + barcode_number: 7, + custom_metadatum_collection: tube4_custom_metadata, + purpose: tube_purpose2, + state: tube4_state + ) + end + let(:tube5) do + create( + :v2_tube_with_metadata, + uuid: tube5_uuid, + barcode_prefix: 'FX', + barcode_number: 8, + custom_metadatum_collection: tube5_custom_metadata, + purpose: tube_purpose2, + state: tube5_state + ) + end + let(:tube6) do + create( + :v2_tube_with_metadata, + uuid: tube6_uuid, + barcode_prefix: 'FX', + barcode_number: 9, + custom_metadatum_collection: tube6_custom_metadata, + purpose: tube_purpose2, + state: tube6_state + ) + end - # wells - let(:well1) { create(:v2_well, location: 'A1', downstream_tubes: [tube1, tube4]) } - let(:well2) { create(:v2_well, location: 'B1', downstream_tubes: [tube2, tube5]) } - let(:well3) { create(:v2_well, location: 'C1', downstream_tubes: [tube3, tube6]) } + # wells + let(:well1) { create(:v2_well, location: 'A1', downstream_tubes: [tube1, tube4]) } + let(:well2) { create(:v2_well, location: 'B1', downstream_tubes: [tube2, tube5]) } + let(:well3) { create(:v2_well, location: 'C1', downstream_tubes: [tube3, tube6]) } - # plate purpose uuid - let(:plate_purpose_uuid) { 'plate_purpose_uuid' } + # plate purpose uuid + let(:plate_purpose_uuid) { 'plate_purpose_uuid' } - # plate purpose name - let(:plate_purpose_name) { 'plate_purpose' } + # plate purpose name + let(:plate_purpose_name) { 'plate_purpose_name' } - # plate purpose - let(:plate_purpose) { create(:v2_purpose, name: plate_purpose_name, uuid: plate_purpose_uuid) } + # plate purpose + let(:plate_purpose) { create(:v2_purpose, name: plate_purpose_name, uuid: plate_purpose_uuid) } - # plate uuid - let(:plate_uuid) { 'plate_uuid' } + # plate uuid + let(:plate_uuid) { 'plate_uuid' } - # plate state - let(:plate_state) { 'passed' } + # plate state + let(:plate_state) { 'passed' } - # plate - let(:plate) do - create(:v2_plate, wells: [well1, well2, well3], barcode_number: 3, purpose: plate_purpose, state: plate_state) - end + # plate + let(:plate) do + create(:v2_plate, wells: [well1, well2, well3], barcode_number: 3, purpose: plate_purpose, state: plate_state) + end - let(:bed1_barcode) { 'bed1_barcode' } - let(:bed2_barcode) { 'bed2_barcode' } - let(:bed3_barcode) { 'bed3_barcode' } - - let(:robot_config) do - { - name: 'robot_name', - beds: { - bed1_barcode => { - purpose: plate_purpose_name, - states: ['passed'], - label: 'Bed 1' - }, - bed2_barcode => { - purpose: tube_purpose1_name, - states: ['pending'], - label: 'Bed 2', - target_state: 'passed' - }, - bed3_barcode => { - purpose: tube_purpose2_name, - states: ['pending'], - label: 'Bed 3', - target_state: 'passed' - } + # bed barcodes + let(:bed1_barcode) { 'bed1_barcode' } + let(:bed2_barcode) { 'bed2_barcode' } + let(:bed3_barcode) { 'bed3_barcode' } + + # bed purposes: same as plate and tube-rack purposes by default + let(:config_plate_purpose) { 'plate_purpose_name' } + let(:config_tube_purpose1) { 'tube_purpose1_name' } + let(:config_tube_purpose2) { 'tube_purpose2_name' } + + let(:robot_name) { 'robot_name' } + + let(:robot_config) do + { + :name => robot_name, + :beds => { + bed1_barcode => { + purpose: config_plate_purpose, + states: ['passed'], + label: 'Bed 1' }, - relationships: [ - { - 'type' => 'relationship_type', - 'options' => { - 'parent' => bed1_barcode, - 'children' => [bed2_barcode, bed3_barcode] - } + bed2_barcode => { + purpose: config_tube_purpose1, + states: ['pending'], + label: 'Bed 2', + target_state: 'passed' + }, + bed3_barcode => { + purpose: config_tube_purpose2, + states: ['pending'], + label: 'Bed 3', + target_state: 'passed' + } + }, + 'class' => 'Robots::PlateToTubeRacksRobot', + :relationships => [ + { + 'type' => 'relationship_type', + 'options' => { + 'parent' => bed1_barcode, + 'children' => [bed2_barcode, bed3_barcode] } - ] - } - end + } + ] + } + end - let(:robot) { described_class.new(robot_config.merge(api: api, user_uuid: user_uuid)) } + let(:robot) { described_class.new(robot_config.merge(api: api, user_uuid: user_uuid)) } - before do - # Stub robot requests to the Sequencescape API to look up plate by - # barcode. It returns the plate with its wells and downstream tubes. - includes = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' - bed_plate_lookup_with_barcode(plate.barcode.human, [plate], includes) - bed_plate_lookup_with_barcode(tube_rack1_barcode, [], includes) - bed_plate_lookup_with_barcode(tube_rack2_barcode, [], includes) - end + let(:scanned_layout) do + { + bed1_barcode => [plate.human_barcode], + bed2_barcode => [tube_rack1_barcode], + bed3_barcode => [tube_rack2_barcode] + } + end + + before do + # Stub robot requests to the Sequencescape API to look up plate by + # barcode. It returns the plate with its wells and downstream tubes. + includes = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' + bed_plate_lookup_with_barcode(plate.barcode.human, [plate], includes) + bed_plate_lookup_with_barcode(tube_rack1_barcode, [], includes) + bed_plate_lookup_with_barcode(tube_rack2_barcode, [], includes) + end + + describe '#verify' do + # The robot will receive the scanned layout as bed_labwares from request parameters + # and return a report object showing the validity of each bed and robot. + subject { robot.verify(bed_labwares: scanned_layout) } context 'with two destination purposes' do # Parent plate has two child tube-racks. We validate parent bed (bed1) # and two child beds (bed2 and bed3). context 'with a valid scanned layout' do - let(:scanned_layout) do - { - bed1_barcode => [plate.human_barcode], - bed2_barcode => [tube_rack1_barcode], - bed3_barcode => [tube_rack2_barcode] - } - end - it { is_expected.to be_valid } end @@ -233,7 +244,7 @@ it { is_expected.not_to be_valid } - it 'has correct messages' do + it 'has correct error messages' do errors = [ "#{unknown_bed_barcode} does not appear to be a valid bed barcode.", 'Bed 1: should not be empty.', @@ -243,12 +254,31 @@ end end + context 'with a plate missing' do + # We forgot to scan the plate. + let(:scanned_layout) { { bed2_barcode => [tube_rack1_barcode], bed3_barcode => [tube_rack2_barcode] } } + it { is_expected.not_to be_valid } + + it 'has correct error messages' do + # The following errors are expected because we could not find the + # plate, we do not know anything about the tube-racks, and we had to + # remove the beds from the robot. + errors = [ + 'Bed 1: should not be empty.', + 'Bed 1: should have children.', + "#{bed2_barcode} does not appear to be a valid bed barcode.", + "#{bed3_barcode} does not appear to be a valid bed barcode." + ] + errors.each { |error| expect(subject.message).to include(error) } + end + end + context 'with a child tube-rack missing' do # We forgot to scan the second tube-rack. let(:scanned_layout) { { bed1_barcode => [plate.human_barcode], bed2_barcode => [tube_rack1_barcode] } } it { is_expected.not_to be_valid } - it 'has correct messages' do + it 'has correct error messages' do errors = [ "Bed 3: Was expected to contain labware barcode #{tube_rack2_barcode} but nothing was scanned (empty)." ] @@ -261,7 +291,7 @@ let(:scanned_layout) { { bed1_barcode => [plate.human_barcode] } } it { is_expected.not_to be_valid } - it 'has correct messages' do + it 'has correct error messages' do errors = [ "Bed 2: Was expected to contain labware barcode #{tube_rack1_barcode} but nothing was scanned (empty).", "Bed 3: Was expected to contain labware barcode #{tube_rack2_barcode} but nothing was scanned (empty)." @@ -283,7 +313,7 @@ it { is_expected.not_to be_valid } - it 'has correct messages' do + it 'has correct error messages' do errors = [ "#{unknown_bed_barcode} does not appear to be a valid bed barcode.", "Bed 3: Was expected to contain labware barcode #{tube_rack2_barcode} but nothing was scanned (empty)." @@ -291,6 +321,21 @@ errors.each { |error| expect(subject.message).to include(error) } end end + + context 'with a plate that has an incorrect purpose' do + # The plate has a purpose that does not match the bed purpose. + let(:plate_purpose_name) { 'incorrect_purpose' } + + it { is_expected.not_to be_valid } + + it 'has correct error messages' do + errors = [ + "Bed 1 - Labware #{plate.barcode.human} is a #{plate_purpose_name} " \ + "not a #{config_plate_purpose} labware." + ] + errors.each { |error| expect(subject.message).to include(error) } + end + end end context 'with one destination purpose' do @@ -308,4 +353,40 @@ end end end + + describe '#perform_transfer' do + let(:state_change_requests) do + tube_uuids.map do |tube_uuid| + stub_api_post( + 'state_changes', + payload: { + state_change: { + target_state: 'passed', + reason: "Robot #{robot_name} started", + customer_accepts_responsibility: false, + target: tube_uuid, + user: user_uuid, + contents: nil + } + }, + body: json(:state_change, target_state: 'passed') + ) + end + end + + before do + # Create purpose configs in Settings for state changers. + create(:purpose_config, uuid: plate_purpose_uuid, name: plate_purpose_name) + create(:purpose_config, uuid: tube_purpose1_uuid, name: tube_purpose1_name) + create(:purpose_config, uuid: tube_purpose2_uuid, name: tube_purpose2_name) + + # Stub state change requests to the Sequencescape API; one for each tube on tube-racks. + state_change_requests + end + + it 'performs transfers for all tubes on the tube-racks' do + robot.perform_transfer(scanned_layout) + expect(state_change_requests[0]).to have_been_requested + end + end end From da6942ee915a1b05f24d85a0d2724f1e410dc788 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 00:32:21 +0000 Subject: [PATCH 36/52] Added well_order to get wells by coordinate --- app/models/robots/plate_to_tube_racks_robot.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 24d8d4ca7..9dff2640c 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -30,6 +30,14 @@ class PlateToTubeRacksRobot < Robots::SplittingRobot # Option for including downstream tubes and metadata in Plate API response. PLATE_INCLUDES = 'purpose,wells,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection' + # Returns the well order for getting wells from the plate. + # + # @return [Symbol] the well order + # + def well_order + :coordinate + end + # Returns the bed class for this robot. # # @return [Class] the bed class From 9a8f1ce77bcd65804bfb33751060b348bdc866cc Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:43:43 +0000 Subject: [PATCH 37/52] Updated param description in code comments Co-authored-by: KatyTaylor --- app/models/robots/plate_to_tube_racks_robot.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 9dff2640c..86feaff6a 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -48,7 +48,7 @@ def bed_class # Performs the transfer between plate and tube racks. This method is called # by the robot controller when the user clicks the start robot button. # - # @param [Hash] bed_labwares the bed_labwares hash from request parameters + # @param [Hash] bed_labwares the bed_labwares hash from request parameters (from user scanning labware into beds) # @return [void] # def perform_transfer(bed_labwares) From 879e774f2b25bb14707c90e6a459117505cc2d28 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 13:16:09 +0000 Subject: [PATCH 38/52] Fixed pushing the tubes supplied as an argument --- app/models/robots/tube_rack_wrapper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index fc1f18c0c..ae0245c03 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -19,7 +19,7 @@ def initialize(barcode, parent, tubes: []) @parent = parent @tubes = [] @tube_positions = {} # Keep track of tube positions for push performance - tubes.each { |tube| push(tube) } # Eliminate duplicate tubes by position + tubes.each { |tube| push_tube(tube) } # Eliminate duplicate tubes by position end # Returns the last tube on the tube rack. This method used for delegating From 6e6c8ac01508d9394e04f5f9a28ce9e17a54b093 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:36:24 +0000 Subject: [PATCH 39/52] Updated code comments of robots controller --- app/controllers/robots_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/robots_controller.rb b/app/controllers/robots_controller.rb index d87016d83..9c51bacce 100644 --- a/app/controllers/robots_controller.rb +++ b/app/controllers/robots_controller.rb @@ -32,8 +32,10 @@ def start # rubocop:todo Metrics/AbcSize end end - # Updates all bed labware metadata with robot barcode. - # Beds with no transitions and labware are ignored. + # Saves the scanned robot barcode against all the target labware involved in + # this bed verification (using the 'labware metadata' model). Beds that are + # not involved in this bed verification (no 'transitions', and no labware + # scanned into them) are ignored. # # @param [String] robot_barcode # @raise [Sequencescape::Api::ResourceNotFound] if the labware cannot be found From ae2a08a48f91aa63c1590a9406bcc2f4f206e3cd Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:39:56 +0000 Subject: [PATCH 40/52] Updated more code comments of robots controller --- app/controllers/robots_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/robots_controller.rb b/app/controllers/robots_controller.rb index 9c51bacce..f972ab792 100644 --- a/app/controllers/robots_controller.rb +++ b/app/controllers/robots_controller.rb @@ -52,8 +52,9 @@ def update_all_labware_metadata(robot_barcode) end end - # Updates bed labware metadata with robot barcode. If the bed has its own - # method for updating, use that, otherwise use the method of this controller. + # Saves the scanned robot barcode against the labware scanned into this bed + # (using the 'labware metadata' model). If the bed has its own method for + # updating, use that, otherwise use the method of this controller. # # @param [String] labware_barcode # @param [String] robot_barcode From 7b80d7c765a54501c25f791189b6ebbdb9d65e9b Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:41:01 +0000 Subject: [PATCH 41/52] Removed type from robot config relationships --- config/robots.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/robots.rb b/config/robots.rb index 8538acedb..5dafdaa35 100644 --- a/config/robots.rb +++ b/config/robots.rb @@ -3089,7 +3089,6 @@ class: 'Robots::PlateToTubeRacksRobot', relationships: [ { - 'type' => 'LRC PBMC Bank to LRC Bank Seq/Spare', 'options' => { 'parent' => bed(12).barcode, 'children' => [bed(15).barcode, bed(14).barcode] From be3e7bc6e9e3fdca141f263300f21448f4ae5270 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:48:29 +0000 Subject: [PATCH 42/52] Update app/models/robots/tube_rack_wrapper.rb Co-authored-by: KatyTaylor --- app/models/robots/tube_rack_wrapper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index ae0245c03..b10c8dbbe 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -22,7 +22,7 @@ def initialize(barcode, parent, tubes: []) tubes.each { |tube| push_tube(tube) } # Eliminate duplicate tubes by position end - # Returns the last tube on the tube rack. This method used for delegating + # Returns the last tube on the tube rack. This method is used for delegating # certain methods to make it behave like a like a labware object. # # @return [Tube] the last tube From e8363942345b081fe7df8fd42d040607acaf55cb Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:48:57 +0000 Subject: [PATCH 43/52] Update app/models/robots/plate_to_tube_racks_robot.rb Co-authored-by: KatyTaylor --- app/models/robots/plate_to_tube_racks_robot.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 86feaff6a..d24d3cf01 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -154,7 +154,7 @@ def prepare_beds # Deletes the beds and their relationships from the robot's configuration. # This method is called by the prepare_beds method after finding which - # beds should not be verified. For scRNA, this means either we need to + # beds should not be verified. For the scRNA Core pipeline, this means either we need to # verify the parent bed first as it has a problem, or we have to remove # the sequencing tube-rack from the robot's config as the parent has only # contingency-only tube rack to be verified. From cc5deb64bc03117b21dabee9526922fd62dfe788 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:58:10 +0000 Subject: [PATCH 44/52] Update app/models/robots/tube_rack_wrapper.rb Co-authored-by: KatyTaylor --- app/models/robots/tube_rack_wrapper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index b10c8dbbe..6863b592e 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Robots # This wrapper class is for tube racks that are not actual recorded labware. - # The instances acts as a labware wrapper and provides access to the tubes. + # The instance acts as a labware wrapper and provides access to the tubes. # Labware methods are delegated to the first tube on the tube rack. # class TubeRackWrapper From 1a6ba664c4498ca84fd0c41518ae3fa9293455ec Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:58:50 +0000 Subject: [PATCH 45/52] Update app/models/robots/tube_rack_wrapper.rb Co-authored-by: KatyTaylor --- app/models/robots/tube_rack_wrapper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index 6863b592e..68aacfd3e 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -23,7 +23,7 @@ def initialize(barcode, parent, tubes: []) end # Returns the last tube on the tube rack. This method is used for delegating - # certain methods to make it behave like a like a labware object. + # certain methods to make it behave like a labware object. # # @return [Tube] the last tube def last_tube From 28f7c672473856f75948750f0fa0380ebff1c420 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:59:14 +0000 Subject: [PATCH 46/52] Update app/models/robots/tube_rack_wrapper.rb Co-authored-by: KatyTaylor --- app/models/robots/tube_rack_wrapper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index 68aacfd3e..17e31fd8b 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -2,7 +2,7 @@ module Robots # This wrapper class is for tube racks that are not actual recorded labware. # The instance acts as a labware wrapper and provides access to the tubes. - # Labware methods are delegated to the first tube on the tube rack. + # Labware methods are delegated to the last tube on the tube rack. # class TubeRackWrapper attr_accessor :barcode, :parent, :tubes From 5e83c039afb5362173b8376ce3b6653615472f21 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:47:18 +0000 Subject: [PATCH 47/52] Removed typo from code comment --- app/models/robots/plate_to_tube_racks_robot.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index d24d3cf01..5a7255b5c 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -135,7 +135,7 @@ def prepare_labware_store(bed_labwares) # override the bed configuration based on availability of labware here. # # NB. The child labware are tube-rack wrapper objects, not actual labware. - # The information about tube-racks for are found using the metadata of the + # The information about tube-racks are found using the metadata of the # downstream tubes, included in the Sequencescape API response. # # @ return [void] From f7a67f270dd3059d3a360e5c79d5b833a4901cfe Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:56:54 +0000 Subject: [PATCH 48/52] Prettier --- config/robots.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/config/robots.rb b/config/robots.rb index 5dafdaa35..e8429d14e 100644 --- a/config/robots.rb +++ b/config/robots.rb @@ -3088,12 +3088,7 @@ }, class: 'Robots::PlateToTubeRacksRobot', relationships: [ - { - 'options' => { - 'parent' => bed(12).barcode, - 'children' => [bed(15).barcode, bed(14).barcode] - } - } + { 'options' => { 'parent' => bed(12).barcode, 'children' => [bed(15).barcode, bed(14).barcode] } } ] ) end From ba21795d81780d4854925cc4a0611ef38db04f6e Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Oct 2023 23:57:23 +0000 Subject: [PATCH 49/52] Renamed find_or_create_tube_rack to find_or_create_tube_rack_wrapper --- app/models/robots/plate_to_tube_racks_robot.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 5a7255b5c..7fb4b63c6 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -230,7 +230,7 @@ def find_tube_racks(plate) next if well.downstream_tubes.blank? well.downstream_tubes.each do |tube| barcode = tube.custom_metadatum_collection.metadata[:tube_rack_barcode] - find_or_create_tube_rack(racks, barcode, plate).push_tube(tube) + find_or_create_tube_rack_wrapper(racks, barcode, plate).push_tube(tube) end end end @@ -242,7 +242,7 @@ def find_tube_racks(plate) # @param [Plate] plate the parent plate # @return [TubeRackWrapper] the tube rack wrapper object # - def find_or_create_tube_rack(racks, barcode, plate) + def find_or_create_tube_rack_wrapper(racks, barcode, plate) rack = racks.detect { |tube_rack| tube_rack.barcode.human == barcode } return rack if rack.present? labware_barcode = LabwareBarcode.new(human: barcode, machine: barcode) From bc03b3b8f2fc3bf753cebbf9a3276a096a47f87b Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 31 Oct 2023 01:00:21 +0000 Subject: [PATCH 50/52] Updated comments about @tube_positions --- app/models/robots/tube_rack_wrapper.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index 17e31fd8b..61360b7e8 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -18,7 +18,10 @@ def initialize(barcode, parent, tubes: []) @barcode = barcode @parent = parent @tubes = [] - @tube_positions = {} # Keep track of tube positions for push performance + + # Keep track of tube positions, tube coordinate on rack => index in + # @tubes array, e.g. 'C1' => 0 . + @tube_positions = {} tubes.each { |tube| push_tube(tube) } # Eliminate duplicate tubes by position end From 62636bbc328527a4fd2e6d4eaa8c2444d954bae5 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Nov 2023 00:17:42 +0000 Subject: [PATCH 51/52] Updated transfer test to check the state change on all tubes --- spec/models/robots/plate_to_tube_racks_robot_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/models/robots/plate_to_tube_racks_robot_spec.rb b/spec/models/robots/plate_to_tube_racks_robot_spec.rb index 25aa43bf2..a86d3603a 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -386,7 +386,9 @@ it 'performs transfers for all tubes on the tube-racks' do robot.perform_transfer(scanned_layout) + expect(state_change_requests[0]).to have_been_requested + tube_uuids.each_with_index { |_tube_uuid, index| expect(state_change_requests[index]).to have_been_requested } end end end From aa5787cc61c5c382da22e10d1cd9cf7815e2bb2d Mon Sep 17 00:00:00 2001 From: yoldas Date: Fri, 3 Nov 2023 10:30:16 +0000 Subject: [PATCH 52/52] Fixed the order of tag data for Yard @param tags --- app/controllers/robots_controller.rb | 10 +++--- .../robots/bed/plate_to_tube_racks_bed.rb | 6 ++-- .../robots/plate_to_tube_racks_robot.rb | 32 +++++++++---------- app/models/robots/tube_rack_wrapper.rb | 10 +++--- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/app/controllers/robots_controller.rb b/app/controllers/robots_controller.rb index f972ab792..32f8e32bd 100644 --- a/app/controllers/robots_controller.rb +++ b/app/controllers/robots_controller.rb @@ -37,7 +37,7 @@ def start # rubocop:todo Metrics/AbcSize # not involved in this bed verification (no 'transitions', and no labware # scanned into them) are ignored. # - # @param [String] robot_barcode + # @param robot_barcode [String] the robot barcode scanned # @raise [Sequencescape::Api::ResourceNotFound] if the labware cannot be found # def update_all_labware_metadata(robot_barcode) @@ -56,8 +56,8 @@ def update_all_labware_metadata(robot_barcode) # (using the 'labware metadata' model). If the bed has its own method for # updating, use that, otherwise use the method of this controller. # - # @param [String] labware_barcode - # @param [String] robot_barcode + # @param labware_barcode [String] the barcode of the labware on the bed + # @param robot_barcode [String] the robot barcode scanned # @raise [Sequencescape::Api::ResourceNotFound] if the labware cannot be found # def update_bed_labware_metadata(bed, robot_barcode) @@ -68,8 +68,8 @@ def update_bed_labware_metadata(bed, robot_barcode) # Updates labware metadata with robot barcode. # - # @param [String] labware_barcode - # @param [String] robot_barcode + # @param labware_barcode [String] the barcode of the labware on the bed + # @param robot_barcode [String] the robot barcode scanned # @raise [Sequencescape::Api::ResourceNotFound] if the labware cannot be found # def labware_created_with_robot(labware_barcode, robot_barcode) diff --git a/app/models/robots/bed/plate_to_tube_racks_bed.rb b/app/models/robots/bed/plate_to_tube_racks_bed.rb index 6c8cd05e3..01617e757 100644 --- a/app/models/robots/bed/plate_to_tube_racks_bed.rb +++ b/app/models/robots/bed/plate_to_tube_racks_bed.rb @@ -11,7 +11,7 @@ class PlateToTubeRacksBed < Robots::Bed::Base # This method is called inside the robot controller's start action for # tube-rack wrappers and it sets the created_with_robot metadata field. # - # @param [String] robot_barcode the robot barcode + # @param robot_barcode [String] the robot barcode # @return [void] # def labware_created_with_robot(robot_barcode) @@ -35,7 +35,7 @@ def child_labware # Loads labware into this bed from the robot's labware store. # - # @param [Array] barcodes array containing the barcode of the labware + # @param barcodes [Array] array containing the barcode of the labware # @return [void] # def load(barcodes) @@ -57,7 +57,7 @@ def transition # Changes the state of one tube to the target state. This method is called # by the transition method. # - # @param [Tube] tube the tube + # @param tube [Tube] the tube for which the state should be changed def change_tube_state(tube) state_changer = StateChangers.lookup_for(tube.purpose.uuid) state_changer.new(api, tube.uuid, user_uuid).move_to!(target_state, "Robot #{robot.name} started") diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 7fb4b63c6..9f00bc6a8 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -48,7 +48,7 @@ def bed_class # Performs the transfer between plate and tube racks. This method is called # by the robot controller when the user clicks the start robot button. # - # @param [Hash] bed_labwares the bed_labwares hash from request parameters (from user scanning labware into beds) + # @param bed_labwares [Hash] the bed_labwares hash from request parameters (from user scanning labware into beds) # @return [void] # def perform_transfer(bed_labwares) @@ -60,7 +60,7 @@ def perform_transfer(bed_labwares) # called by the robot controller when the user clicks the validate layout # button. # - # @param [Hash] params request parameters + # @param params [Hash] request parameters # @return [Report] # def verify(params) @@ -73,7 +73,7 @@ def verify(params) # labware. The labware returned can be Plate objects or labware-like # wrapper objects for tube racks. # - # @param [Array] barcodes array of barcodes + # @param barcodes [Array] array of barcodes # @return [Array] # def find_bed_labware(barcodes) @@ -83,7 +83,7 @@ def find_bed_labware(barcodes) # Returns an array of child labware from the robot's labware store for # the given Plate. # - # @param [Plate] plate the parent plate + # @param plate [Plate] the parent plate # @return [Array] array of tube rack wrapper objects # def child_labware(plate) @@ -94,7 +94,7 @@ def child_labware(plate) # Prepares the robot before handling actions. # - # @param [Hash] bed_labwares hash from request parameters + # @param bed_labwares [Hash] the hash from request parameters # @return [void] def prepare_robot(bed_labwares) prepare_labware_store(bed_labwares) @@ -104,7 +104,7 @@ def prepare_robot(bed_labwares) # Prepares the labware store before handling robot actions. This method is # called before the robot's bed verification and perform transfer actions. # - # @param [Hash] bed_labwares hash from request parameters + # @param bed_labwares [Hash] the hash from request parameters # @return [void] # def prepare_labware_store(bed_labwares) @@ -159,8 +159,8 @@ def prepare_beds # the sequencing tube-rack from the robot's config as the parent has only # contingency-only tube rack to be verified. # - # @param [Array] barcodes array of barcodes to be removed - # @param [Array] relationship_children array of child barcodes + # @param barcodes [Array] array of barcodes to be removed + # @param relationship_children [Array] array of child barcodes # @return [void] # def delete_beds(barcodes, relationship_children) @@ -171,7 +171,7 @@ def delete_beds(barcodes, relationship_children) # Returns an array of sanitised barcodes from the bed_labwares hash from # request parameters. # - # @param [Hash] bed_labwares hash from request parameters + # @param bed_labwares [Hash] the hash from request parameters # @return [Array] array of barcodes # def stripped_barcodes(bed_labwares) @@ -180,7 +180,7 @@ def stripped_barcodes(bed_labwares) # Adds the plate to the robot's labware store. # - # @param [Plate] plate the parent plate + # @param plate [Plate] the parent plate # @return [void] # def add_plate_to_labware_store(plate) @@ -189,7 +189,7 @@ def add_plate_to_labware_store(plate) # Adds the tube racks wrappers from plate includes to the labware store. # - # @param [Plate] plate the parent plate + # @param plate [Plate] the parent plate # @return [void] # def add_tube_racks_to_labware_store(plate) @@ -209,7 +209,7 @@ def labware_store # Returns the Plate for the given barcode from the Sequencescape API. # The call includes downstream tubes and their metadata as well. # - # @param [String] barcode the barcode of the plate + # @param barcode [String] the barcode of the plate # @return [Plate] the plate # def find_plate(barcode) @@ -219,7 +219,7 @@ def find_plate(barcode) # Returns an array of tube rack wrapper objects that from the downstream tubes # of the given plate. # - # @param [Plate] plate the parent plate + # @param plate [Plate] the parent plate # @return [Array] array of tube rack wrapper objects # def find_tube_racks(plate) @@ -237,9 +237,9 @@ def find_tube_racks(plate) # Returns an existing or new tube rack wrapper object. # - # @param [Array] racks the tube racks found so far - # @param [String] barcode the barcode of the tube rack - # @param [Plate] plate the parent plate + # @param racks [Array] the tube racks found so far + # @param barcode[String] the barcode of the tube rack + # @param plate [Plate] the parent plate # @return [TubeRackWrapper] the tube rack wrapper object # def find_or_create_tube_rack_wrapper(racks, barcode, plate) diff --git a/app/models/robots/tube_rack_wrapper.rb b/app/models/robots/tube_rack_wrapper.rb index 61360b7e8..a63445220 100644 --- a/app/models/robots/tube_rack_wrapper.rb +++ b/app/models/robots/tube_rack_wrapper.rb @@ -10,9 +10,9 @@ class TubeRackWrapper # Initializes a new instance of the class. # - # @param [LabwareBarcode] barcode the barcode object of the tube rack - # @param [Plate] parent the parent plate - # @tubes [Array] the tubes on the tube rack + # @param barcode [LabwareBarcode] the barcode object of the tube rack + # @param parent [Plate] the parent plate + # @param tubes [Array] the tubes on the tube rack # def initialize(barcode, parent, tubes: []) @barcode = barcode @@ -37,7 +37,7 @@ def last_tube # is an existing tube with the same position, the tube with the latest # creation date is kept. # - # @param [Tube] tube the tube to be added + # @param tube [Tube] the tube to be added # @return [Void] # def push_tube(tube) @@ -62,7 +62,7 @@ def human_barcode # Returns the tube rack position of a tube from its metadata # - # @param [Tube] tube the tube + # @param tube [Tube] the tube # @return [String] the tube rack position # def tube_rack_position(tube)