From 70722e37c3ab717a9b01a38cf9d0f9f7e2bda6d0 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 26 Mar 2024 15:23:12 +0000 Subject: [PATCH 1/9] added ruby lsp directory to prettier ignore --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 4bfe8e9f31..b5372448cc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -52,3 +52,4 @@ config/cucumber.yml config/database.yml *.min.js public/vite-* +.ruby-lsp/vendor From 6169a7090d15eed28791a12fb8d030a010a35fbf Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 26 Mar 2024 15:24:06 +0000 Subject: [PATCH 2/9] missing hyphens top of file added --- config/default_records/submission_templates/004_novaseq.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/default_records/submission_templates/004_novaseq.yml b/config/default_records/submission_templates/004_novaseq.yml index 522827e682..bbd6a549ea 100644 --- a/config/default_records/submission_templates/004_novaseq.yml +++ b/config/default_records/submission_templates/004_novaseq.yml @@ -1,3 +1,4 @@ +--- Limber-Htp - WGS - NovaSeq 6000 Paired end sequencing: name: "Limber-Htp - WGS - NovaSeq 6000 Paired end sequencing" submission_class_name: "LinearSubmission" From afa18bbedd42a3abb30a2f92c06c6a9d30d78b35 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 26 Mar 2024 15:26:58 +0000 Subject: [PATCH 3/9] added request type and submission template for faculty input point in scRNA core pipeline --- ...11_scrna_core_cdna_prep_plate_purposes.wip.yml | 10 +++------- ...ber_scrna_core_cdna_prep_request_types.wip.yml | 15 +++++++++++++++ ...na_core_cdna_prep_submission_templates.wip.yml | 6 ++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/config/default_records/plate_purposes/011_scrna_core_cdna_prep_plate_purposes.wip.yml b/config/default_records/plate_purposes/011_scrna_core_cdna_prep_plate_purposes.wip.yml index faec61f135..a57a9a9283 100644 --- a/config/default_records/plate_purposes/011_scrna_core_cdna_prep_plate_purposes.wip.yml +++ b/config/default_records/plate_purposes/011_scrna_core_cdna_prep_plate_purposes.wip.yml @@ -2,14 +2,10 @@ # Most are defined in the Limber config, but the below also need to be defined in Sequencescape. # They are in a separate file so it can be 'feature flagged off' until needed. --- -# The 'LRC PBMC Pools' purpose is controlled by Limber. However, it has been -# added here to create submission and request type records for scRNA Core cDNA -# Prep stage. -LRC PBMC Pools: - stock_plate: false - cherrypickable_target: false # The 'LRC PBMC Pools Input' purpose is included here so that it's available for -# sample manifests. +# sample manifests, and also because it is an acceptable purpose for the scRNA Core cDNA Prep Input +# submission template. LRC PBMC Pools Input: + input_plate: true stock_plate: true cherrypickable_target: false diff --git a/config/default_records/request_types/014_limber_scrna_core_cdna_prep_request_types.wip.yml b/config/default_records/request_types/014_limber_scrna_core_cdna_prep_request_types.wip.yml index e20f41dff3..14bab79557 100644 --- a/config/default_records/request_types/014_limber_scrna_core_cdna_prep_request_types.wip.yml +++ b/config/default_records/request_types/014_limber_scrna_core_cdna_prep_request_types.wip.yml @@ -20,3 +20,18 @@ limber_scrna_core_cdna_prep_v2: # - Chromium single cell 3 prime HT v3 # - Chromium single cell BCR HT # - Chromium single cell TCR HT +limber_scrna_core_cdna_prep_input: + name: scRNA Core cDNA Prep Input + asset_type: Well + order: 1 + request_class_name: IlluminaHtp::Requests::StdLibraryRequest + for_multiplexing: false + billable: true + product_line_name: Short Read + acceptable_purposes: + - LRC PBMC Pools Input + library_types: + - Chromium single cell 5 prime HT v2 + # - Chromium single cell 3 prime HT v3 + # - Chromium single cell BCR HT + # - Chromium single cell TCR HT diff --git a/config/default_records/submission_templates/011_scrna_core_cdna_prep_submission_templates.wip.yml b/config/default_records/submission_templates/011_scrna_core_cdna_prep_submission_templates.wip.yml index f127beb98a..f7661354d7 100644 --- a/config/default_records/submission_templates/011_scrna_core_cdna_prep_submission_templates.wip.yml +++ b/config/default_records/submission_templates/011_scrna_core_cdna_prep_submission_templates.wip.yml @@ -16,3 +16,9 @@ Limber-Htp - scRNA Core cDNA Prep: request_type_keys: ["limber_scrna_core_cdna_prep_v2"] product_line_name: Short Read product_catalogue_name: scRNA Core +Limber-Htp - scRNA Core cDNA Prep Input: + submission_class_name: "LinearSubmission" + related_records: + request_type_keys: ["limber_scrna_core_cdna_prep_input"] + product_line_name: Short Read + product_catalogue_name: scRNA Core From e42b47671714b6cbacce63d0cc90a0576cd1e476 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Thu, 28 Mar 2024 10:50:13 +0000 Subject: [PATCH 4/9] added functionality to test submission and generate plates uat actions to allow multiple samples per well --- .../uat_actions/generate_plates.rb | 89 ++++++++++++++++--- .../uat_actions/test_submission.rb | 64 ++++++++++--- spec/uat_actions/generate_plates_spec.rb | 14 ++- spec/uat_actions/test_submission_spec.rb | 10 +++ 4 files changed, 149 insertions(+), 28 deletions(-) diff --git a/app/uat_actions/uat_actions/generate_plates.rb b/app/uat_actions/uat_actions/generate_plates.rb index 57f1b8f745..54bab9d288 100644 --- a/app/uat_actions/uat_actions/generate_plates.rb +++ b/app/uat_actions/uat_actions/generate_plates.rb @@ -22,10 +22,18 @@ class UatActions::GeneratePlates < UatActions form_field :well_count, :number_field, label: 'Well Count', - help: 'The number of occupied wells on each plate', + help: 'The number of occupied wells on each plate (locations will be randomised if less than full)', options: { minimum: 1 } + form_field :number_of_samples_in_each_well, + :number_field, + label: 'Number of samples in each well', + help: 'The number of samples to create in each well. Default is 1. Max 10.', + options: { + minimum: 1, + maximum: 10 + } form_field :study_name, :select, label: 'Study', @@ -38,11 +46,13 @@ class UatActions::GeneratePlates < UatActions select_options: %w[Column Row Random] validate :well_count_smaller_than_plate_size + validates :number_of_samples_in_each_well, numericality: { greater_than: 0, only_integer: true, allow_blank: false } def self.default new( plate_count: 1, well_count: 96, + number_of_samples_in_each_well: 1, study_name: UatActions::StaticRecords.study.name, plate_purpose_name: PlatePurpose.stock_plate_purpose.name, well_layout: 'Column' @@ -69,21 +79,72 @@ def well_count_smaller_than_plate_size false end + # Ensures number of samples per occupied well is at least 1 + def num_samples_per_well + @num_samples_per_well ||= + if number_of_samples_in_each_well.present? && number_of_samples_in_each_well.to_i.positive? + number_of_samples_in_each_well.to_i + else + 1 + end + end + + # Constructs wells for the given plate. + # For each well in the plate, it creates the specified number of samples using the `create_sample` method. + # @param plate [Plate] the plate for which to construct wells def construct_wells(plate) wells(plate).each do |well| - sample_name = "sample_#{plate.human_barcode}_#{well.map.description}" - sample = - Sample.new( - name: sample_name, - sanger_sample_id: sample_name, - studies: [study], - sample_metadata_attributes: { - supplier_name: sample_name, - cohort: "Cohort#{plate.human_barcode}", - sample_description: "Description#{plate.human_barcode}" - } - ) - sample.save!(validate: false) + num_samples_per_well.times { |sample_index| create_sample(plate, well, sample_index + 1) } + end + end + + # Creates a new sample with a unique name based on the plate, well, and sample index. + # The sample is built using the `build_sample` method and saved using the `save_sample` method. + # If the sample fails to save due to an ActiveRecord::RecordInvalid error, the error message is + # added to the base errors. + # @param plate [Plate] the plate associated with the sample + # @param well [Well] the well associated with the sample + # @param sample_index [Integer] the index of the sample + # @raise [ActiveRecord::RecordInvalid] if the sample fails to save + def create_sample(plate, well, sample_index) + sample_name = "sample_#{sample_index}_#{plate.human_barcode}_#{well.map.description}" + sample = build_sample(sample_name, plate) + save_sample(sample, well, sample_index) + rescue ActiveRecord::RecordInvalid => e + errors.add(:base, "Failed to create sample: #{e.message}") + end + + # Builds a new Sample object with the given name and associated plate. + # The sample's metadata attributes are also set, including the supplier name, cohort, and sample description. + # @param sample_name [String] the name of the sample, also used as the sanger_sample_id and supplier_name + # @param plate [Plate] the plate associated with the sample, its human_barcode is used in the cohort and + # sample_description + # @return [Sample] the newly built Sample object + def build_sample(sample_name, plate) + Sample.new( + name: sample_name, + sanger_sample_id: sample_name, + studies: [study], + sample_metadata_attributes: { + supplier_name: sample_name, + cohort: "Cohort#{plate.human_barcode}", + sample_description: "SD-#{plate.human_barcode}" + } + ) + end + + # Saves the given sample and creates an aliquot in the specified well. + # If there are multiple samples in each well, the aliquot is created with a tag depth. + # @param sample [Sample] the sample to be saved + # @param well [Well] the well where the aliquot will be created + # @param sample_index [Integer] the index of the sample in the well, used as tag depth + # if there are multiple samples per well + def save_sample(sample, well, sample_index) + sample.save!(validate: false) + + if num_samples_per_well > 1 + well.aliquots.create!(sample: sample, study: study, tag_depth: sample_index) + else well.aliquots.create!(sample: sample, study: study) end end diff --git a/app/uat_actions/uat_actions/test_submission.rb b/app/uat_actions/uat_actions/test_submission.rb index 502377f826..2a4e23f248 100644 --- a/app/uat_actions/uat_actions/test_submission.rb +++ b/app/uat_actions/uat_actions/test_submission.rb @@ -67,6 +67,18 @@ class UatActions::TestSubmission < UatActions # rubocop:todo Metrics/ClassLength options: { minimum: 1 } + form_field :number_of_samples_in_each_well, + :number_field, + label: 'Number of samples per occupied well', + help: + 'Use this option to create wells containing a pool of multiple samples. Enter ' \ + 'the number of samples per well. All occupied wells will have this number of samples.' \ + 'Useful for a pipeline where pools of starting samples is required.' \ + 'Leave blank for 1 sample per well. Max 10 samples per well.', + options: { + minimum: 1, + maximum: 10 + } form_field :number_of_wells_to_submit, :number_field, label: 'Number of wells to submit', @@ -80,6 +92,7 @@ class UatActions::TestSubmission < UatActions # rubocop:todo Metrics/ClassLength validates :submission_template, presence: { message: 'could not be found' } validates :number_of_wells_with_samples, numericality: { greater_than: 0, only_integer: true, allow_blank: true } + validates :number_of_samples_in_each_well, numericality: { greater_than: 0, only_integer: true, allow_blank: true } validates :number_of_wells_to_submit, numericality: { greater_than: 0, only_integer: true, allow_blank: true } # @@ -87,7 +100,7 @@ class UatActions::TestSubmission < UatActions # rubocop:todo Metrics/ClassLength # # @return [UatActions::TestSubmission] A default object for rendering a form def self.default - new + new(number_of_samples_in_each_well: 1) end def self.compatible_submission_templates @@ -121,6 +134,7 @@ def perform # rubocop:todo Metrics/AbcSize report['primer_panel'] = order.request_options[:primer_panel_name] if order.request_options[:primer_panel_name] .present? report['number_of_wells_with_samples'] = labware.wells.with_aliquots.size + report['number_of_samples_in_each_well'] = labware.wells.with_aliquots.first.aliquots.size report['number_of_wells_to_submit'] = assets.size order.submission.built! true @@ -166,24 +180,48 @@ def labware @labware ||= plate_barcode.blank? ? generate_plate : Plate.find_by_barcode(plate_barcode.strip) end - def generate_plate # rubocop:todo Metrics/MethodLength - generator = UatActions::GeneratePlates.default - generator.plate_purpose_name = plate_purpose_name.presence || default_purpose_name - - num_sample_wells = number_of_wells_with_samples.to_i - generator.well_count = - if num_sample_wells.zero? - # default option, create a full plate - 96 + # Ensures number of samples per occupied well is at least 1 + def num_samples_per_well + @num_samples_per_well ||= + if number_of_samples_in_each_well.present? && number_of_samples_in_each_well.to_i.positive? + number_of_samples_in_each_well.to_i else - # take the number entered in the form - num_sample_wells + 1 end - generator.well_layout = 'Random' + end + + # Generates a new plate using a plate generator. + # The generator is set up with the appropriate parameters, then used to perform the plate generation. + # After the plate is generated, the barcode is retrieved. + # @return [Plate] the newly generated Plate object + def generate_plate + generator = setup_generator generator.perform Plate.find_by_barcode(generator.report['plate_0']) end + # Sets up a plate generator with the appropriate parameters. + # The generator is created with default settings, then its attributes are set based on the current object's state. + # The plate purpose name is set to the plate_purpose_name entered by the user, or to the default purpose name if + # plate_purpose_name is not present. + # The well count is determined by the `determine_well_count` method. + # The well layout is set to 'Random'. + # The number of samples in each well is set to num_samples_per_well. + # @return [UatActions::GeneratePlates] the configured plate generator + def setup_generator + generator = UatActions::GeneratePlates.default + generator.plate_purpose_name = plate_purpose_name.presence || default_purpose_name + generator.well_count = determine_well_count + generator.well_layout = 'Random' + generator.number_of_samples_in_each_well = num_samples_per_well + generator + end + + def determine_well_count + num_sample_wells = number_of_wells_with_samples.to_i + num_sample_wells.zero? ? 96 : num_sample_wells + end + def order_request_options default_request_options.merge(custom_request_options) end diff --git a/spec/uat_actions/generate_plates_spec.rb b/spec/uat_actions/generate_plates_spec.rb index eefe4b2adc..40aec601e3 100644 --- a/spec/uat_actions/generate_plates_spec.rb +++ b/spec/uat_actions/generate_plates_spec.rb @@ -11,13 +11,15 @@ let(:plate_barcode_3) { build(:plate_barcode) } context 'when creating a single plate' do + let(:num_samples_per_well) { 1 } let(:parameters) do { plate_purpose_name: PlatePurpose.stock_plate_purpose.name, plate_count: 1, well_count: 1, study_name: study.name, - well_layout: 'Column' + well_layout: 'Column', + number_of_samples_in_each_well: num_samples_per_well } end let(:report) do @@ -32,6 +34,16 @@ expect(uat_action.perform).to be true expect(uat_action.report['plate_0']).to eq report['plate_0'] expect(Plate.find_by_barcode(report['plate_0']).wells.first.aliquots.first.study).to eq study + expect(Plate.find_by_barcode(report['plate_0']).wells.first.aliquots.size).to eq 1 + end + + context 'with multiple samples per well' do + let(:num_samples_per_well) { 4 } + + it 'can be performed' do + expect(uat_action.perform).to be true + expect(Plate.find_by_barcode(report['plate_0']).wells.first.aliquots.size).to eq 4 + end end end diff --git a/spec/uat_actions/test_submission_spec.rb b/spec/uat_actions/test_submission_spec.rb index 6a5dd1de1b..43311460fe 100644 --- a/spec/uat_actions/test_submission_spec.rb +++ b/spec/uat_actions/test_submission_spec.rb @@ -87,6 +87,16 @@ expect(uat_action.report['number_of_wells_to_submit']).to be_a Integer end end + + context 'with optional number of samples per well supplied' do + let(:parameters) { { submission_template_name: submission_template.name, number_of_samples_in_each_well: '2' } } + + it 'can be performed' do + expect(uat_action.perform).to be true + expect(uat_action.report['plate_barcode_0']).to eq report['plate_barcode_0'] + expect(uat_action.report['number_of_samples_in_each_well']).to be_a Integer + end + end end it 'returns a default' do From c0dd628c8958ca00f4e2a2aad73c2a77b6e3865d Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Thu, 28 Mar 2024 12:03:33 +0000 Subject: [PATCH 5/9] linted --- spec/uat_actions/generate_plates_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/uat_actions/generate_plates_spec.rb b/spec/uat_actions/generate_plates_spec.rb index 40aec601e3..d9ac522268 100644 --- a/spec/uat_actions/generate_plates_spec.rb +++ b/spec/uat_actions/generate_plates_spec.rb @@ -36,7 +36,7 @@ expect(Plate.find_by_barcode(report['plate_0']).wells.first.aliquots.first.study).to eq study expect(Plate.find_by_barcode(report['plate_0']).wells.first.aliquots.size).to eq 1 end - + context 'with multiple samples per well' do let(:num_samples_per_well) { 4 } From 9c10936c7bbf369e1d5838ee7f688357988ea800 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 2 Apr 2024 12:00:24 +0100 Subject: [PATCH 6/9] fix for comparison of counts, ids need to be uniq --- app/models/plate_purpose/input.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/plate_purpose/input.rb b/app/models/plate_purpose/input.rb index 80e93e4547..cf048f9119 100644 --- a/app/models/plate_purpose/input.rb +++ b/app/models/plate_purpose/input.rb @@ -20,7 +20,7 @@ class PlatePurpose::Input < PlatePurpose def state_of(plate) # If there are no wells with aliquots we're pending - ids_of_wells_with_aliquots = plate.wells.with_aliquots.ids + ids_of_wells_with_aliquots = plate.wells.with_aliquots.ids.uniq return UNREADY_STATE if ids_of_wells_with_aliquots.empty? # All of the wells with aliquots must have customer requests for us to consider the plate passed From 4dbaedd6c06c8cc644b9bd334b7483f2b810afe8 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 2 Apr 2024 12:00:50 +0100 Subject: [PATCH 7/9] fix comment wording --- app/uat_actions/uat_actions/test_submission.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/uat_actions/uat_actions/test_submission.rb b/app/uat_actions/uat_actions/test_submission.rb index 2a4e23f248..0cd347e6f4 100644 --- a/app/uat_actions/uat_actions/test_submission.rb +++ b/app/uat_actions/uat_actions/test_submission.rb @@ -152,7 +152,7 @@ def assets @assets ||= select_assets end - # take a sample of the wells to go into the submission + # take a selection of the wells to go into the submission # rubocop:todo Metrics/MethodLength def select_assets # rubocop:todo Metrics/AbcSize num_subm_wells = number_of_wells_to_submit.to_i From a61c83b3dba2208a0163376c733b43d409267215 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 2 Apr 2024 15:22:07 +0100 Subject: [PATCH 8/9] fix for creating a submission with multiple samples per well but need to only create one request per well --- .../uat_actions/test_submission.rb | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/uat_actions/uat_actions/test_submission.rb b/app/uat_actions/uat_actions/test_submission.rb index 0cd347e6f4..c0bc6ff804 100644 --- a/app/uat_actions/uat_actions/test_submission.rb +++ b/app/uat_actions/uat_actions/test_submission.rb @@ -153,23 +153,22 @@ def assets end # take a selection of the wells to go into the submission - # rubocop:todo Metrics/MethodLength def select_assets # rubocop:todo Metrics/AbcSize - num_subm_wells = number_of_wells_to_submit.to_i - if num_subm_wells.zero? + num_wells_to_submit_from_plate = number_of_wells_to_submit.to_i + + # fetch wells with aliquots, and remove duplicates in cases where multiple aliquots per well + wells_with_aliquots = labware.wells.with_aliquots.uniq + + if num_wells_to_submit_from_plate.zero? # default option, take all wells with aliquots - labware.wells.with_aliquots + wells_with_aliquots else - # take the number entered in the form - reqd_num_samples = num_subm_wells - # check the number is less than the total wells with aliquots # N.B. sort the array after random sampling to get back into original well order - num_wells_with_aliquots = labware.wells.with_aliquots.size - if reqd_num_samples > num_wells_with_aliquots - labware.wells.with_aliquots + if num_wells_to_submit_from_plate > wells_with_aliquots.size + wells_with_aliquots else - labware.wells.with_aliquots.sample(reqd_num_samples).sort_by(&:map_id) + wells_with_aliquots.sample(num_wells_to_submit_from_plate).sort_by(&:map_id) end end end From 9c24c79a2a59af58ffe975d9121a5ceb4de0035f Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 2 Apr 2024 17:58:11 +0100 Subject: [PATCH 9/9] linted --- app/uat_actions/uat_actions/test_submission.rb | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/app/uat_actions/uat_actions/test_submission.rb b/app/uat_actions/uat_actions/test_submission.rb index c0bc6ff804..db8d070940 100644 --- a/app/uat_actions/uat_actions/test_submission.rb +++ b/app/uat_actions/uat_actions/test_submission.rb @@ -153,28 +153,21 @@ def assets end # take a selection of the wells to go into the submission - def select_assets # rubocop:todo Metrics/AbcSize + def select_assets num_wells_to_submit_from_plate = number_of_wells_to_submit.to_i # fetch wells with aliquots, and remove duplicates in cases where multiple aliquots per well wells_with_aliquots = labware.wells.with_aliquots.uniq - if num_wells_to_submit_from_plate.zero? - # default option, take all wells with aliquots + # return all wells if a subset is not required or the requested number is greater than the total wells with aliquots + if num_wells_to_submit_from_plate.zero? || num_wells_to_submit_from_plate > wells_with_aliquots.size wells_with_aliquots else - # check the number is less than the total wells with aliquots # N.B. sort the array after random sampling to get back into original well order - if num_wells_to_submit_from_plate > wells_with_aliquots.size - wells_with_aliquots - else - wells_with_aliquots.sample(num_wells_to_submit_from_plate).sort_by(&:map_id) - end + wells_with_aliquots.sample(num_wells_to_submit_from_plate).sort_by(&:map_id) end end - # rubocop:enable Metrics/MethodLength - def labware @labware ||= plate_barcode.blank? ? generate_plate : Plate.find_by_barcode(plate_barcode.strip) end