-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Y24-402 - Specify 'no. pools' rather than 'no. samples per pool' in submission #4467
base: migrating-to-number-of-pools-epic
Are you sure you want to change the base?
Changes from all commits
9a47613
ae562ca
f4ea40a
6054967
73b3530
273d79a
7b9d50e
7ced469
af5ef2a
0883d04
5f0691f
cb26850
04c49ae
8887732
5aa28cb
7aba186
8223742
ce715de
217a62f
89a86bd
f4649f8
7058a7c
6b25c81
0f93a2d
3ddc5b9
d47250d
c2c5e20
fa10bc9
bdd639d
eaacc48
933afad
b881745
682785c
dfce479
8c75444
ff64214
fea780f
3375001
d58bd71
e409e5e
e44c0ce
9ef7c10
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
# frozen_string_literal: true | ||
# rubocop:todo Metrics/ModuleLength | ||
module Submission::ValidationsByTemplateName | ||
# Template names | ||
SCRNA_CORE_CDNA_PREP_GEM_X_5P = 'Limber-Htp - scRNA Core cDNA Prep GEM-X 5p' | ||
|
@@ -7,9 +8,13 @@ | |
HEADER_TEMPLATE_NAME = 'template name' | ||
HEADER_STUDY_NAME = 'study name' | ||
HEADER_PROJECT_NAME = 'project name' | ||
HEADER_NUM_SAMPLES = 'scrna core number of samples per pool' | ||
HEADER_BARCODE = 'barcode' | ||
HEADER_PLATE_WELLS = 'plate well' | ||
HEADER_NUMBER_OF_POOLS = 'scrna core number of pools' | ||
HEADER_CELLS_PER_CHIP_WELL = 'scrna core cells per chip well' | ||
|
||
SAMPLES_PER_POOL = { max: 25, min: 5 }.freeze | ||
|
||
# Applies additional validations based on the submission template type. | ||
# | ||
# This method determines the submission template type from the CSV data and calls the appropriate | ||
|
@@ -32,11 +37,23 @@ | |
case submission_template_name | ||
# this validation is for the scRNA pipeline cDNA submission | ||
when SCRNA_CORE_CDNA_PREP_GEM_X_5P | ||
validate_consistent_column_value(HEADER_NUM_SAMPLES) | ||
validate_consistent_column_value(HEADER_NUMBER_OF_POOLS) | ||
validate_consistent_column_value(HEADER_CELLS_PER_CHIP_WELL) | ||
validate_samples_per_pool_for_labware | ||
end | ||
end | ||
|
||
def apply_number_of_samples_per_pool_validation | ||
# Creates groups of rows based on the study and project name (pool_number, study-project) combinations | ||
group_rows_by_study_and_project | ||
end | ||
|
||
def group_rows_by_study_and_project | ||
index_of_study_name = headers.index(HEADER_STUDY_NAME) | ||
index_of_project_name = headers.index(HEADER_PROJECT_NAME) | ||
csv_data_rows.group_by { |row| [row[index_of_study_name], row[index_of_project_name]] } | ||
end | ||
|
||
# Validates that the specified column is consistent for all rows with the same study and project name. | ||
# | ||
# This method groups the rows in the CSV data by the study name and project name, and checks if the specified column | ||
|
@@ -45,25 +62,219 @@ | |
# | ||
# @param column_header [String] The header of the column to validate. | ||
# @return [void] | ||
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize | ||
def validate_consistent_column_value(column_header) | ||
index_of_study_name = headers.index(HEADER_STUDY_NAME) | ||
index_of_project_name = headers.index(HEADER_PROJECT_NAME) | ||
index_of_column = headers.index(column_header) | ||
|
||
grouped_rows = csv_data_rows.group_by { |row| [row[index_of_study_name], row[index_of_project_name]] } | ||
grouped_rows = group_rows_by_study_and_project | ||
|
||
grouped_rows.each do |study_project, rows| | ||
unique_values = rows.pluck(index_of_column).uniq | ||
validate_unique_values(study_project, rows, index_of_column, column_header) | ||
end | ||
end | ||
|
||
# Validates the number of samples per pool for labware. | ||
# | ||
# This method checks if the headers for barcode and plate wells are present. | ||
# If they are, it groups the rows by study and project, and processes each group. | ||
# The processing involves determining if the labware is a plate or tube and | ||
# validating the number of samples per pool accordingly. | ||
# | ||
# @return [void] | ||
def validate_samples_per_pool_for_labware | ||
return if headers.index(HEADER_BARCODE).nil? && headers.index(HEADER_PLATE_WELLS).nil? | ||
|
||
grouped_rows = group_rows_by_study_and_project | ||
grouped_rows.each_value { |rows| process_rows(rows) } | ||
end | ||
|
||
private | ||
|
||
# Validates that the specified column has unique values for each study and project. | ||
# | ||
# This method checks if the specified column has unique values for each study and project. | ||
# If inconsistencies are found, an error is added to the errors collection. | ||
# | ||
# @param study_project [Array<String>] The study and project names. | ||
# @param rows [Array<Array<String>>] The rows of CSV data to process. | ||
# @param index_of_column [Integer] The index of the column to validate. | ||
# @param column_header [String] The header of the column to validate. | ||
# @return [void] | ||
def validate_unique_values(study_project, rows, index_of_column, column_header) | ||
unique_values = rows.pluck(index_of_column).uniq | ||
return unless unique_values.size > 1 | ||
|
||
errors.add( | ||
:spreadsheet, | ||
"Inconsistent values for column '#{column_header}' for Study name '#{study_project[0]}' and Project name " \ | ||
"'#{study_project[1]}', all rows for a specific study and project must have the same value" | ||
) | ||
end | ||
|
||
# Processes the rows to determine the type of labware and validate accordingly. | ||
# | ||
# This method extracts the barcodes and well locations from the rows and determines if the labware is a plate or tube. | ||
# It then calls the appropriate validation method based on the labware type. | ||
# | ||
# @param rows [Array<Array<String>>] The rows of CSV data to process. | ||
# @return [void] | ||
# rubocop:disable Metrics/MethodLength | ||
def process_rows(rows) | ||
barcodes = rows.pluck(headers.index(HEADER_BARCODE)) | ||
well_locations = rows.pluck(headers.index(HEADER_PLATE_WELLS)) | ||
|
||
if plate?(barcodes, well_locations) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if it's not a plate or tube? e.g. we have tube racks now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. plus add test for tube |
||
validate_for_plates(barcodes, well_locations, rows) | ||
elsif tube?(barcodes, well_locations) | ||
validate_for_tubes(barcodes, rows) | ||
else | ||
errors.add( | ||
:spreadsheet, | ||
'Invalid labware type. Please provide either a plate barcode with well locations or tube barcodes only' | ||
) | ||
end | ||
end | ||
# rubocop:enable Metrics/MethodLength | ||
|
||
# Validates the number of samples per pool for plates. | ||
# | ||
# This method finds the plate using the provided barcodes and retrieves the wells located at the specified well | ||
# locations. | ||
# It then calculates the total number of samples per study and project and the number of pools. | ||
# Finally, it validates the number of samples per pool. | ||
# | ||
# @param barcodes [Array<String>] The barcodes of the plates. | ||
# @param well_locations [Array<String>] The well locations on the plate. | ||
# @param rows [Array<Array<String>>] The rows of CSV data to process. | ||
# @return [void] | ||
# rubocop:disable Metrics/AbcSize | ||
def validate_for_plates(barcodes, well_locations, rows) | ||
plate = Plate.find_from_any_barcode(barcodes.uniq.first) | ||
return if plate.nil? | ||
|
||
wells = plate.wells.for_bulk_submission.located_at(well_locations) | ||
total_number_of_samples_per_study_project = wells.map(&:samples).flatten.count.to_i | ||
number_of_pools = rows.pluck(headers.index(HEADER_NUMBER_OF_POOLS)).uniq.first.to_i | ||
|
||
validate_samples_per_pool(rows, total_number_of_samples_per_study_project, number_of_pools) | ||
end | ||
# rubocop:enable Metrics/AbcSize | ||
|
||
# Validates the number of samples per pool for tubes. | ||
# | ||
# This method finds the tubes using the provided barcodes and calculates the total number of samples per study and | ||
# project. | ||
# It then retrieves the number of pools from the rows and validates the number of samples per pool. | ||
# | ||
# @param barcodes [Array<String>] The barcodes of the tubes. | ||
# @param rows [Array<Array<String>>] The rows of CSV data to process. | ||
# @return [void] | ||
def validate_for_tubes(barcodes, rows) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. refactor this? rubocop clearly not happy |
||
tubes = find_tubes(barcodes) | ||
total_number_of_samples_per_study_project = calculate_total_samples(tubes) | ||
number_of_pools = extract_number_of_pools(rows) | ||
|
||
validate_samples_per_pool(rows, total_number_of_samples_per_study_project, number_of_pools) | ||
end | ||
|
||
# Finds the tubes using the provided barcodes. | ||
# | ||
# This method retrieves the tubes that match the provided barcodes and raises an error if any barcodes are missing. | ||
# | ||
# @param barcodes [Array<String>] The barcodes of the tubes. | ||
# @return [Array<Receptacle>] The found tubes. | ||
def find_tubes(barcodes) | ||
Receptacle | ||
.on_a(Tube) | ||
.for_bulk_submission | ||
.with_barcode(barcodes) | ||
.tap do |found| | ||
missing = find_missing_barcodes(barcodes, found) | ||
raise ActiveRecord::RecordNotFound, "Could not find Tubes with barcodes #{missing.inspect}" if missing.present? | ||
end | ||
end | ||
|
||
# Finds the missing barcodes from the found tubes. | ||
# | ||
# This method checks which barcodes are not present in the found tubes. | ||
# | ||
# @param barcodes [Array<String>] The barcodes of the tubes. | ||
# @param found [Array<Receptacle>] The found tubes. | ||
# @return [Array<String>] The missing barcodes. | ||
def find_missing_barcodes(barcodes, found) | ||
barcodes.reject { |barcode| found.any? { |tube| tube.any_barcode_matching?(barcode) } } | ||
end | ||
|
||
# Calculates the total number of samples from the tubes. | ||
# | ||
# This method calculates the total number of samples by flattening the samples from the tubes and counting them. | ||
# | ||
# @param tubes [Array<Receptacle>] The tubes to calculate samples from. | ||
# @return [Integer] The total number of samples. | ||
def calculate_total_samples(tubes) | ||
tubes.map(&:samples).flatten.count.to_i | ||
end | ||
|
||
# Extracts the number of pools from the rows. | ||
# | ||
# This method retrieves the number of pools from the specified column in the rows. | ||
# | ||
# @param rows [Array<Array<String>>] The rows of CSV data to process. | ||
# @return [Integer] The number of pools. | ||
def extract_number_of_pools(rows) | ||
rows.pluck(headers.index(HEADER_NUMBER_OF_POOLS)).uniq.first.to_i | ||
end | ||
|
||
# Determines if the labware is a plate based on the presence of barcodes and well locations. | ||
# | ||
# This method checks if both barcodes and well locations are present to determine if the labware is a plate. | ||
# | ||
# @param barcodes [Array<String>] The barcodes of the labware. | ||
# @param well_locations [Array<String>] The well locations on the labware. | ||
# @return [Boolean] Returns true if both barcodes and well locations are present, indicating the labware is a plate. | ||
def plate?(barcodes, well_locations) | ||
barcodes.present? && well_locations.none?(&:nil?) | ||
end | ||
|
||
# Determines if the labware is a tube based on the presence of barcodes and absence of well locations. | ||
# | ||
# This method checks if barcodes are present and well locations are absent to determine if the labware is a tube. | ||
# | ||
# @param barcodes [Array<String>] The barcodes of the labware. | ||
# @param well_locations [Array<String>] The well locations on the labware. | ||
# @return [Boolean] Returns true if barcodes are present and well locations are absent, indicating the labware is a | ||
# tube. | ||
def tube?(barcodes, well_locations) | ||
barcodes.present? && well_locations.all?(&:nil?) | ||
end | ||
|
||
# Validates the number of samples per pool. | ||
# | ||
# This method calculates the number of samples per pool by dividing the total number of samples by the number of | ||
# pools. | ||
# It then iterates through each pool and checks if the number of samples per pool is within the allowed range. | ||
# If the number of samples per pool is less than the minimum or greater than the maximum allowed, an error is added. | ||
# | ||
# @param rows [Array<Array<String>>] The rows of CSV data to process. | ||
# @param total_samples [Integer] The total number of samples. | ||
# @param number_of_pools [Integer] The number of pools. | ||
# @return [void] | ||
# rubocop:disable Metrics/MethodLength | ||
def validate_samples_per_pool(rows, total_samples, number_of_pools) | ||
int_division = total_samples / number_of_pools | ||
remainder = total_samples % number_of_pools | ||
|
||
number_of_pools.times do |pool_number| | ||
samples_per_pool = int_division | ||
samples_per_pool += 1 if pool_number < remainder | ||
next unless samples_per_pool > SAMPLES_PER_POOL[:max] || samples_per_pool < SAMPLES_PER_POOL[:min] | ||
|
||
next unless unique_values.size > 1 | ||
errors.add( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add test |
||
:spreadsheet, | ||
"Inconsistent values for column '#{column_header}' for Study name '#{study_project[0]}' and Project name " \ | ||
"'#{study_project[1]}', " \ | ||
'all rows for a specific study and project must have the same value' | ||
"Number of samples per pool for Study name '#{rows.first[headers.index(HEADER_STUDY_NAME)]}' " \ | ||
"and Project name '#{rows.first[headers.index(HEADER_PROJECT_NAME)]}' " \ | ||
"is less than 5 or greater than 25 for pool number #{pool_number}" | ||
) | ||
end | ||
end | ||
# rubocop:enable Metrics/MethodLength,Metrics/AbcSize | ||
# rubocop:enable Metrics/MethodLength | ||
end | ||
# rubocop:enable Metrics/ModuleLength |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -269,26 +269,26 @@ requested_flowcell_type: | |
conditional_formattings: | ||
empty_cell: | ||
is_error: | ||
number_of_samples_per_pool: | ||
heading: scRNA Core Number of Samples per Pool | ||
attribute: :number_of_samples_per_pool | ||
number_of_pools: | ||
heading: scRNA Core Number of Pools | ||
attribute: :number_of_pools | ||
unlocked: true | ||
type: :integer | ||
validation: | ||
options: | ||
type: :whole | ||
operator: :between | ||
formula1: "5" | ||
formula2: "25" | ||
formula1: "1" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just checking 1 is the minimum, I thought it was 2 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you have 2-8 in the ranges below |
||
formula2: "8" | ||
allowBlank: true | ||
showInputMessage: true | ||
promptTitle: "Samples per Pool" | ||
prompt: "The requested number of samples per pool (between 5 and 25 inclusive)" | ||
promptTitle: "Number of pools" | ||
prompt: "The requested number pools (between 1 and 8 inclusive)" | ||
conditional_formattings: | ||
empty_cell: | ||
is_text: | ||
is_num_samples_per_pool_in_valid_range: | ||
is_num_samples_per_pool_outside_valid_range: | ||
is_num_of_pools_in_valid_range: | ||
is_num_of_pools_outside_valid_range: | ||
cells_per_chip_well: | ||
heading: scRNA Core Cells per Chip Well | ||
attribute: :cells_per_chip_well | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add method comments for new methods.
codepilot is pretty good at doing most of it if you highlight the method and ask it to 'add a method comment'