From b0ef8bb71c7e3d8bc3839b9e77b56d0e9dfceab4 Mon Sep 17 00:00:00 2001 From: yoldas Date: Wed, 11 Feb 2026 23:36:35 +0000 Subject: [PATCH 1/5] Use Ultima tag groups and tags from database for Barcode_Plate_Num and Index_Barcode_Num indexes --- .../sample_sheet_generator.rb | 30 ++++++++++++------- .../sample_sheet_generator_spec.rb | 4 +-- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 2bc5b509d3..1722db917e 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -34,6 +34,11 @@ class Generator # rubocop:disable Metrics/ClassLength study_id ].freeze NUM_COLUMS = SAMPLES_HEADERS.size + # The names of the Ultima tag groups names are listed here for consistent + # index numbers for the Barcode_Plate_Num column, i.e. 1 or 2. The number is + # also used for determining the consistent starting index number for the + # Index_Barcode_Num column, i.e. Z0001 or Z097. + ULTIMA_TAG_GROUP_NAMES = ['Ultima P1', 'Ultima P2'].freeze # Initializes the generator with the given batch. # @param batch [UltimaSequencingBatch] the batch to generate sample sheets for @@ -187,28 +192,31 @@ def study_id_for(aliquot) aliquot.study_id.to_s end - # Returns a mapping of tags to their respective 1-based index numbers. - # This sorts the tags by their tag group ID and map ID to ensure consistent ordering. + # Returns a mapping of all Ultima tags to their respective 1-based index + # numbers. This sorts the tags by their tag group ID and map ID to ensure + # consistent ordering. The index numbers run across all Ultima tag groups, + # i.e. the index is 1 for the first tag in the first tag group and 97 for + # the first tag in the second tag group. # @return [Hash{Tag => Integer}] mapping of tags to index numbers def tag_index_map @tag_index_map ||= begin - tags = batch_tag_groups.flat_map { |tg| tg.tags.sort_by(&:map_id) } + tags = ultima_tag_groups.flat_map { |tg| tg.tags.sort_by(&:map_id) } tags.each_with_index.to_h { |tag, i| [tag, i + 1] } end end - # Returns a mapping of tag groups to 1-based index numbers. + # Returns a mapping of all Ultima tag groups to 1-based index numbers. + # This sorts the tag groups by their ID to ensure consistent ordering. # @return [Hash{TagGroup => Integer}] mapping of tag groups to index numbers def tag_group_index_map - @tag_group_index_map ||= batch_tag_groups.each_with_index.to_h { |tg, i| [tg, i + 1] } + @tag_group_index_map ||= ultima_tag_groups.each_with_index.to_h { |tg, i| [tg, i + 1] } end - # Returns all unique tag groups used in the batch. - # This sorts the tag groups by their ID to ensure consistent ordering. - # @return [Array] the tag groups of the batch's requests - def batch_tag_groups - @batch_tag_groups ||= batch_requests - .flat_map { |request| request.asset.aliquots.map { |aliquot| aliquot.tag.tag_group } }.uniq.sort_by(&:id) + # Returns all unique tag groups used for Ultima sequencing from database. + # The tag groups are sorted by ID to ensure consistent ordering. + # @return [Array] the tag groups used for Ultima sequencing + def ultima_tag_groups + @ultima_tag_groups ||= TagGroup.where(name: ULTIMA_TAG_GROUP_NAMES).order(:id) end # Returns the requests associated with the batch. diff --git a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb index a1da05bb06..9cd903902f 100644 --- a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb +++ b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb @@ -50,8 +50,8 @@ before { create(:ultima_global) } # Eagerly create tag groups and tags to get consistent IDs. - let!(:tag_group1) { create(:tag_group_with_tags, tag_count: 96) } - let!(:tag_group2) { create(:tag_group_with_tags, tag_count: 96) } + let!(:tag_group1) { create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P1') } + let!(:tag_group2) { create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P2') } let(:tag_groups) { [tag_group1, tag_group2] } let(:request_type) { create(:ultima_sequencing) } From 9511631fda3fe6ea203089ff4bea025a690bbdd3 Mon Sep 17 00:00:00 2001 From: yoldas Date: Wed, 11 Feb 2026 23:43:38 +0000 Subject: [PATCH 2/5] Fix typo in comment --- .../ultima_sample_sheet/sample_sheet_generator.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 1722db917e..62ebef1be9 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -34,9 +34,9 @@ class Generator # rubocop:disable Metrics/ClassLength study_id ].freeze NUM_COLUMS = SAMPLES_HEADERS.size - # The names of the Ultima tag groups names are listed here for consistent - # index numbers for the Barcode_Plate_Num column, i.e. 1 or 2. The number is - # also used for determining the consistent starting index number for the + # The names of the Ultima tag groups are listed here for consistent index + # numbers for the Barcode_Plate_Num column, i.e. 1 or 2. The number is also + # used for determining the consistent starting index number for the # Index_Barcode_Num column, i.e. Z0001 or Z097. ULTIMA_TAG_GROUP_NAMES = ['Ultima P1', 'Ultima P2'].freeze From dba36c2a3aef337430de9e95264c640eb60d84b3 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 12 Feb 2026 10:58:35 +0000 Subject: [PATCH 3/5] Set the order of Ultima tag groups explicitly --- .../sample_sheet_generator.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 62ebef1be9..d4938e6f03 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -34,11 +34,14 @@ class Generator # rubocop:disable Metrics/ClassLength study_id ].freeze NUM_COLUMS = SAMPLES_HEADERS.size - # The names of the Ultima tag groups are listed here for consistent index - # numbers for the Barcode_Plate_Num column, i.e. 1 or 2. The number is also - # used for determining the consistent starting index number for the + # The names of the Ultima tag groups are mapped to the index numbers for + # the Barcode_Plate_Num column, i.e. 1 or 2. The number is also used for + # determining the consistent starting index number for the # Index_Barcode_Num column, i.e. Z0001 or Z097. - ULTIMA_TAG_GROUP_NAMES = ['Ultima P1', 'Ultima P2'].freeze + ULTIMA_TAG_GROUPS = { + 'Ultima P1' => 1, + 'Ultima P2' => 2 + }.freeze # Initializes the generator with the given batch. # @param batch [UltimaSequencingBatch] the batch to generate sample sheets for @@ -206,17 +209,16 @@ def tag_index_map end # Returns a mapping of all Ultima tag groups to 1-based index numbers. - # This sorts the tag groups by their ID to ensure consistent ordering. + # This indexes the tag groups as given in the ULTIMA_TAG_GROUPS hash. # @return [Hash{TagGroup => Integer}] mapping of tag groups to index numbers def tag_group_index_map - @tag_group_index_map ||= ultima_tag_groups.each_with_index.to_h { |tg, i| [tg, i + 1] } + @tag_group_index_map ||= ultima_tag_groups.index_with { |tg| ULTIMA_TAG_GROUPS[tg.name] } end # Returns all unique tag groups used for Ultima sequencing from database. - # The tag groups are sorted by ID to ensure consistent ordering. # @return [Array] the tag groups used for Ultima sequencing def ultima_tag_groups - @ultima_tag_groups ||= TagGroup.where(name: ULTIMA_TAG_GROUP_NAMES).order(:id) + @ultima_tag_groups ||= TagGroup.where(name: ULTIMA_TAG_GROUPS.keys) end # Returns the requests associated with the batch. From 64a2ded77eb649335e9ce5eba42d0dae97337bc0 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 12 Feb 2026 11:19:43 +0000 Subject: [PATCH 4/5] Add test for matching z-index, oligo, and plate number in files --- .../sample_sheet_generator_spec.rb | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb index 9cd903902f..923a12a490 100644 --- a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb +++ b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb @@ -45,13 +45,28 @@ # 6,Sample6,Z0099,TCAG,2,C1,native,6 # # rubocop:enable Layout/LineLength +# rubocop:disable RSpec/MultipleExpectations, RSpec/ExampleLength RSpec.describe UltimaSampleSheet::SampleSheetGenerator do # Eagerly create the global section record. before { create(:ultima_global) } + # First oligo sequences for the two tag groups. + let(:plate1_first_oligo) { 'CAGCTCGAATGCGAT' } + let(:plate2_first_oligo) { 'CAGTCAGTTGCAGAT' } + # Eagerly create tag groups and tags to get consistent IDs. - let!(:tag_group1) { create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P1') } - let!(:tag_group2) { create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P2') } + let!(:tag_group1) do + create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P1').tap do |tg| + # To test Z0001 matching with the oligo sequence. + tg.tags.first.update!(oligo: plate1_first_oligo) + end + end + let!(:tag_group2) do + create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P2').tap do |tg| + # To test Z0097 matching with the oligo sequence. + tg.tags.first.update!(oligo: plate2_first_oligo) + end + end let(:tag_groups) { [tag_group1, tag_group2] } let(:request_type) { create(:ultima_sequencing) } @@ -210,7 +225,7 @@ def map_description(map_id) expect(csv2[1].compact_blank).to eq(["Batch #{batch.id} #{tube2.human_barcode}"]) # Second CSV end - it 'generates global sections' do # rubocop:disable RSpec/MultipleExpectations + it 'generates global sections' do # Test: Add the following hardcoded values, Application(WGS native gDNA), # sequencing_recipe(UG_116cycles_Baseline_1.8.5.2) and analysis_recipe(wgs1) expect(csv1[3].compact_blank).to eq(generator.class::GLOBAL_TITLE) @@ -219,11 +234,24 @@ def map_description(map_id) expect(csv1[6].compact_blank).to eq([]) end - it 'generates samples sections' do # rubocop:disable RSpec/MultipleExpectations + it 'generates samples sections' do expect(csv1[7].compact_blank).to eq(generator.class::SAMPLES_TITLE) expect(csv1[8].compact_blank).to eq(generator.class::SAMPLES_HEADERS) expect(csv1[9..]).to eq(csv1_samples) # First CSV expect(csv2[9..]).to eq(csv2_samples) # Second CSV end + + it 'matches the z-indexes, oligo sequences, and plate numbers' do + # First CSV + expect(csv1[9][2]).to eq('Z0001') # Index_Barcode_Num + expect(csv1[9][3]).to eq(plate1_first_oligo) # Index_Barcode_Sequence + expect(csv1[9][4]).to eq('1') # Barcode_Plate_Num + + # Second CSV + expect(csv2[9][2]).to eq('Z0097') # Index_Barcode_Num + expect(csv2[9][3]).to eq(plate2_first_oligo) # Index_Barcode_Sequence + expect(csv2[9][4]).to eq('2') # Barcode_Plate_Num + end end end +# rubocop:enable RSpec/MultipleExpectations, RSpec/ExampleLength From b88bcc3c68777efd5e7a99ed9887ee5933e10983 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 12 Feb 2026 23:52:42 +0000 Subject: [PATCH 5/5] Sort iteration of aliquots to avoid row shuffling --- app/controllers/ultima_sample_sheet/sample_sheet_generator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index d4938e6f03..212b3738c4 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -123,7 +123,7 @@ def add_global_section(csv, _request) def add_samples_section(csv, request) csv << pad(SAMPLES_TITLE) csv << pad(SAMPLES_HEADERS) - request.asset.aliquots.each do |aliquot| + request.asset.aliquots.sort_by(&:id).each do |aliquot| csv << [ sample_id_for(aliquot), library_name_for(aliquot),