Skip to content

Commit

Permalink
changes to remove tube rack wrapper and use tube rack directly
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewsparkes committed Jan 13, 2025
1 parent f818f26 commit 7081e89
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 94 deletions.
69 changes: 17 additions & 52 deletions app/models/robots/plate_to_tube_racks_robot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down Expand Up @@ -74,20 +65,22 @@ def verify(params)
# wrapper objects for tube racks.
#
# @param barcodes [Array<String>] array of barcodes
# @return [Array<Plate, TubeRackWrapper>]
# @return [Array<Plate, TubeRack>]
#
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<TubeRackWrapper>] array of tube rack wrapper objects
# @return [Array<TubeRack>] 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
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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<TubeRackWrapper>] 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<TubeRackWrapper>] 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
145 changes: 103 additions & 42 deletions spec/models/robots/plate_to_tube_racks_robot_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down Expand Up @@ -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]) }
Expand Down Expand Up @@ -159,29 +211,30 @@
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,
states: ['passed'],
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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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 }

Expand All @@ -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] } }

Expand Down

0 comments on commit 7081e89

Please sign in to comment.