From af8d255fd3f2a6681e26ce52419602a115fc949a Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 24 Mar 2026 13:16:35 +0000 Subject: [PATCH 01/11] added wafer size to ultima requests and pipeline --- app/models/bulk_submission.rb | 6 ++-- app/models/ultima_sequencing_pipeline.rb | 9 ++++++ app/models/ultima_sequencing_request.rb | 5 ++- app/validators/ultima_validator.rb | 10 +++++- app/views/bulk_submissions/_guidance.html.erb | 1 + config/bulk_submission_excel/columns.yml | 19 +++++++++++ config/bulk_submission_excel/ranges.yml | 5 +++ .../003_ultima_request_information_types.yml | 7 ++++ config/locales/metadata/en.yml | 3 ++ ...2536_add_wafer_size_to_request_metadata.rb | 10 ++++++ db/schema.rb | 3 +- spec/data/bulk_submission_excel/columns.yml | 19 +++++++++++ spec/data/bulk_submission_excel/ranges.yml | 5 +++ spec/factories/request_factories.rb | 3 +- .../models/ultima_sequencing_pipeline_spec.rb | 29 +++++++++++++++++ spec/models/ultima_sequencing_request_spec.rb | 8 +++++ spec/validators/ultima_validator_spec.rb | 32 +++++++++++++++++++ 17 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb diff --git a/app/models/bulk_submission.rb b/app/models/bulk_submission.rb index 170ca24bc5..0f29773072 100644 --- a/app/models/bulk_submission.rb +++ b/app/models/bulk_submission.rb @@ -253,7 +253,8 @@ def process # rubocop:todo Metrics/CyclomaticComplexity 'scrna core cells per chip well', '% phix requested', 'low diversity', - 'ot recipe' + 'ot recipe', + 'wafer size' ].freeze ALIAS_FIELDS = { 'plate barcode' => 'barcode', 'tube barcode' => 'barcode' }.freeze @@ -360,7 +361,8 @@ def extract_request_options(details) ['scrna core cells per chip well', 'cells_per_chip_well'], ['% phix requested', 'percent_phix_requested'], ['low diversity', 'low_diversity'], - ['ot recipe', 'ot_recipe'] + ['ot recipe', 'ot_recipe'], + ['wafer size', 'wafer_size'] ].each do |source_key, target_key| assign_value_if_source_present(details, source_key, request_options, target_key) end diff --git a/app/models/ultima_sequencing_pipeline.rb b/app/models/ultima_sequencing_pipeline.rb index 3a28998d3b..7b0b90e847 100644 --- a/app/models/ultima_sequencing_pipeline.rb +++ b/app/models/ultima_sequencing_pipeline.rb @@ -11,6 +11,15 @@ def ot_recipe_consistent_for_batch?(batch) (ot_recipe_list.uniq.size == 1) end + def wafer_size_consistent_for_batch?(batch) + wafer_size_list = batch.requests.filter_map { |request| request.request_metadata.wafer_size } + + # There are some requests that don't have the wafer_size attribute + return false if wafer_size_list.size != batch.requests.size + + (wafer_size_list.uniq.size == 1) + end + def post_release_batch(batch, _user) # Same logic as the superclass, but with a different Messenger root and template batch.assets.compact.uniq.each(&:index_aliquots) diff --git a/app/models/ultima_sequencing_request.rb b/app/models/ultima_sequencing_request.rb index 63be2fae7b..11026d6389 100644 --- a/app/models/ultima_sequencing_request.rb +++ b/app/models/ultima_sequencing_request.rb @@ -6,6 +6,7 @@ class UltimaSequencingRequest < SequencingRequest FREE = 'Free' FLEX = 'Flex' OT_RECIPE_OPTIONS = [FREE, FLEX].freeze + WAFER_SIZE_OPTIONS = %w[5TB 10TB 20TB].freeze has_metadata as: Request do # Defining the sequencing request metadata here again, as 'has_metadata' @@ -25,11 +26,13 @@ class UltimaSequencingRequest < SequencingRequest custom_attribute(:ot_recipe, default: FREE, in: OT_RECIPE_OPTIONS, required: true) enum :ot_recipe, { Free: 0, Flex: 1 } + custom_attribute(:wafer_size, default: '10TB', in: WAFER_SIZE_OPTIONS, required: true) + enum :wafer_size, { '5TB': 0, '10TB': 1, '20TB': 2 } end # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests. # This delegation has no real effect outside of the tests. - delegate :ot_recipe, to: :request_metadata + delegate :ot_recipe, :wafer_size, to: :request_metadata # Generates unique wafer ID, concatenation of batch_for_opentrons, # id_pool_lims, and request_order. diff --git a/app/validators/ultima_validator.rb b/app/validators/ultima_validator.rb index bc69826792..302e4a5829 100644 --- a/app/validators/ultima_validator.rb +++ b/app/validators/ultima_validator.rb @@ -2,16 +2,18 @@ class UltimaValidator < ActiveModel::Validator TWO_REQUESTS_MSG = 'Batches must contain exactly two requests.' OT_RECIPE_CONSISTENT_MSG = 'OT Recipe must be the same for both requests.' + WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' # Used in _pipeline_limit.html to display custom validation warnings def self.validation_info - 'OT Recipe must be the same for both requests.' + 'OT Recipe and Wafer Size must be the same for both requests.' end # Validates that a batch contains the two requests. def validate(record) validate_exactly_two_requests(record) requests_have_same_ot_recipe(record) + requests_have_same_wafer_size(record) end private @@ -27,4 +29,10 @@ def requests_have_same_ot_recipe(record) record.errors.add(:base, OT_RECIPE_CONSISTENT_MSG) end + + def requests_have_same_wafer_size(record) + return if record.pipeline.wafer_size_consistent_for_batch?(record) + + record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) + end end diff --git a/app/views/bulk_submissions/_guidance.html.erb b/app/views/bulk_submissions/_guidance.html.erb index 6532b2d383..4706dfc832 100644 --- a/app/views/bulk_submissions/_guidance.html.erb +++ b/app/views/bulk_submissions/_guidance.html.erb @@ -47,4 +47,5 @@ used to split a submission up into orders, and as a result must have the same re
% PhiX requested
Required percentage of PhiX needed.
Low Diversity
Low Diversity value being "Yes" or "No"
OT Recipe
OT (OpenTron liquid handler) Recipe value being "Free" or "Flex"
+
Wafer Size
Wafer size value being "5TB", "10TB" or "20TB"
diff --git a/config/bulk_submission_excel/columns.yml b/config/bulk_submission_excel/columns.yml index 02de62cb55..8e265391cb 100644 --- a/config/bulk_submission_excel/columns.yml +++ b/config/bulk_submission_excel/columns.yml @@ -367,3 +367,22 @@ ot_recipe: range_name: :ot_recipe conditional_formattings: empty_cell: +wafer_size: + heading: "Wafer Size" + unlocked: true + attribute: :wafer_size + validation: + options: + type: :list + formula1: "$A$1:$A$2" + allowBlank: false + showInputMessage: true + showErrorMessage: true + errorStyle: :stop + errorTitle: "Wafer Size" + error: "You must enter a value from the list provided." + promptTitle: "Wafer Size" + prompt: "Select a value type from the approved list" + range_name: :wafer_size + conditional_formattings: + empty_cell: diff --git a/config/bulk_submission_excel/ranges.yml b/config/bulk_submission_excel/ranges.yml index a1c091015a..b211a116cb 100644 --- a/config/bulk_submission_excel/ranges.yml +++ b/config/bulk_submission_excel/ranges.yml @@ -27,3 +27,8 @@ ot_recipe: options: - "Free" - "Flex" +wafer_size: + options: + - "5TB" + - "10TB" + - "20TB" diff --git a/config/default_records/request_information_types/003_ultima_request_information_types.yml b/config/default_records/request_information_types/003_ultima_request_information_types.yml index 39cabd99c3..6fa20cb84e 100644 --- a/config/default_records/request_information_types/003_ultima_request_information_types.yml +++ b/config/default_records/request_information_types/003_ultima_request_information_types.yml @@ -6,6 +6,13 @@ ot_recipe: width: 5 hide_in_inbox: 0 +wafer_size: + name: Wafer size + key: wafer_size + label: Wafer size + width: 5 + hide_in_inbox: 0 + fragment_size_required_from: name: Fragment size required (from) key: fragment_size_required_from diff --git a/config/locales/metadata/en.yml b/config/locales/metadata/en.yml index 08f1fbd67e..1cee050a2e 100644 --- a/config/locales/metadata/en.yml +++ b/config/locales/metadata/en.yml @@ -79,6 +79,9 @@ en: ot_recipe: label: OT Recipe + wafer_size: + label: Wafer Size + library_creation_request: <<: *REQUEST sequencing_request: diff --git a/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb new file mode 100644 index 0000000000..60f9adc656 --- /dev/null +++ b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# This migration adds a wafer_size column to the request_metadata table, which is used to store +# the wafer size for Ultima sequencing requests. This is stored as an integer enum, with possible +# values of 5TB, 10TB, and 20TB at time of writing. +class AddWaferSizeToRequestMetadata < ActiveRecord::Migration[7.1] + def change + add_column :request_metadata, :wafer_size, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 34cce1cefe..44a678a359 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_17_142326) do +ActiveRecord::Schema[7.2].define(version: 2026_03_24_112536) do create_table "accession_sample_statuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.integer "sample_id", null: false t.string "status", null: false @@ -1191,6 +1191,7 @@ t.boolean "low_diversity" t.integer "percent_phix_requested" t.integer "ot_recipe" + t.integer "wafer_size" t.index ["request_id"], name: "index_request_metadata_on_request_id" end diff --git a/spec/data/bulk_submission_excel/columns.yml b/spec/data/bulk_submission_excel/columns.yml index 03280783ee..ab5bd8739e 100644 --- a/spec/data/bulk_submission_excel/columns.yml +++ b/spec/data/bulk_submission_excel/columns.yml @@ -315,3 +315,22 @@ ot_recipe: range_name: :ot_recipe conditional_formattings: empty_cell: +wafer_size: + heading: "Wafer Size" + unlocked: true + attribute: :wafer_size + validation: + options: + type: :list + formula1: "$A$1:$A$2" + allowBlank: false + showInputMessage: true + showErrorMessage: true + errorStyle: :stop + errorTitle: "Wafer Size" + error: "You must enter a value from the list provided." + promptTitle: "Wafer Size" + prompt: "Select a value type from the approved list" + range_name: :wafer_size + conditional_formattings: + empty_cell: diff --git a/spec/data/bulk_submission_excel/ranges.yml b/spec/data/bulk_submission_excel/ranges.yml index a1c091015a..b211a116cb 100644 --- a/spec/data/bulk_submission_excel/ranges.yml +++ b/spec/data/bulk_submission_excel/ranges.yml @@ -27,3 +27,8 @@ ot_recipe: options: - "Free" - "Flex" +wafer_size: + options: + - "5TB" + - "10TB" + - "20TB" diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index 5a012985fc..570208fd28 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -109,7 +109,8 @@ { fragment_size_required_from: 150, fragment_size_required_to: 400, - ot_recipe: 'Free' + ot_recipe: 'Free', + wafer_size: '10TB' } end diff --git a/spec/models/ultima_sequencing_pipeline_spec.rb b/spec/models/ultima_sequencing_pipeline_spec.rb index 9e55c1018e..99cd6dca05 100644 --- a/spec/models/ultima_sequencing_pipeline_spec.rb +++ b/spec/models/ultima_sequencing_pipeline_spec.rb @@ -38,6 +38,35 @@ end end + describe '#wafer_size_consistent_for_batch?' do + it 'returns true when all requests have the same wafer_size' do + batch = pipeline.batches.build + r1 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + r2 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [r1, r2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be true + end + + it 'returns false when requests have different wafer_sizes' do + batch = pipeline.batches.build + req1 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) + req2 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [req1, req2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false + end + + it 'returns false when some requests are missing wafer_size' do + batch = pipeline.batches.build + r1 = create(:sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + r2 = create(:sequencing_request, request_metadata_attributes: {}) # no wafer_size + batch.requests << [r1, r2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false + end + end + describe '#post_release_batch' do let(:batch) { create(:batch) } diff --git a/spec/models/ultima_sequencing_request_spec.rb b/spec/models/ultima_sequencing_request_spec.rb index d152679b4e..b990cbb89d 100644 --- a/spec/models/ultima_sequencing_request_spec.rb +++ b/spec/models/ultima_sequencing_request_spec.rb @@ -39,5 +39,13 @@ expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") end end + + context 'when wafer_size value is not assigned' do + it 'is invalid and displays required wafer size error message' do + request.request_metadata.wafer_size = nil + request.validate + expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank") + end + end end end diff --git a/spec/validators/ultima_validator_spec.rb b/spec/validators/ultima_validator_spec.rb index c651ef2695..1c1ad66aa5 100644 --- a/spec/validators/ultima_validator_spec.rb +++ b/spec/validators/ultima_validator_spec.rb @@ -37,6 +37,38 @@ end end + context 'when batch contains two requests with the same Wafer Size' do + let(:pipeline) { UltimaSequencingPipeline.new } + let(:batch) { create(:batch, pipeline:) } + let(:request1) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) } + let(:request2) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) } + + before do + batch.requests << [request1, request2] + end + + it 'is valid' do + validator.validate(batch) + expect(batch.errors[:base]).to be_empty + end + end + + context 'when batch contains two requests with different wafer_size' do + let(:pipeline) { UltimaSequencingPipeline.new } + let(:batch) { create(:batch, pipeline:) } + let(:request1) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) } + let(:request2) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) } + + before do + batch.requests << [request1, request2] + end + + it 'is invalid due to wafer_size mismatch' do + validator.validate(batch) + expect(batch.errors[:base]).to include(described_class::WAFER_SIZE_CONSISTENT_MSG) + end + end + context 'when batch contains a single request' do let(:pipeline) { UltimaSequencingPipeline.new } let(:batch) { create(:batch, pipeline:) } From ede2885d58298cb3d8ddf8f8b86e40587ebfb499 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 10:01:02 +0000 Subject: [PATCH 02/11] adding ug200 request type --- app/models/ultima_sequencing_request.rb | 5 +- .../ultima_u_g_200_sequencing_request.rb | 61 +++++++++++++++++++ .../003_ultima_request_type_validators.yml | 15 +++++ .../026_ultima_ug200_request_types.yml | 11 ++++ .../021_ultima_ug200_submission_templates.yml | 10 +++ ...2536_add_wafer_size_to_request_metadata.rb | 4 +- db/schema.rb | 3 +- spec/factories/request_factories.rb | 17 +++++- spec/factories/request_type_factories.rb | 5 ++ .../ultima_ug200_sequencing_request_spec.rb | 51 ++++++++++++++++ 10 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 app/models/ultima_u_g_200_sequencing_request.rb create mode 100644 config/default_records/request_type_validators/003_ultima_request_type_validators.yml create mode 100644 config/default_records/request_types/026_ultima_ug200_request_types.yml create mode 100644 config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml create mode 100644 spec/models/ultima_ug200_sequencing_request_spec.rb diff --git a/app/models/ultima_sequencing_request.rb b/app/models/ultima_sequencing_request.rb index 11026d6389..63be2fae7b 100644 --- a/app/models/ultima_sequencing_request.rb +++ b/app/models/ultima_sequencing_request.rb @@ -6,7 +6,6 @@ class UltimaSequencingRequest < SequencingRequest FREE = 'Free' FLEX = 'Flex' OT_RECIPE_OPTIONS = [FREE, FLEX].freeze - WAFER_SIZE_OPTIONS = %w[5TB 10TB 20TB].freeze has_metadata as: Request do # Defining the sequencing request metadata here again, as 'has_metadata' @@ -26,13 +25,11 @@ class UltimaSequencingRequest < SequencingRequest custom_attribute(:ot_recipe, default: FREE, in: OT_RECIPE_OPTIONS, required: true) enum :ot_recipe, { Free: 0, Flex: 1 } - custom_attribute(:wafer_size, default: '10TB', in: WAFER_SIZE_OPTIONS, required: true) - enum :wafer_size, { '5TB': 0, '10TB': 1, '20TB': 2 } end # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests. # This delegation has no real effect outside of the tests. - delegate :ot_recipe, :wafer_size, to: :request_metadata + delegate :ot_recipe, to: :request_metadata # Generates unique wafer ID, concatenation of batch_for_opentrons, # id_pool_lims, and request_order. diff --git a/app/models/ultima_u_g_200_sequencing_request.rb b/app/models/ultima_u_g_200_sequencing_request.rb new file mode 100644 index 0000000000..ad505477a0 --- /dev/null +++ b/app/models/ultima_u_g_200_sequencing_request.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Request class specific to the Ultima UG200 sequencing platform. +# Includes specific validation for wafer combination with read type. +class UltimaUG200SequencingRequest < SequencingRequest + include Api::Messages::UseqWaferIo::LaneExtensions + + WAFER_SIZE_OPTIONS = %w[5TB 10TB 20TB].freeze + + has_metadata as: Request do + # Defining the sequencing request metadata here again, as 'has_metadata' + # does not automatically append these custom attributes to the request. + # + # The has_metadata call dynamically defines an inner Metadata class and + # takes the attributes from the block and adds them to the Metadata class. + # There is an assumption that the inner Metadata class is available in a + # sequencing request class defintion. Calling has_metadata again does not + # inherit the attributes given in the block supplied in the superclass. + # They need to be supplied again for this class for a proper inner Metadata + # class definition. In a future refactoring these attributes can be moved a + # class attribute and subclasses can merge its own attibutes to that. A + # common method can set up the inner Metadata class in the subclasses. + custom_attribute(:fragment_size_required_from, integer: true, minimum: 1) + custom_attribute(:fragment_size_required_to, integer: true, minimum: 1) + + # TODO: the defaults set here do NOT work on the option lists in screens for some reason. + custom_attribute(:wafer_size, default: '10TB', in: WAFER_SIZE_OPTIONS, required: true) + enum :wafer_size, { '5TB': 0, '10TB': 1, '20TB': 2 } + custom_attribute(:read_length, default: 300, integer: true, validator: true, required: true, selection: true) + end + + # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests. + # This delegation has no real effect outside of the tests. + delegate :wafer_size, :read_length, to: :request_metadata + + class UltimaUG200RequestOptionsValidator < DelegateValidation::Validator + delegate :wafer_size, :read_length, :request_types, to: :target + + validate :validate_read_length_by_wafer_size + + def validate_read_length_by_wafer_size + puts "Wafer Size: #{wafer_size}, Read Length: #{read_length}" + binding.pry + return if wafer_size == '10TB' && read_length.to_i == 300 + + errors.add(:read_length, + 'The user can only select a Read Length of 300 with the 10TB wafer type for Ultima UG200 requests') + end + end + + def self.delegate_validator + UltimaUG200SequencingRequest::UltimaUG200RequestOptionsValidator + end + + # Generates unique wafer ID, concatenation of batch_for_opentrons, + # id_pool_lims, and request_order. + # @return [String] unique wafer ID for LIMS + def id_wafer_lims + "#{batch.id}_#{source_labware.human_barcode}_#{position}" + end +end diff --git a/config/default_records/request_type_validators/003_ultima_request_type_validators.yml b/config/default_records/request_type_validators/003_ultima_request_type_validators.yml new file mode 100644 index 0000000000..8226313081 --- /dev/null +++ b/config/default_records/request_type_validators/003_ultima_request_type_validators.yml @@ -0,0 +1,15 @@ +--- +WaferSizeRequestedUltimaUG200Sequencing: + request_type_key: ultima_u_g_200_sequencing + request_option: wafer_size + valid_options: + - 5TB + - 10TB + - 20TB +ReadLengthRequestedUltimaUG200Sequencing: + request_type_key: ultima_u_g_200_sequencing + request_option: read_length + valid_options: + - 150 + - 200 + - 300 diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.yml b/config/default_records/request_types/026_ultima_ug200_request_types.yml new file mode 100644 index 0000000000..35581520fd --- /dev/null +++ b/config/default_records/request_types/026_ultima_ug200_request_types.yml @@ -0,0 +1,11 @@ +# Request types for Ultima UG200 sequencing platform. +--- +ultima_u_g_200_sequencing: + name: Ultima UG200 sequencing + asset_type: LibraryTube + order: 2 + initial_state: pending + billable: true + product_line_name: Ultima + request_class_name: UltimaUG200SequencingRequest + request_purpose: standard diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml new file mode 100644 index 0000000000..27b6a6a6b7 --- /dev/null +++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml @@ -0,0 +1,10 @@ +# This submission template is associated with the Ultima PCR Free Library preparation pipeline +# and the Ultima UG200 sequencing platform. +--- +Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing: + submission_class_name: "LinearSubmission" + related_records: + request_type_keys: ["limber_ultima_htp_pcr_free", "limber_multiplexing_ultima", "ultima_u_g_200_sequencing"] + order_role: PCR Free + product_line_name: Ultima + product_catalogue_name: GenericNoPCR diff --git a/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb index 60f9adc656..f1e811d8f0 100644 --- a/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb +++ b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true # This migration adds a wafer_size column to the request_metadata table, which is used to store -# the wafer size for Ultima sequencing requests. This is stored as an integer enum, with possible +# the wafer size for Ultima sequencing requests. This is stored as a string, with possible # values of 5TB, 10TB, and 20TB at time of writing. class AddWaferSizeToRequestMetadata < ActiveRecord::Migration[7.1] def change - add_column :request_metadata, :wafer_size, :integer + add_column :request_metadata, :wafer_size, :string end end diff --git a/db/schema.rb b/db/schema.rb index 44a678a359..34cce1cefe 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_24_112536) do +ActiveRecord::Schema[7.2].define(version: 2026_03_17_142326) do create_table "accession_sample_statuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.integer "sample_id", null: false t.string "status", null: false @@ -1191,7 +1191,6 @@ t.boolean "low_diversity" t.integer "percent_phix_requested" t.integer "ot_recipe" - t.integer "wafer_size" t.index ["request_id"], name: "index_request_metadata_on_request_id" end diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index 570208fd28..fcb9e714a8 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -109,8 +109,7 @@ { fragment_size_required_from: 150, fragment_size_required_to: 400, - ot_recipe: 'Free', - wafer_size: '10TB' + ot_recipe: 'Free' } end @@ -125,6 +124,20 @@ end end + factory(:ultima_u_g_200_sequencing_request) do + request_type factory: %i[ultima_u_g_200_sequencing] + request_purpose { :standard } + sti_type { 'UltimaUG200SequencingRequest' } + request_metadata_attributes do + { + fragment_size_required_from: 150, + fragment_size_required_to: 400, + read_length: 300, + wafer_size: '10TB' + } + end + end + factory(:library_creation_request, parent: :request, class: 'LibraryCreationRequest') do asset factory: %i[sample_tube] request_type factory: %i[library_creation_request_type] diff --git a/spec/factories/request_type_factories.rb b/spec/factories/request_type_factories.rb index 4d18e1eb3d..536c7ad123 100644 --- a/spec/factories/request_type_factories.rb +++ b/spec/factories/request_type_factories.rb @@ -139,6 +139,11 @@ request_class { UltimaSequencingRequest } end + factory :ultima_u_g_200_sequencing do + asset_type { 'LibraryTube' } + request_class { UltimaUG200SequencingRequest } + end + factory :miseq_sequencing_request_type do request_class { MiSeqSequencingRequest } asset_type { 'LibraryTube' } diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb new file mode 100644 index 0000000000..424e4020cb --- /dev/null +++ b/spec/models/ultima_ug200_sequencing_request_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UltimaUG200SequencingRequest do + let(:request) { create(:ultima_u_g_200_sequencing_request) } + + describe 'Validations' do + context 'when all attributes are valid' do + it 'is valid' do + expect(request).to be_valid + end + end + + context 'when fragment_size_required_from is less than 1' do + it 'is invalid and displays fragment size from error message' do + request.request_metadata.fragment_size_required_from = 0 + request.validate + expect(request.errors[:'request_metadata.fragment_size_required_from']).to include( + 'must be greater than or equal to 1' + ) + end + end + + context 'when fragment_size_required_to is less than 1' do + it 'is invalid and displays fragment size to error message' do + request.request_metadata.fragment_size_required_to = 0 + request.validate + expect(request.errors[:'request_metadata.fragment_size_required_to']).to include( + 'must be greater than or equal to 1' + ) + end + end + + context 'when ot_recipe value is not assigned' do + it 'is invalid and displays required percent phix requested error message' do + request.request_metadata.ot_recipe = nil + request.validate + expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") + end + end + + context 'when wafer_size value is not assigned' do + it 'is invalid and displays required wafer size error message' do + request.request_metadata.wafer_size = nil + request.validate + expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank") + end + end + end +end From 5ab4f98dc574786e4f83509710db02aeb14320f4 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 10:03:11 +0000 Subject: [PATCH 03/11] change to schema --- db/schema.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 34cce1cefe..4f5e30ff8a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_17_142326) do +ActiveRecord::Schema[7.2].define(version: 2026_03_24_112536) do create_table "accession_sample_statuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.integer "sample_id", null: false t.string "status", null: false @@ -1191,6 +1191,7 @@ t.boolean "low_diversity" t.integer "percent_phix_requested" t.integer "ot_recipe" + t.string "wafer_size" t.index ["request_id"], name: "index_request_metadata_on_request_id" end From 9b576426369c8c9582b16c43888fa01e2626d666 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 13:04:45 +0000 Subject: [PATCH 04/11] Added inflection for UG200 Various config refactoring --- app/models/ultima_sequencing_pipeline.rb | 11 +-------- .../ultima_ug200_sequencing_pipeline.rb | 13 +++++++++++ ....rb => ultima_ug200_sequencing_request.rb} | 10 +++----- app/validators/ultima_ug200_validator.rb | 23 +++++++++++++++++++ app/validators/ultima_validator.rb | 10 +------- ...200_pipeline_request_information_types.yml | 17 ++++++++++++++ .../pipelines/004_ultima_ug200_pipelines.yml | 17 ++++++++++++++ .../003_ultima_request_information_types.yml | 7 ------ ...ultima_ug200_request_information_types.yml | 7 ++++++ ..._ultima_ug200_request_type_validators.yml} | 4 ++-- .../026_ultima_ug200_request_types.yml | 3 ++- .../021_ultima_ug200_submission_templates.yml | 10 +++++++- config/initializers/inflections.rb | 1 + config/locales/metadata/en.yml | 3 +++ spec/factories/request_factories.rb | 4 ++-- spec/factories/request_type_factories.rb | 2 +- .../ultima_ug200_sequencing_request_spec.rb | 2 +- 17 files changed, 103 insertions(+), 41 deletions(-) create mode 100644 app/models/ultima_ug200_sequencing_pipeline.rb rename app/models/{ultima_u_g_200_sequencing_request.rb => ultima_ug200_sequencing_request.rb} (89%) create mode 100644 app/validators/ultima_ug200_validator.rb create mode 100644 config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml create mode 100644 config/default_records/pipelines/004_ultima_ug200_pipelines.yml create mode 100644 config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml rename config/default_records/request_type_validators/{003_ultima_request_type_validators.yml => 003_ultima_ug200_request_type_validators.yml} (72%) diff --git a/app/models/ultima_sequencing_pipeline.rb b/app/models/ultima_sequencing_pipeline.rb index 7b0b90e847..639c7ee349 100644 --- a/app/models/ultima_sequencing_pipeline.rb +++ b/app/models/ultima_sequencing_pipeline.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Specialized sequencing pipeline for Ultima +# Specialized sequencing pipeline for Ultima UG100 class UltimaSequencingPipeline < SequencingPipeline def ot_recipe_consistent_for_batch?(batch) ot_recipe_list = batch.requests.filter_map { |request| request.request_metadata.ot_recipe } @@ -11,15 +11,6 @@ def ot_recipe_consistent_for_batch?(batch) (ot_recipe_list.uniq.size == 1) end - def wafer_size_consistent_for_batch?(batch) - wafer_size_list = batch.requests.filter_map { |request| request.request_metadata.wafer_size } - - # There are some requests that don't have the wafer_size attribute - return false if wafer_size_list.size != batch.requests.size - - (wafer_size_list.uniq.size == 1) - end - def post_release_batch(batch, _user) # Same logic as the superclass, but with a different Messenger root and template batch.assets.compact.uniq.each(&:index_aliquots) diff --git a/app/models/ultima_ug200_sequencing_pipeline.rb b/app/models/ultima_ug200_sequencing_pipeline.rb new file mode 100644 index 0000000000..896f4e7696 --- /dev/null +++ b/app/models/ultima_ug200_sequencing_pipeline.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Specialized sequencing pipeline for Ultima UG200 +class UltimaUG200SequencingPipeline < UltimaSequencingPipeline + def wafer_size_consistent_for_batch?(batch) + wafer_size_list = batch.requests.filter_map { |request| request.request_metadata.wafer_size } + + # There are some requests that don't have the wafer_size attribute + return false if wafer_size_list.size != batch.requests.size + + (wafer_size_list.uniq.size == 1) + end +end diff --git a/app/models/ultima_u_g_200_sequencing_request.rb b/app/models/ultima_ug200_sequencing_request.rb similarity index 89% rename from app/models/ultima_u_g_200_sequencing_request.rb rename to app/models/ultima_ug200_sequencing_request.rb index ad505477a0..2f7b569f91 100644 --- a/app/models/ultima_u_g_200_sequencing_request.rb +++ b/app/models/ultima_ug200_sequencing_request.rb @@ -5,8 +5,6 @@ class UltimaUG200SequencingRequest < SequencingRequest include Api::Messages::UseqWaferIo::LaneExtensions - WAFER_SIZE_OPTIONS = %w[5TB 10TB 20TB].freeze - has_metadata as: Request do # Defining the sequencing request metadata here again, as 'has_metadata' # does not automatically append these custom attributes to the request. @@ -23,9 +21,9 @@ class UltimaUG200SequencingRequest < SequencingRequest custom_attribute(:fragment_size_required_from, integer: true, minimum: 1) custom_attribute(:fragment_size_required_to, integer: true, minimum: 1) - # TODO: the defaults set here do NOT work on the option lists in screens for some reason. - custom_attribute(:wafer_size, default: '10TB', in: WAFER_SIZE_OPTIONS, required: true) - enum :wafer_size, { '5TB': 0, '10TB': 1, '20TB': 2 } + # TODO: the defaults set here do NOT work on the option lists in the bulk submission screen, + # but do work on the request additional sequencing screen for some reason. + custom_attribute(:wafer_size, default: '10TB', validator: true, required: true, selection: true) custom_attribute(:read_length, default: 300, integer: true, validator: true, required: true, selection: true) end @@ -39,8 +37,6 @@ class UltimaUG200RequestOptionsValidator < DelegateValidation::Validator validate :validate_read_length_by_wafer_size def validate_read_length_by_wafer_size - puts "Wafer Size: #{wafer_size}, Read Length: #{read_length}" - binding.pry return if wafer_size == '10TB' && read_length.to_i == 300 errors.add(:read_length, diff --git a/app/validators/ultima_ug200_validator.rb b/app/validators/ultima_ug200_validator.rb new file mode 100644 index 0000000000..ed2b32eb9d --- /dev/null +++ b/app/validators/ultima_ug200_validator.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +class UltimaUG200Validator < UltimaValidator + WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' + + # Used in _pipeline_limit.html to display custom validation warnings + def self.validation_info + 'Wafer Size must be the same for both requests.' + end + + # Validates that a batch contains the two requests. + def validate(record) + validate_exactly_two_requests(record) + requests_have_same_wafer_size(record) + end + + private + + def requests_have_same_wafer_size(record) + return if record.pipeline.wafer_size_consistent_for_batch?(record) + + record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) + end +end diff --git a/app/validators/ultima_validator.rb b/app/validators/ultima_validator.rb index 302e4a5829..bc69826792 100644 --- a/app/validators/ultima_validator.rb +++ b/app/validators/ultima_validator.rb @@ -2,18 +2,16 @@ class UltimaValidator < ActiveModel::Validator TWO_REQUESTS_MSG = 'Batches must contain exactly two requests.' OT_RECIPE_CONSISTENT_MSG = 'OT Recipe must be the same for both requests.' - WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' # Used in _pipeline_limit.html to display custom validation warnings def self.validation_info - 'OT Recipe and Wafer Size must be the same for both requests.' + 'OT Recipe must be the same for both requests.' end # Validates that a batch contains the two requests. def validate(record) validate_exactly_two_requests(record) requests_have_same_ot_recipe(record) - requests_have_same_wafer_size(record) end private @@ -29,10 +27,4 @@ def requests_have_same_ot_recipe(record) record.errors.add(:base, OT_RECIPE_CONSISTENT_MSG) end - - def requests_have_same_wafer_size(record) - return if record.pipeline.wafer_size_consistent_for_batch?(record) - - record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) - end end diff --git a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml new file mode 100644 index 0000000000..ca846cb34c --- /dev/null +++ b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml @@ -0,0 +1,17 @@ +--- +FragmentSizeRequiredFromInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: fragment_size_required_from + +FragmentSizeRequiredToInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: fragment_size_required_to + +WaferSizeInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: wafer_size + +# TODO: does readlength need to be here +ReadLengthInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: read_length diff --git a/config/default_records/pipelines/004_ultima_ug200_pipelines.yml b/config/default_records/pipelines/004_ultima_ug200_pipelines.yml new file mode 100644 index 0000000000..21b128df05 --- /dev/null +++ b/config/default_records/pipelines/004_ultima_ug200_pipelines.yml @@ -0,0 +1,17 @@ +--- +Ultima UG200: + name: Ultima UG200 Sequencing + sti_type: UltimaUG200SequencingPipeline + validator_class_name: UltimaUG200Validator + sorter: 10 + max_size: 2 + summary: 1 + externally_managed: 0 + group_name: Sequencing + control_request_type_id: 0 + min_size: 1 + request_type_keys: + - ultima_ug200_sequencing + workflow: + name: Ultima UG200 + item_limit: 2 diff --git a/config/default_records/request_information_types/003_ultima_request_information_types.yml b/config/default_records/request_information_types/003_ultima_request_information_types.yml index 6fa20cb84e..39cabd99c3 100644 --- a/config/default_records/request_information_types/003_ultima_request_information_types.yml +++ b/config/default_records/request_information_types/003_ultima_request_information_types.yml @@ -6,13 +6,6 @@ ot_recipe: width: 5 hide_in_inbox: 0 -wafer_size: - name: Wafer size - key: wafer_size - label: Wafer size - width: 5 - hide_in_inbox: 0 - fragment_size_required_from: name: Fragment size required (from) key: fragment_size_required_from diff --git a/config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml b/config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml new file mode 100644 index 0000000000..bde23fdb31 --- /dev/null +++ b/config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml @@ -0,0 +1,7 @@ +--- +wafer_size: + name: Wafer size + key: wafer_size + label: Wafer size + width: 5 + hide_in_inbox: 0 diff --git a/config/default_records/request_type_validators/003_ultima_request_type_validators.yml b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml similarity index 72% rename from config/default_records/request_type_validators/003_ultima_request_type_validators.yml rename to config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml index 8226313081..cf37fb6f84 100644 --- a/config/default_records/request_type_validators/003_ultima_request_type_validators.yml +++ b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml @@ -1,13 +1,13 @@ --- WaferSizeRequestedUltimaUG200Sequencing: - request_type_key: ultima_u_g_200_sequencing + request_type_key: ultima_ug200_sequencing request_option: wafer_size valid_options: - 5TB - 10TB - 20TB ReadLengthRequestedUltimaUG200Sequencing: - request_type_key: ultima_u_g_200_sequencing + request_type_key: ultima_ug200_sequencing request_option: read_length valid_options: - 150 diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.yml b/config/default_records/request_types/026_ultima_ug200_request_types.yml index 35581520fd..99dbf035b3 100644 --- a/config/default_records/request_types/026_ultima_ug200_request_types.yml +++ b/config/default_records/request_types/026_ultima_ug200_request_types.yml @@ -1,11 +1,12 @@ # Request types for Ultima UG200 sequencing platform. --- -ultima_u_g_200_sequencing: +ultima_ug200_sequencing: name: Ultima UG200 sequencing asset_type: LibraryTube order: 2 initial_state: pending billable: true + # Using same product line name as Ultima UG100 product_line_name: Ultima request_class_name: UltimaUG200SequencingRequest request_purpose: standard diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml index 27b6a6a6b7..cec0787649 100644 --- a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml +++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml @@ -4,7 +4,15 @@ Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing: submission_class_name: "LinearSubmission" related_records: - request_type_keys: ["limber_ultima_htp_pcr_free", "limber_multiplexing_ultima", "ultima_u_g_200_sequencing"] + request_type_keys: ["limber_ultima_htp_pcr_free", "limber_multiplexing_ultima", "ultima_ug200_sequencing"] + order_role: PCR Free + product_line_name: Ultima + product_catalogue_name: GenericNoPCR +# TODO: assumption that we need automated version for UG200, but is this the case? +Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing Automated: + submission_class_name: "LinearSubmission" + related_records: + request_type_keys: ["ultima_ug200_sequencing"] order_role: PCR Free product_line_name: Ultima product_catalogue_name: GenericNoPCR diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 55a6a2b9bd..984e1a1a24 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -18,6 +18,7 @@ inflect.acronym 'ENA' # European Nucleotide Archive inflect.acronym 'HTTP' # HyperText Transfer Protocol inflect.acronym 'EBI' # European Bioinformatics Institute + inflect.acronym 'UG200' # Ultima UG200 end # These inflection rules are supported but not enabled by default: diff --git a/config/locales/metadata/en.yml b/config/locales/metadata/en.yml index 1cee050a2e..da8fa0683b 100644 --- a/config/locales/metadata/en.yml +++ b/config/locales/metadata/en.yml @@ -110,6 +110,9 @@ en: ultima_sequencing_request: <<: *REQUEST + ultima_ug200_sequencing_request: + <<: *REQUEST + pulldown: requests: wgs_library_request: diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index fcb9e714a8..622aa97d85 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -124,8 +124,8 @@ end end - factory(:ultima_u_g_200_sequencing_request) do - request_type factory: %i[ultima_u_g_200_sequencing] + factory(:ultima_ug200_sequencing_request) do + request_type factory: %i[ultima_ug200_sequencing] request_purpose { :standard } sti_type { 'UltimaUG200SequencingRequest' } request_metadata_attributes do diff --git a/spec/factories/request_type_factories.rb b/spec/factories/request_type_factories.rb index 536c7ad123..87b2fecfd4 100644 --- a/spec/factories/request_type_factories.rb +++ b/spec/factories/request_type_factories.rb @@ -139,7 +139,7 @@ request_class { UltimaSequencingRequest } end - factory :ultima_u_g_200_sequencing do + factory :ultima_ug200_sequencing do asset_type { 'LibraryTube' } request_class { UltimaUG200SequencingRequest } end diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb index 424e4020cb..d1163868ee 100644 --- a/spec/models/ultima_ug200_sequencing_request_spec.rb +++ b/spec/models/ultima_ug200_sequencing_request_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe UltimaUG200SequencingRequest do - let(:request) { create(:ultima_u_g_200_sequencing_request) } + let(:request) { create(:ultima_ug200_sequencing_request) } describe 'Validations' do context 'when all attributes are valid' do From 2d737b3aff173e3e88d3675f19acae11f7bca4c3 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 14:58:53 +0000 Subject: [PATCH 05/11] added UG200 task descriptors (draft may change) --- .../005_ultima_ug200_descriptors.yml | 85 +++++++++++++++++++ .../tasks/005_ultima_ug200_tasks.yml | 13 +++ 2 files changed, 98 insertions(+) create mode 100644 config/default_records/descriptors/005_ultima_ug200_descriptors.yml create mode 100644 config/default_records/tasks/005_ultima_ug200_tasks.yml diff --git a/config/default_records/descriptors/005_ultima_ug200_descriptors.yml b/config/default_records/descriptors/005_ultima_ug200_descriptors.yml new file mode 100644 index 0000000000..d5c77bcb3e --- /dev/null +++ b/config/default_records/descriptors/005_ultima_ug200_descriptors.yml @@ -0,0 +1,85 @@ +--- +"OTR carrier Lot #": + name: "OTR carrier Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 0 +OTR carrier expiry: + name: OTR carrier expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 1 +"Reaction Mix 7 Lot #": + name: "Reaction Mix 7 Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 2 +Reaction Mix 7 expiry: + name: Reaction Mix 7 expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 3 +"NFW Lot #": + name: "NFW Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 4 +NFW expiry: + name: NFW expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 5 +"Oil Lot #": + name: "Oil Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 6 +Oil expiry: + name: Oil expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 7 +Pipette carousel: + name: Pipette carousel + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 8 +Opentrons Inst. Name: + name: Opentrons Inst. Name + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: true + sorter: 9 +Assign Control Bead Tube: + name: Assign Control Bead Tube + task: Amp + workflow: Ultima UG200 + kind: Text + required: false + sorter: 0 +UG AMP Inst. Name: + name: UG AMP Inst. Name + task: Amp + workflow: Ultima UG200 + kind: Text + required: true + sorter: 1 diff --git a/config/default_records/tasks/005_ultima_ug200_tasks.yml b/config/default_records/tasks/005_ultima_ug200_tasks.yml new file mode 100644 index 0000000000..c031066648 --- /dev/null +++ b/config/default_records/tasks/005_ultima_ug200_tasks.yml @@ -0,0 +1,13 @@ +--- +Opentrons: + name: Opentrons + workflow: Ultima UG200 + sorted: 0 + lab_activity: true + sti_type: SetDescriptorsTask +Amp: + name: Amp + workflow: Ultima UG200 + sorted: 1 + lab_activity: true + sti_type: SetDescriptorsTask From 4a746a7f82eea621959b1744a670ade19e2895aa Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 15:27:58 +0000 Subject: [PATCH 06/11] fixed tests --- spec/models/ultima_sequencing_request_spec.rb | 8 -------- .../ultima_ug200_sequencing_request_spec.rb | 16 ++++++++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/spec/models/ultima_sequencing_request_spec.rb b/spec/models/ultima_sequencing_request_spec.rb index b990cbb89d..d152679b4e 100644 --- a/spec/models/ultima_sequencing_request_spec.rb +++ b/spec/models/ultima_sequencing_request_spec.rb @@ -39,13 +39,5 @@ expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") end end - - context 'when wafer_size value is not assigned' do - it 'is invalid and displays required wafer size error message' do - request.request_metadata.wafer_size = nil - request.validate - expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank") - end - end end end diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb index d1163868ee..b0b2696b46 100644 --- a/spec/models/ultima_ug200_sequencing_request_spec.rb +++ b/spec/models/ultima_ug200_sequencing_request_spec.rb @@ -32,14 +32,6 @@ end end - context 'when ot_recipe value is not assigned' do - it 'is invalid and displays required percent phix requested error message' do - request.request_metadata.ot_recipe = nil - request.validate - expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") - end - end - context 'when wafer_size value is not assigned' do it 'is invalid and displays required wafer size error message' do request.request_metadata.wafer_size = nil @@ -47,5 +39,13 @@ expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank") end end + + context 'when read_length value is not assigned' do + it 'is invalid and displays required read length error message' do + request.request_metadata.read_length = nil + request.validate + expect(request.errors[:'request_metadata.read_length']).to include("can't be blank") + end + end end end From 16e939c4367e114876495c4ef2e8d03cdd5980c1 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 16:32:16 +0000 Subject: [PATCH 07/11] fixed tests --- .../models/ultima_sequencing_pipeline_spec.rb | 29 ------------ .../ultima_ug200_sequencing_pipeline_spec.rb | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 spec/models/ultima_ug200_sequencing_pipeline_spec.rb diff --git a/spec/models/ultima_sequencing_pipeline_spec.rb b/spec/models/ultima_sequencing_pipeline_spec.rb index 99cd6dca05..9e55c1018e 100644 --- a/spec/models/ultima_sequencing_pipeline_spec.rb +++ b/spec/models/ultima_sequencing_pipeline_spec.rb @@ -38,35 +38,6 @@ end end - describe '#wafer_size_consistent_for_batch?' do - it 'returns true when all requests have the same wafer_size' do - batch = pipeline.batches.build - r1 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) - r2 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) - batch.requests << [r1, r2] - - expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be true - end - - it 'returns false when requests have different wafer_sizes' do - batch = pipeline.batches.build - req1 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) - req2 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) - batch.requests << [req1, req2] - - expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false - end - - it 'returns false when some requests are missing wafer_size' do - batch = pipeline.batches.build - r1 = create(:sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) - r2 = create(:sequencing_request, request_metadata_attributes: {}) # no wafer_size - batch.requests << [r1, r2] - - expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false - end - end - describe '#post_release_batch' do let(:batch) { create(:batch) } diff --git a/spec/models/ultima_ug200_sequencing_pipeline_spec.rb b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb new file mode 100644 index 0000000000..1f792086cd --- /dev/null +++ b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe UltimaUG200SequencingPipeline do + let(:pipeline) do + described_class.new( + workflow: Workflow.new, + request_types: [create(:ultima_ug200_sequencing)] + ) + end + + describe '#wafer_size_consistent_for_batch?' do + it 'returns true when all requests have the same wafer_size' do + batch = pipeline.batches.build + r1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + r2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [r1, r2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be true + end + + it 'returns false when requests have different wafer_sizes' do + batch = pipeline.batches.build + req1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) + req2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [req1, req2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false + end + + # NB. Wafer size is a required field, so cannot be missing. Tested in the request spec. + end + + describe '#post_release_batch' do + let(:batch) { create(:batch) } + + it 'calls Messenger with UseqWaferIo template and useq_wafer root' do + allow(Messenger).to receive(:create!) + pipeline.post_release_batch(batch, create(:user)) + + expect(Messenger).to have_received(:create!).with( + hash_including(target: batch, template: 'UseqWaferIo', root: 'useq_wafer') + ) + end + end +end From a988ce72f10710ad0e69745a6cc9cbefe2c0826a Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 27 Mar 2026 14:37:10 +0000 Subject: [PATCH 08/11] modified ug200 sequencing request to remove read length and add ot recipe --- app/models/ultima_ug200_sequencing_request.rb | 28 +++++------------- ...200_pipeline_request_information_types.yml | 7 ++--- ...3_ultima_ug200_request_type_validators.yml | 7 ----- spec/factories/request_factories.rb | 2 +- .../ultima_ug200_sequencing_pipeline_spec.rb | 29 +++++++++++++++++++ 5 files changed, 41 insertions(+), 32 deletions(-) diff --git a/app/models/ultima_ug200_sequencing_request.rb b/app/models/ultima_ug200_sequencing_request.rb index 2f7b569f91..0587f149dc 100644 --- a/app/models/ultima_ug200_sequencing_request.rb +++ b/app/models/ultima_ug200_sequencing_request.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true # Request class specific to the Ultima UG200 sequencing platform. -# Includes specific validation for wafer combination with read type. +# Includes wafer size and OT recipe. class UltimaUG200SequencingRequest < SequencingRequest include Api::Messages::UseqWaferIo::LaneExtensions + FREE = 'Free' + FLEX = 'Flex' + OT_RECIPE_OPTIONS = [FREE, FLEX].freeze + has_metadata as: Request do # Defining the sequencing request metadata here again, as 'has_metadata' # does not automatically append these custom attributes to the request. @@ -23,30 +27,14 @@ class UltimaUG200SequencingRequest < SequencingRequest # TODO: the defaults set here do NOT work on the option lists in the bulk submission screen, # but do work on the request additional sequencing screen for some reason. + custom_attribute(:ot_recipe, default: FREE, in: OT_RECIPE_OPTIONS, required: true) + enum :ot_recipe, { Free: 0, Flex: 1 } custom_attribute(:wafer_size, default: '10TB', validator: true, required: true, selection: true) - custom_attribute(:read_length, default: 300, integer: true, validator: true, required: true, selection: true) end # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests. # This delegation has no real effect outside of the tests. - delegate :wafer_size, :read_length, to: :request_metadata - - class UltimaUG200RequestOptionsValidator < DelegateValidation::Validator - delegate :wafer_size, :read_length, :request_types, to: :target - - validate :validate_read_length_by_wafer_size - - def validate_read_length_by_wafer_size - return if wafer_size == '10TB' && read_length.to_i == 300 - - errors.add(:read_length, - 'The user can only select a Read Length of 300 with the 10TB wafer type for Ultima UG200 requests') - end - end - - def self.delegate_validator - UltimaUG200SequencingRequest::UltimaUG200RequestOptionsValidator - end + delegate :wafer_size, :ot_recipe, to: :request_metadata # Generates unique wafer ID, concatenation of batch_for_opentrons, # id_pool_lims, and request_order. diff --git a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml index ca846cb34c..e39f735c93 100644 --- a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml +++ b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml @@ -11,7 +11,6 @@ WaferSizeInformationTypeForUltimaUG200: pipeline_name: Ultima UG200 request_information_type_key: wafer_size -# TODO: does readlength need to be here -ReadLengthInformationTypeForUltimaUG200: - pipeline_name: Ultima UG200 - request_information_type_key: read_length +OTRecipeInformationTypeForUltima: + pipeline_name: Ultima + request_information_type_key: ot_recipe diff --git a/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml index cf37fb6f84..028e593253 100644 --- a/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml +++ b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml @@ -6,10 +6,3 @@ WaferSizeRequestedUltimaUG200Sequencing: - 5TB - 10TB - 20TB -ReadLengthRequestedUltimaUG200Sequencing: - request_type_key: ultima_ug200_sequencing - request_option: read_length - valid_options: - - 150 - - 200 - - 300 diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index 622aa97d85..ad180e5d76 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -132,7 +132,7 @@ { fragment_size_required_from: 150, fragment_size_required_to: 400, - read_length: 300, + ot_recipe: 'Free', wafer_size: '10TB' } end diff --git a/spec/models/ultima_ug200_sequencing_pipeline_spec.rb b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb index 1f792086cd..361bce75a5 100644 --- a/spec/models/ultima_ug200_sequencing_pipeline_spec.rb +++ b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb @@ -9,6 +9,35 @@ ) end + describe '#ot_recipe_consistent_for_batch?' do + it 'returns true when all requests have the same ot_recipe' do + batch = pipeline.batches.build + r1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + r2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + batch.requests << [r1, r2] + + expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be true + end + + it 'returns false when requests have different ot_recipes' do + batch = pipeline.batches.build + req1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + req2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Flex' }) + batch.requests << [req1, req2] + + expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be false + end + + it 'returns false when some requests are missing ot_recipe' do + batch = pipeline.batches.build + r1 = create(:sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + r2 = create(:sequencing_request, request_metadata_attributes: {}) # no ot_recipe + batch.requests << [r1, r2] + + expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be false + end + end + describe '#wafer_size_consistent_for_batch?' do it 'returns true when all requests have the same wafer_size' do batch = pipeline.batches.build From 72a640df53b92856116cf4408cfc85934a469cb4 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 27 Mar 2026 14:42:09 +0000 Subject: [PATCH 09/11] wip the submission template yml --- ...mplates.yml => 021_ultima_ug200_submission_templates.wip.yml} | 1 - 1 file changed, 1 deletion(-) rename config/default_records/submission_templates/{021_ultima_ug200_submission_templates.yml => 021_ultima_ug200_submission_templates.wip.yml} (89%) diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml similarity index 89% rename from config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml rename to config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml index cec0787649..e5b5363585 100644 --- a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml +++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml @@ -8,7 +8,6 @@ Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing: order_role: PCR Free product_line_name: Ultima product_catalogue_name: GenericNoPCR -# TODO: assumption that we need automated version for UG200, but is this the case? Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing Automated: submission_class_name: "LinearSubmission" related_records: From ac9574d42fb0d3b52e2e22feab308f667783d710 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 27 Mar 2026 17:01:50 +0000 Subject: [PATCH 10/11] for ug200 changed readlength for ot recipe --- app/validators/ultima_ug200_validator.rb | 10 +++++++++- spec/models/ultima_ug200_sequencing_request_spec.rb | 8 ++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/validators/ultima_ug200_validator.rb b/app/validators/ultima_ug200_validator.rb index ed2b32eb9d..1dc5c9889e 100644 --- a/app/validators/ultima_ug200_validator.rb +++ b/app/validators/ultima_ug200_validator.rb @@ -1,15 +1,17 @@ # frozen_string_literal: true class UltimaUG200Validator < UltimaValidator WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' + OT_RECIPE_CONSISTENT_MSG = 'OT Recipe must be the same for both requests.' # Used in _pipeline_limit.html to display custom validation warnings def self.validation_info - 'Wafer Size must be the same for both requests.' + 'Wafer Size and OT Recipe must be the same for both requests.' end # Validates that a batch contains the two requests. def validate(record) validate_exactly_two_requests(record) + requests_have_same_ot_recipe(record) requests_have_same_wafer_size(record) end @@ -20,4 +22,10 @@ def requests_have_same_wafer_size(record) record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) end + + def requests_have_same_ot_recipe(record) + return if record.pipeline.ot_recipe_consistent_for_batch?(record) + + record.errors.add(:base, OT_RECIPE_CONSISTENT_MSG) + end end diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb index b0b2696b46..a87bc38fd2 100644 --- a/spec/models/ultima_ug200_sequencing_request_spec.rb +++ b/spec/models/ultima_ug200_sequencing_request_spec.rb @@ -40,11 +40,11 @@ end end - context 'when read_length value is not assigned' do - it 'is invalid and displays required read length error message' do - request.request_metadata.read_length = nil + context 'when ot_recipe value is not assigned' do + it 'is invalid and displays required OT recipe error message' do + request.request_metadata.ot_recipe = nil request.validate - expect(request.errors[:'request_metadata.read_length']).to include("can't be blank") + expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") end end end From 573135fa76790fcebbf04ce93b2382d06ced4633 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 27 Mar 2026 17:05:19 +0000 Subject: [PATCH 11/11] fixed incorrect it text --- spec/models/ultima_sequencing_request_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/ultima_sequencing_request_spec.rb b/spec/models/ultima_sequencing_request_spec.rb index d152679b4e..3cbaa4864f 100644 --- a/spec/models/ultima_sequencing_request_spec.rb +++ b/spec/models/ultima_sequencing_request_spec.rb @@ -33,7 +33,7 @@ end context 'when ot_recipe value is not assigned' do - it 'is invalid and displays required percent phix requested error message' do + it 'is invalid and displays required OT recipe error message' do request.request_metadata.ot_recipe = nil request.validate expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank")