diff --git a/app/models/robots/plate_to_tube_racks_robot.rb b/app/models/robots/plate_to_tube_racks_robot.rb index 9f00bc6a8..0cf455d07 100644 --- a/app/models/robots/plate_to_tube_racks_robot.rb +++ b/app/models/robots/plate_to_tube_racks_robot.rb @@ -27,16 +27,7 @@ module Robots class PlateToTubeRacksRobot < Robots::SplittingRobot 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 well order for getting wells from the plate. - # - # @return [Symbol] the well order - # - def well_order - :coordinate - end + PLATE_INCLUDES = 'purpose' # Returns the bed class for this robot. # @@ -74,20 +65,22 @@ def verify(params) # wrapper objects for tube racks. # # @param barcodes [Array] array of barcodes - # @return [Array] + # @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 + # Returns an array of child tube racks from the robot's labware store for # the given Plate. # # @param plate [Plate] the parent plate - # @return [Array] array of tube rack wrapper objects + # @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 } + labware_store.values.select do |labware| + labware.respond_to?(:parents) && labware.parents.first&.uuid == plate.uuid + end end private @@ -103,6 +96,8 @@ 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. + # NB. This is what labwares should be scanned, given the plate barcode scanned. + # i.e. the plate barcode is scanned, and the expected tube rack children are determined. # # @param bed_labwares [Hash] the hash from request parameters # @return [void] @@ -134,10 +129,6 @@ def prepare_labware_store(bed_labwares) # 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 are found using the metadata of the - # downstream tubes, included in the Sequencescape API response. - # # @ return [void] # def prepare_beds @@ -193,7 +184,14 @@ 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.human] = rack } + plate.children.each do |tube_rack| + # cycle beds, if tube rack matches purpose and state from config, add it + beds.each_value do |bed| + if bed.purpose == tube_rack.purpose.name && bed.states.include?(tube_rack.state) + labware_store[tube_rack.barcode.human] = tube_rack + end + end + end end # Returns the labware store. The hash is indexed by the labware barcode. @@ -215,38 +213,5 @@ def labware_store 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.blank? - well.downstream_tubes.each do |tube| - barcode = tube.custom_metadatum_collection.metadata[:tube_rack_barcode] - find_or_create_tube_rack_wrapper(racks, barcode, plate).push_tube(tube) - end - end - end - - # Returns an existing or new tube rack wrapper object. - # - # @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) - 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 - end end end 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 c264a5240..35e3719de 100644 --- a/spec/models/robots/plate_to_tube_racks_robot_spec.rb +++ b/spec/models/robots/plate_to_tube_racks_robot_spec.rb @@ -11,24 +11,8 @@ 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_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube1_metadata) } - let(:tube2_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube2_metadata) } - let(:tube3_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube3_metadata) } - let(:tube4_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube4_metadata) } - let(:tube5_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube5_metadata) } - let(:tube6_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube6_metadata) } + let(:tube_rack1_barcode) { 'TR1F' } + let(:tube_rack2_barcode) { 'TR2G' } # tube uuids let(:tube1_uuid) { 'tube1_uuid' } @@ -128,6 +112,74 @@ ) end + # tube rack purpose uuids + let(:tube_rack1_purpose_uuid) { 'tube_rack1_purpose_uuid' } + let(:tube_rack2_purpose_uuid) { 'tube_rack2_purpose_uuid' } + + # tube rack purpose names + let(:tube_rack1_purpose_name) { 'tube_rack1_purpose_name' } + let(:tube_rack2_purpose_name) { 'tube_rack2_purpose_name' } + + # tube rack purposes + let(:tube_rack1_purpose) do + create(:v2_tube_rack_purpose, name: tube_rack1_purpose_name, uuid: tube_rack1_purpose_uuid) + end + let(:tube_rack2_purpose) do + create(:v2_tube_rack_purpose, name: tube_rack2_purpose_name, uuid: tube_rack2_purpose_uuid) + end + + # tube rack uuids + let(:tube_rack1_uuid) { 'tube_rack1_uuid' } + let(:tube_rack2_uuid) { 'tube_rack2_uuid' } + + # tube racks + let!(:tube_rack1) do + create( + :tube_rack, + purpose: tube_rack1_purpose, + barcode_number: 1, + barcode_prefix: 'TR', + uuid: tube_rack1_uuid, + tubes: { + A1: tube1, + B1: tube2, + C1: tube3 + }, + parents: [plate] + ) + end + let!(:tube_rack2) do + create( + :tube_rack, + purpose: tube_rack2_purpose, + barcode_number: 2, + barcode_prefix: 'TR', + uuid: tube_rack2_uuid, + tubes: { + A1: tube4, + B1: tube5, + C1: tube6 + }, + parents: [plate] + ) + end + + # 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_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube1_metadata) } + let(:tube2_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube2_metadata) } + let(:tube3_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube3_metadata) } + let(:tube4_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube4_metadata) } + let(:tube5_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube5_metadata) } + let(:tube6_custom_metadatum_collection) { create(:custom_metadatum_collection, metadata: tube6_metadata) } + # wells let(:well1) { create(:v2_well, location: 'A1', downstream_tubes: [tube1, tube4]) } let(:well2) { create(:v2_well, location: 'B1', downstream_tubes: [tube2, tube5]) } @@ -159,15 +211,16 @@ 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(:config_plate_purpose) { plate_purpose_name } + let(:config_tube_rack1_purpose) { tube_rack1_purpose_name } + let(:config_tube_rack2_purpose) { tube_rack2_purpose_name } let(:robot_name) { 'robot_name' } let(:robot_config) do { :name => robot_name, + :verify_robot => false, :beds => { bed1_barcode => { purpose: config_plate_purpose, @@ -175,13 +228,13 @@ label: 'Bed 1' }, bed2_barcode => { - purpose: config_tube_purpose1, + purpose: config_tube_rack1_purpose, states: ['pending'], label: 'Bed 2', target_state: 'passed' }, bed3_barcode => { - purpose: config_tube_purpose2, + purpose: config_tube_rack2_purpose, states: ['pending'], label: 'Bed 3', target_state: 'passed' @@ -211,12 +264,21 @@ 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) + # Stub robot request to the Sequencescape API to look up the parent plate + plate_includes = described_class::PLATE_INCLUDES + bed_plate_lookup_with_barcode(plate.barcode.human, [plate], plate_includes) + + # Stub robot requests to the Sequencescape API to look up tube-racks as if plates, should return empty + bed_plate_lookup_with_barcode(tube_rack1_barcode, [], plate_includes) + bed_plate_lookup_with_barcode(tube_rack2_barcode, [], plate_includes) + + # Stub robot requests to the Sequencescape API to look up tube-racks + tube_rack_includes = 'racked_tubes,racked_tubes.tubes,racked_tubes.tubes.custom_metadatum_collection' + bed_tube_rack_lookup_with_barcode(tube_rack1_barcode, [tube_rack1], tube_rack_includes) + bed_tube_rack_lookup_with_barcode(tube_rack2_barcode, [tube_rack2], tube_rack_includes) + + # Set up children of plate + allow(plate).to receive(:children).and_return([tube_rack1, tube_rack2]) end describe '#verify' do @@ -260,16 +322,9 @@ 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) } + # The following errors are expected because we could not find the plate. + expected_errors = ['Bed 1: should not be empty.', 'Bed 1: should have children.'] + expected_errors.each { |error| expect(subject.message).to include(error) } end end @@ -291,12 +346,13 @@ let(:scanned_layout) { { bed1_barcode => [plate.human_barcode] } } it { is_expected.not_to be_valid } + # code knows by comparing to the labware store which specific tube racks are missing it 'has correct error messages' do - errors = [ + expected_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) } + expected_errors.each { |error| expect(subject.message).to include(error) } end end @@ -323,8 +379,9 @@ end context 'with a plate that has an incorrect purpose' do - # The plate has a purpose that does not match the bed purpose. + # The plate has a purpose that does not match the expected bed purpose. let(:plate_purpose_name) { 'incorrect_purpose' } + let(:config_plate_purpose) { 'plate_purpose_name' } it { is_expected.not_to be_valid } @@ -346,6 +403,10 @@ let(:well2) { create(:v2_well, location: 'B1', downstream_tubes: [tube5]) } let(:well3) { create(:v2_well, location: 'C1', downstream_tubes: [tube6]) } + let(:tube_rack1) { nil } + + before { allow(plate).to receive(:children).and_return([tube_rack2]) } + context 'with a valid scanned layout' do let(:scanned_layout) { { bed1_barcode => [plate.human_barcode], bed3_barcode => [tube_rack2_barcode] } }