From 6806092b26aa206a8aca9fcba47cdd83ff339651 Mon Sep 17 00:00:00 2001 From: sabrine33 Date: Tue, 12 May 2026 09:28:36 +0100 Subject: [PATCH 1/4] update the logic behind construct under_rep comments to include all submission types. also update the logic behind comparing aliquots --- .../under_rep_well_comments_to_broadcast.rb | 154 ++++++++++-------- ...der_rep_well_comments_to_broadcast_spec.rb | 140 ++++++++-------- 2 files changed, 156 insertions(+), 138 deletions(-) diff --git a/app/models/under_rep_well_comments_to_broadcast.rb b/app/models/under_rep_well_comments_to_broadcast.rb index 1ae1ade4a6..579c148d70 100644 --- a/app/models/under_rep_well_comments_to_broadcast.rb +++ b/app/models/under_rep_well_comments_to_broadcast.rb @@ -1,23 +1,32 @@ # frozen_string_literal: true -# This module encapsulates logic for identifying and generating comment data -# related to "under-represented" wells within a released batch. -# Works for pipelines where all requests (library prep, multiplexing, sequencing) share a single submission (e.g., WGS). -# Other pipelines may differ, as they can involve multiple submissions at different stages. -# Left as-is for this WGS proof of concept; future extensions may require refactoring. +# This module encapsulates the logic for identifying and generating comment +# data related to "under-represented" wells within a released batch. +# under_represented poly_metadata entries are linked to library requests associated with wells containing +# under-represented samples. # -# It: -# - Extracts requests containing "under_represented" metadata. -# - Builds structured `UnderRepWellComment` objects that represent -# per-well comments to be serialized or broadcasted. -# - Provides a `comments` method used by CommentIO for serialization. +# It supports: +# - Pipelines where all requests (library prep, multiplexing, sequencing) +# belong to a single submission (e.g. WGS) +# - Pipelines where sequencing and library prep requests belong to +# different submissions (e.g. RNA) # +# Responsibilities: +# - Retrieve ancestor plates from batch lanes +# - Extract requests containing `under_represented` metadata from the +# ancestor plates +# - Build structured `UnderRepWellComment` objects representing +# per-well comments for serialization or broadcasting +# - Handle `under_represented` poly_metadata linked to library requests +# and attached to wells containing under-represented samples +# - Provide a `comments` method used by `CommentIO` for serialization + module UnderRepWellCommentsToBroadcast UNDER_REPRESENTED_KEY = 'under_represented' # Represents a single "under-represented" well comment, holding metadata # about its batch, position, tag index, and associated poly_metadatum. - # + # @attr_reader [Object] poly_metadatum The metadata object describing the comment. # @attr_reader [Integer] batch_id The ID of the batch this comment belongs to. # @attr_reader [Integer] position The lane or well position of the comment. @@ -39,51 +48,56 @@ def initialize(poly_metadatum:, batch_id:, position:, tag_index:) delegate :destroyed?, to: :poly_metadatum end - # Returns all requests related to the batch that include a poly_metadatum entry - # with the `UNDER_REPRESENTED` key. + # Entry point used by CommentIO to retrieve the comments to serialize. # - # @return [Array] an array of requests that contain under-represented metadata. - def request_with_under_represented_wells - submissions.flat_map(&:requests).select do |r| - r.poly_metadata.any? { |pol| pol.key == UNDER_REPRESENTED_KEY } - end + # @return [Array] comments representing under-represented wells. + def comments + under_represented_well_comments || [] end ## - # Builds all `UnderRepWellComment` objects for requests within the batch - # that have under-represented wells. The resulting list aggregates comments - # across multiple requests and associated assets. - # + # Builds all `UnderRepWellComment` objects for the batch by iterating over + # each sequencing request and its associated lane. + # Aggregates comments across all lanes and their ancestor plates. # @return [Array] a flat list of comment objects. - # def under_represented_well_comments - request_with_under_represented_wells.flat_map do |request| - build_comments_for_request(request) + requests.flat_map do |batch_request| + lane = batch_request.target_asset + build_comments_for_lane(lane, batch_request) end.compact end - # Entry point used by CommentIO to retrieve the comments to serialize. - # - # @return [Array] comments representing under-represented wells. - def comments - under_represented_well_comments || [] - end - private - ## - # Constructs `UnderRepWellComment` objects for a single request, - # filtering only poly_metadata that correspond to under-represented wells. + # Coordinates comment building for a single lane by finding all under-represented + # library requests in the lane's ancestor plates, then building a comment for + # each associated poly_metadatum. # - # @param request [Request] the request to process - # @return [Array] comments built for the given request - # - def build_comments_for_request(request) - under_represented_poly_metadata(request).flat_map do |poly_meta| - build_comments_for_poly_meta(request, poly_meta) + # @param lane [Lane] the lane asset associated with the batch request + # @param batch_request [Request] the sequencing request whose position is used in the comment + # @return [Array] comments built for the given lane + def build_comments_for_lane(lane, batch_request) + under_rep_requests_for_lane(lane).flat_map do |library_request| + under_represented_poly_metadata(library_request).flat_map do |poly_meta| + build_comments(library_request, lane, batch_request, poly_meta) + end end end + # Traverses the ancestor plates of a lane to find all library requests + # containing an `under_represented` poly_metadata entry. Covers both + # single-submission pipelines (e.g. WGS) and multi-submission pipelines + # (e.g. RNA) where sequencing and library prep belong to different submissions. + # + # @param lane [Lane] the lane whose ancestor plates are traversed + # @return [Array] requests containing under-represented metadata + def under_rep_requests_for_lane(lane) + lane.ancestors.grep(Plate) + .flat_map(&:wells) + .flat_map(&:requests) + .select { |r| r.poly_metadata.any? { |pol| pol.key == UNDER_REPRESENTED_KEY } } + end + ## # Returns only those `PolyMetadatum` records whose key matches # the `UNDER_REPRESENTED_KEY` constant. @@ -96,45 +110,49 @@ def under_represented_poly_metadata(request) end ## - # Builds comments for a specific poly_metadatum by iterating through - # all target aliquots and matching them to lane aliquots by tag map ID. - # - # The starting point is the request that holds the poly_metadatum for the - # under-represented well. This request is typically a LibraryRequest, whose - # target_asset corresponds to the under-represented well. - # - # From there, we traverse to the Lane asset via `request.asset.descendants`. - # The Lane provides access to the `batch_request` (through `lane.source_request`), - # which contains the lane position information used in the generated comment. - # This linkage is not fully guaranteed for more complex pipelines where - # descendant relationships may differ. + # Constructs `UnderRepWellComment` objects for a single request, + # filtering only poly_metadata that correspond to under-represented wells. + # Iterates over the well's aliquots and matches each against the lane aliquots to retrieve the correct + # tag index and position. # - # @param request [Request] the request that owns the poly_metadatum - # @param poly_meta [PolyMetadatum] the metadata record for under-represented wells - # @return [Array] list of constructed comment objects - - def build_comments_for_poly_meta(request, poly_meta) - request.target_asset.aliquots.filter_map do |aliquot| - lane = request.asset.descendants.last - next unless aliquot_matches_lane?(lane, aliquot) + # @param library_request [Request] the library request whose target_asset well holds the aliquots + # @param lane [Lane] the lane asset used to find matching aliquots and retrieve the tag index + # @param batch_request [Request] the sequencing request providing the lane position + # @param poly_meta [PolyMetadatum] the under-represented metadata record to associate with the comment + # @return [Array] comments built for each matching aliquot + def build_comments(library_request, lane, batch_request, poly_meta) + library_request.target_asset.aliquots.filter_map do |aliquot| + matching = find_matching_lane_aliquot(lane, aliquot) + next unless matching UnderRepWellComment.new( - position: lane.source_request.position, + position: batch_request.position, batch_id: id, - tag_index: aliquot.tag.map_id, + tag_index: matching.aliquot_index_value, poly_metadatum: poly_meta ) end end ## - # Checks if the given aliquot’s tag map ID matches any aliquot in the lane. + # Determines whether the given aliquot matches an aliquot on the lane + # A match requires equality on: + # - `sample_id` + # - `tag_id` + # - `tag2_id` + # - `tag_depth` + # + # This comparison is used to associate request aliquots with the + # corresponding lane aliquots in order to retrieve the correct tag index for the comment. # # @param lane [Asset] the lane asset derived from the request # @param aliquot [Aliquot] the aliquot being compared - # @return [Boolean] true if the aliquot matches a lane aliquot by index value - # - def aliquot_matches_lane?(lane, aliquot) - lane.aliquots.any? { |a| a.aliquot_index_value == aliquot.tag.map_id } + # @return [Aliquot, nil] the matching lane aliquot, or nil if no match is found + def find_matching_lane_aliquot(lane, aliquot) + lane.aliquots.find do |a| + a.sample_id == aliquot.sample_id && + a.tag_id == aliquot.tag_id && + a.tag2_id == aliquot.tag2_id && a.tag_depth == aliquot.tag_depth + end end end diff --git a/spec/models/under_rep_well_comments_to_broadcast_spec.rb b/spec/models/under_rep_well_comments_to_broadcast_spec.rb index d166e3c0f7..273de2f2bd 100644 --- a/spec/models/under_rep_well_comments_to_broadcast_spec.rb +++ b/spec/models/under_rep_well_comments_to_broadcast_spec.rb @@ -3,64 +3,40 @@ require 'rails_helper' RSpec.describe UnderRepWellCommentsToBroadcast do - subject(:batch) { dummy_batch_class.new(id: 123, submissions: [submission]) } - - let(:dummy_batch_class) do - Class.new do - include UnderRepWellCommentsToBroadcast - - attr_accessor :id, :submissions - - def initialize(id:, submissions: []) - @id = id - @submissions = submissions - end - end - end + let(:pcr_xp_plate) { create(:plate_with_tagged_wells, sample_count: 5) } + let(:well_a1) { pcr_xp_plate.wells.located_at('A1').first } let(:poly_meta_underrep) { create(:poly_metadatum, key: 'under_represented', value: 'true') } let(:poly_meta_other) { create(:poly_metadatum, key: 'other_key', value: 'some_value') } - let(:tag) { create(:tag, map_id: 1) } - let(:aliquot) { create(:aliquot, tag:) } - - let(:lane_aliquot) { create(:aliquot) } - let(:lane_source_request) { create(:request) } - let(:batch_request) { create(:batch_request, request: lane_source_request, position: 1) } - let(:lane) { create(:lane, aliquots: [lane_aliquot], source_request: lane_source_request) } - let(:well_target_asset) { create(:well, aliquots: [aliquot]) } - let(:well_asset) { create(:well) } - - let(:request) do - create( - :library_request, - poly_metadata: [poly_meta_underrep, poly_meta_other], - target_asset: well_target_asset, - asset: well_asset - ) + let(:poly_metadata_library_request) do + create(:library_request, + poly_metadata: [poly_meta_underrep, poly_meta_other], + target_asset: well_a1) end - let(:submission) { create(:submission, requests: [request]) } - - before do - lane_source_request.update!(batch_request:) - AliquotIndexer.index(lane) - allow(well_asset).to receive(:descendants).and_return([lane]) + let(:tube) do + create(:multiplexed_library_tube, aliquots: pcr_xp_plate.aliquots.map(&:dup)) + .tap { |t| t.labware.parents << pcr_xp_plate } end - describe '#request_with_under_represented_wells' do - it 'returns the request containing under_represented poly_metadata' do - expect(batch.request_with_under_represented_wells).to contain_exactly(request) + let(:lane) do + create(:lane, aliquots: tube.aliquots.map(&:dup)).tap do |l| + l.labware.parents << tube + l.index_aliquots end + end - it 'returns empty if no under_represented metadata exists' do - allow(request).to receive(:poly_metadata).and_return([poly_meta_other]) - expect(batch.request_with_under_represented_wells).to be_empty - end + let(:batch_request) { create(:sequencing_request, target_asset: lane, asset: tube.receptacle) } + let(:batch) { create(:batch).tap { |b| b.requests << batch_request } } + + before do + well_a1.requests << poly_metadata_library_request end + describe '#under_represented_well_comments' do - context 'when aliquot matches lane' do + context 'when a lane aliquot matches the well aliquot' do let(:comments) { batch.under_represented_well_comments } let(:comment) { comments.first } @@ -68,78 +44,102 @@ def initialize(id:, submissions: []) expect(comments.size).to eq(1) end - it 'returns an UnderRepWellComment object' do + it 'returns UnderRepWellComment objects' do expect(comment).to be_a(UnderRepWellCommentsToBroadcast::UnderRepWellComment) end it 'sets the correct batch_id' do - expect(comment.batch_id).to eq(123) + expect(comment.batch_id).to eq(batch.id) end - it 'sets the correct tag_index' do - expect(comment.tag_index).to eq(tag.map_id) + it 'sets the correct position from the batch request' do + expect(comment.position).to eq(batch_request.position) end - it 'sets the correct position' do - expect(comment.position).to eq(lane.source_request.position) + it 'sets the correct tag_index from the matching lane aliquot' do + expected_tag_index = lane.aliquots + .find { |a| a.sample_id == well_a1.aliquots.first.sample_id } + &.aliquot_index_value + expect(comment.tag_index).to eq(expected_tag_index) end it 'associates the correct poly_metadatum' do expect(comment.poly_metadatum).to eq(poly_meta_underrep) end + + it 'does not include comments for non-under_represented metadata' do + keys = comments.map { |c| c.poly_metadatum.key } + expect(keys).to all(eq('under_represented')) + end end - context 'when no aliquot matches the lane' do - before { AliquotIndex.where(lane: lane, aliquot: lane_aliquot).delete_all } + context 'when no lane aliquot matches the well aliquot' do + before do + lane # force eager creation before deleting indexes + AliquotIndex.where(lane_id: lane.id).delete_all + # Reload so lane.aliquots no longer carries aliquot_index_value + lane.aliquots.each_with_index do |aliquot, i| + aliquot.update!(tag_id: 9000+i, tag2_id: 8000+i) + end + end it 'returns an empty array' do expect(batch.under_represented_well_comments).to eq([]) end end - end - describe '#comments' do - it 'returns an array of comments' do - expect(batch.comments).to be_an(Array) + context 'when no requests have under_represented metadata' do + let(:clean_batch_request) { create(:sequencing_request, target_asset: create(:lane)) } + let(:clean_batch) { create(:batch).tap { |b| b.requests << clean_batch_request } } + + it 'returns an empty array' do + expect(clean_batch.under_represented_well_comments).to eq([]) + end end + end + # ─── UnderRepWellComment value object ─────────────────────────────────────── describe UnderRepWellCommentsToBroadcast::UnderRepWellComment do subject(:comment) do described_class.new( poly_metadatum: poly_meta_underrep, - batch_id: 10, - position: 2, - tag_index: 5 + batch_id: batch.id, + position: batch_request.position, + tag_index: 1 ) end - it 'exposes the batch_id' do - expect(comment.batch_id).to eq(10) + it 'exposes batch_id' do + expect(comment.batch_id).to eq(batch.id) end - it 'exposes the position' do - expect(comment.position).to eq(2) + it 'exposes position' do + expect(comment.position).to eq(batch_request.position) end - it 'exposes the tag_index' do - expect(comment.tag_index).to eq(5) + it 'exposes tag_index' do + expect(comment.tag_index).to eq(1) end - it 'exposes the poly_metadatum' do + it 'exposes poly_metadatum' do expect(comment.poly_metadatum).to eq(poly_meta_underrep) end it 'delegates key to poly_metadatum' do - expect(comment.key).to eq('under_represented') + expect(comment.key).to eq(poly_meta_underrep.key) end it 'delegates value to poly_metadatum' do - expect(comment.value).to eq('true') + expect(comment.value).to eq(poly_meta_underrep.value) end it 'delegates updated_at to poly_metadatum' do expect(comment.updated_at).to eq(poly_meta_underrep.updated_at) end + + it 'delegates destroyed? to poly_metadatum' do + expect(comment.destroyed?).to eq(poly_meta_underrep.destroyed?) + end end -end +end \ No newline at end of file From 3b79d90444d31b0c8129f15773ca4157a751fd09 Mon Sep 17 00:00:00 2001 From: sabrine33 Date: Tue, 12 May 2026 09:31:32 +0100 Subject: [PATCH 2/4] linting --- ...nder_rep_well_comments_to_broadcast_spec.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/spec/models/under_rep_well_comments_to_broadcast_spec.rb b/spec/models/under_rep_well_comments_to_broadcast_spec.rb index 273de2f2bd..84dddde7ef 100644 --- a/spec/models/under_rep_well_comments_to_broadcast_spec.rb +++ b/spec/models/under_rep_well_comments_to_broadcast_spec.rb @@ -34,7 +34,6 @@ well_a1.requests << poly_metadata_library_request end - describe '#under_represented_well_comments' do context 'when a lane aliquot matches the well aliquot' do let(:comments) { batch.under_represented_well_comments } @@ -58,8 +57,8 @@ it 'sets the correct tag_index from the matching lane aliquot' do expected_tag_index = lane.aliquots - .find { |a| a.sample_id == well_a1.aliquots.first.sample_id } - &.aliquot_index_value + .find { |a| a.sample_id == well_a1.aliquots.first.sample_id } + &.aliquot_index_value expect(comment.tag_index).to eq(expected_tag_index) end @@ -75,11 +74,11 @@ context 'when no lane aliquot matches the well aliquot' do before do - lane # force eager creation before deleting indexes + lane # force eager creation before deleting indexes AliquotIndex.where(lane_id: lane.id).delete_all # Reload so lane.aliquots no longer carries aliquot_index_value lane.aliquots.each_with_index do |aliquot, i| - aliquot.update!(tag_id: 9000+i, tag2_id: 8000+i) + aliquot.update!(tag_id: 9000 + i, tag2_id: 8000 + i) end end @@ -96,7 +95,6 @@ expect(clean_batch.under_represented_well_comments).to eq([]) end end - end # ─── UnderRepWellComment value object ─────────────────────────────────────── @@ -104,9 +102,9 @@ subject(:comment) do described_class.new( poly_metadatum: poly_meta_underrep, - batch_id: batch.id, - position: batch_request.position, - tag_index: 1 + batch_id: batch.id, + position: batch_request.position, + tag_index: 1 ) end @@ -142,4 +140,4 @@ expect(comment.destroyed?).to eq(poly_meta_underrep.destroyed?) end end -end \ No newline at end of file +end From 6430ae290fb23edb59764e18057449383f874de5 Mon Sep 17 00:00:00 2001 From: sabrine33 <139128026+sabrine33@users.noreply.github.com> Date: Wed, 13 May 2026 14:11:20 +0100 Subject: [PATCH 3/4] Update app/models/under_rep_well_comments_to_broadcast.rb Co-authored-by: KatyTaylor --- app/models/under_rep_well_comments_to_broadcast.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/under_rep_well_comments_to_broadcast.rb b/app/models/under_rep_well_comments_to_broadcast.rb index 579c148d70..c47018f122 100644 --- a/app/models/under_rep_well_comments_to_broadcast.rb +++ b/app/models/under_rep_well_comments_to_broadcast.rb @@ -142,8 +142,8 @@ def build_comments(library_request, lane, batch_request, poly_meta) # - `tag2_id` # - `tag_depth` # - # This comparison is used to associate request aliquots with the - # corresponding lane aliquots in order to retrieve the correct tag index for the comment. + # This comparison is used to associate aliquots that were marked as 'under-represented' with their + # downstream lane aliquots in order to retrieve the correct tag index for the comment. # # @param lane [Asset] the lane asset derived from the request # @param aliquot [Aliquot] the aliquot being compared From 0c9f815235720f3adb996bc8549d752694320537 Mon Sep 17 00:00:00 2001 From: sabrine33 <139128026+sabrine33@users.noreply.github.com> Date: Wed, 13 May 2026 14:13:04 +0100 Subject: [PATCH 4/4] Update app/models/under_rep_well_comments_to_broadcast.rb Co-authored-by: KatyTaylor --- app/models/under_rep_well_comments_to_broadcast.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/under_rep_well_comments_to_broadcast.rb b/app/models/under_rep_well_comments_to_broadcast.rb index c47018f122..a3cae62630 100644 --- a/app/models/under_rep_well_comments_to_broadcast.rb +++ b/app/models/under_rep_well_comments_to_broadcast.rb @@ -73,7 +73,7 @@ def under_represented_well_comments # library requests in the lane's ancestor plates, then building a comment for # each associated poly_metadatum. # - # @param lane [Lane] the lane asset associated with the batch request + # @param lane [Lane] the lane receptacle associated with the batch request # @param batch_request [Request] the sequencing request whose position is used in the comment # @return [Array] comments built for the given lane def build_comments_for_lane(lane, batch_request)