Skip to content
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

scRNA - Pooling calculation stories Epic #2069

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c280f87
refactored shared constants to move them into a central config file
andrewsparkes Nov 5, 2024
ac4e199
renamed constants to make more specific, and added usage of study ove…
andrewsparkes Nov 6, 2024
0fd92ec
added check for when no polymetadata exists
andrewsparkes Nov 6, 2024
8e2c12a
removed references to override from study value and added check for n…
andrewsparkes Nov 6, 2024
bb3de69
fix key name
andrewsparkes Nov 11, 2024
80a10e6
removed references to unneeded number of cells per chip well constant…
andrewsparkes Nov 12, 2024
f89a1c6
reduced number of decimal places for wastage factor
andrewsparkes Nov 12, 2024
2f90573
Update app/views/exports/hamilton_gem_x_5p_chip_loading.csv.erb
andrewsparkes Nov 13, 2024
1404745
Update config/initializers/scrna_config.rb
andrewsparkes Nov 13, 2024
eac6a5e
Update config/initializers/scrna_config.rb
andrewsparkes Nov 13, 2024
729c220
Update config/initializers/scrna_config.rb
andrewsparkes Nov 13, 2024
5090dab
Update config/initializers/scrna_config.rb
andrewsparkes Nov 13, 2024
762b8be
Update config/initializers/scrna_config.rb
andrewsparkes Nov 13, 2024
54a81e8
Update config/initializers/scrna_config.rb
andrewsparkes Nov 13, 2024
565facb
linted
andrewsparkes Nov 13, 2024
d9e10de
added comments to describe what is happening in calculations
andrewsparkes Nov 13, 2024
1166fc6
fixed tests due to changed wastage factor
andrewsparkes Nov 13, 2024
2cb2ef4
Merge branch 'y24-375-file-downloads-improvements-for-mvp' into scrna…
andrewsparkes Nov 15, 2024
2818c15
added method to fetch number of cells per chip well from the request …
andrewsparkes Nov 18, 2024
c96d434
added additional constants for allowance calculation
andrewsparkes Nov 20, 2024
2d17ea3
added methods to donor pooling calculator for allowance calculations
andrewsparkes Nov 20, 2024
bff091d
added check for full allowance
andrewsparkes Nov 20, 2024
59c7b08
removed unneeded parameter in tests
andrewsparkes Nov 21, 2024
57fcd22
renamed test class instance variable
andrewsparkes Nov 21, 2024
ca0f7e4
Merge pull request #2078 from sanger/Y24-384-scrna-adjust-pooling-for…
andrewsparkes Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions app/models/concerns/labware_creators/donor_pooling_calculator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,149 @@
end
groups
end

# This method checks the pool for full allowance and adjusts the number of
# cells per chip well value if needed.
# It then stores the number of cells per chip well as metadata on the destination well.
#
# @param pool [Array<SourceWell>] an array of source wells from the v2 API
# @param dest_plate [Object] the destination plate
# @param dest_well_location [String] the location of the destination well
def check_pool_for_full_allowance(pool, dest_plate, dest_well_location)
# count sum of samples in all source wells in the pool (typically will be one sample per source well)
# for each source well, number of samples = well.aliquots.size
count_of_samples_in_pool = pool.sum { |source_well| source_well.aliquots.size }

# fetch number of cells per chip well from the request metadata of the first aliquot in the first source well
number_of_cells_per_chip_well = number_of_cells_per_chip_well_from_request(pool)

# only consider adjusting the number of cells per chip well if the count of samples in the pool is between 5 and 8
if count_of_samples_in_pool >= 5 && count_of_samples_in_pool <= 8
# check and adjust number of cells per chip well if needed
number_of_cells_per_chip_well =
adjust_number_of_cells_per_chip_well(count_of_samples_in_pool, number_of_cells_per_chip_well)
end

# store number of cells per chip well in destination pool well poly metadata
dest_well = dest_plate.well_at_location(dest_well_location)

create_new_well_metadata(
Rails.application.config.scrna_config[:number_of_cells_per_chip_well_key],
number_of_cells_per_chip_well,
dest_well
)
end

private

# This method retrieves the number of cells per chip well from the request metadata
# of the first aliquot in the first source well of the provided pool.
# If the cells per chip well value is not present, it raises a StandardError.
#
# @param pool [Array<SourceWell>] an array of source wells from the v2 API
# @return [Integer] the number of cells per chip well
# @raise [StandardError] if the cells per chip well value is not found in the request metadata
def number_of_cells_per_chip_well_from_request(pool)
# pool is an array of v2 api source wells, fetch the first well from the pool
source_well = pool.first

# fetch request metadata for number of cells per chip well from first aliquot in the source well
# (it should be the same value for all the aliquots in all the source wells in the pool)
cells_per_chip_well = source_well&.aliquots&.first&.request&.request_metadata&.cells_per_chip_well

if cells_per_chip_well.blank?
raise StandardError,
"No request found for source well at #{source_well.location}, cannot fetch cells per chip " \
'well metadata for full allowance calculations'
end

cells_per_chip_well
end

# This method adjusts the number of cells per chip well based on the count of samples in the pool.
# If the final suspension volume is greater than or equal to the full allowance, the existing value is retained.
# Otherwise, the number of cells per chip well is adjusted according to the full allowance table.
#
# @param count_of_samples_in_pool [Integer] the number of samples in the pool
# @param number_of_cells_per_chip_well [Integer] the initial number of cells per chip well
# @return [Integer] the adjusted number of cells per chip well
def adjust_number_of_cells_per_chip_well(count_of_samples_in_pool, number_of_cells_per_chip_well)
# calculate total cells in 300ul for the pool
total_cells_in_300ul = calculate_total_cells_in_300ul(count_of_samples_in_pool)

# calculate final suspension volume
final_suspension_volume =
total_cells_in_300ul / Rails.application.config.scrna_config[:desired_chip_loading_concentration]

# calculate chip loading volume
chip_loading_volume = calculate_chip_loading_volume(number_of_cells_per_chip_well)

# calculate full allowance
full_allowance = calculate_full_allowance(chip_loading_volume)

# do not adjust existing value if we have enough volume
return number_of_cells_per_chip_well if final_suspension_volume >= full_allowance

# we need to adjust the number of cells per chip well according to the number of samples
Rails.application.config.scrna_config[:full_allowance_table][count_of_samples_in_pool]
end

# This method calculates the total cells in 300ul for a given pool of samples.
# It uses the configuration values from the scrna_config to determine the required
# number of cells per sample in the pool and the wastage factor.
#
# @param count_of_samples_in_pool [Integer] the number of samples in the pool
# @return [Float] the total cells in 300ul
def calculate_total_cells_in_300ul(count_of_samples_in_pool)
scrna_config = Rails.application.config.scrna_config

(count_of_samples_in_pool * scrna_config[:required_number_of_cells_per_sample_in_pool]) *
scrna_config[:wastage_factor]
end

# This method calculates the chip loading volume for a given pool of samples.
# It retrieves the number of cells per chip well from the request metadata
# and uses the desired chip loading concentration from the scrna_config.
#
# @param num_cells_per_chip_well [Integer] the number of cells per chip well from the request metadata
# @return [Float] the chip loading volume
def calculate_chip_loading_volume(num_cells_per_chip_well)
chip_loading_conc = Rails.application.config.scrna_config[:desired_chip_loading_concentration]

num_cells_per_chip_well / chip_loading_conc
end

# This method calculates the full allowance volume for a given chip loading volume.
# It uses configuration values from the scrna_config for the desired
# number of runs, the volume taken for cell counting, and the wastage volume.
#
# @param chip_loading_volume [Float] the chip loading volume
# @return [Float] the full allowance volume
def calculate_full_allowance(chip_loading_volume)
scrna_config = Rails.application.config.scrna_config

(chip_loading_volume * scrna_config[:desired_number_of_runs]) +
(scrna_config[:desired_number_of_runs] * scrna_config[:volume_taken_for_cell_counting]) +
scrna_config[:wastage_volume]
end

# This method creates a new well metadata entry for a given destination well.
# It initializes a new PolyMetadatum object with the provided metadata key and value,
# associates it with the destination well, and attempts to save it.
# If the save operation fails, it raises a StandardError with a descriptive message.
#
# @param metadata_key [String] the key for the metadata
# @param metadata_value [String] the value for the metadata
# @param dest_well [Object] the destination well to associate the metadata with
# @raise [StandardError] if the metadata entry fails to save
def create_new_well_metadata(metadata_key, metadata_value, dest_well)
pm_v2 = Sequencescape::Api::V2::PolyMetadatum.new(key: metadata_key, value: metadata_value)
pm_v2.relationships.metadatable = dest_well

return if pm_v2.save

raise StandardError,

Check warning on line 303 in app/models/concerns/labware_creators/donor_pooling_calculator.rb

View check run for this annotation

Codecov / codecov/patch

app/models/concerns/labware_creators/donor_pooling_calculator.rb#L303

Added line #L303 was not covered by tests
"New metadata for request (key: #{metadata_key}, value: #{metadata_value}) " \
"did not save on destination well at location #{dest_well.location}"
end
end
35 changes: 25 additions & 10 deletions app/models/labware_creators/donor_pooling_plate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -162,23 +162,20 @@ def calculated_number_of_pools
# @param dest_uuid [String] The UUID of the destination plate.
# @return [Boolean] Returns true if no exception is raised.
def transfer_material_from_parent!(dest_uuid)
dest_plate = Sequencescape::Api::V2::Plate.find_by(uuid: dest_uuid)
api.transfer_request_collection.create!(
user: user_uuid,
transfer_requests: transfer_request_attributes(dest_plate)
)
@dest_plate = Sequencescape::Api::V2::Plate.find_by(uuid: dest_uuid)
api.transfer_request_collection.create!(user: user_uuid, transfer_requests: transfer_request_attributes)
determine_if_pools_have_full_allowance
true
end

# Generates the attributes for transfer requests from the source wells to the
# destination plate.
#
# @param dest_plate [Sequencescape::Api::V2::Plate] The destination plate.
# @return [Array<Hash>] An array of hashes, each representing the attributes
# for a transfer request.
def transfer_request_attributes(dest_plate)
def transfer_request_attributes
well_filter.filtered.filter_map do |source_well, additional_parameters|
request_hash(source_well, dest_plate, additional_parameters)
request_hash(source_well, additional_parameters)
end
end

Expand All @@ -191,11 +188,11 @@ def transfer_request_attributes(dest_plate)
# @param dest_plate [Sequencescape::Api::V2::Plate] The destination plate.
# @param additional_parameters [Hash] Additional parameters to include.
# @return [Hash] A hash representing a transfer request.
def request_hash(source_well, dest_plate, additional_parameters)
def request_hash(source_well, additional_parameters)
dest_location = transfer_hash[source_well][:dest_locn]
{
'source_asset' => source_well.uuid,
'target_asset' => dest_plate.well_at_location(dest_location)&.uuid,
'target_asset' => @dest_plate.well_at_location(dest_location)&.uuid,
:aliquot_attributes => {
'tag_depth' => tag_depth_hash[source_well]
}
Expand Down Expand Up @@ -240,5 +237,23 @@ def build_pools
groups = split_groups_by_unique_donor_ids(groups)
distribute_groups_across_pools(groups, calculated_number_of_pools)
end

# This method determines if the pools have full allowance.
# It iterates over each pool, retrieves the destination well location from the transfer hash,
# and checks each pool for full allowance by calling the check_pool_for_full_allowance method
# in the donor pooling calculator class.
# That method then writes the number of cells per chip well to the poly_metadata of the pool wells.
#
# @return [void]
def determine_if_pools_have_full_allowance
# a pool is array of v2 wells
pools.each do |pool|
# destination location is the same for all wells in the pool, so fetch from first source wells
dest_well_location = transfer_hash[pool.first][:dest_locn]

# check this pool for full allowance
check_pool_for_full_allowance(pool, @dest_plate, dest_well_location)
end
end
end
end
32 changes: 8 additions & 24 deletions app/models/validators/required_number_of_cells_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# for the user to configure the option after seeing the warning and download
# the driver file again to use the correct value.
class RequiredNumberOfCellsValidator < ActiveModel::Validator
STUDIES_WITHOUT_REQUIRED_NUMBER_OF_CELLS =
STUDIES_WITHOUT_REQUIRED_NUMBER_OF_CELLS_PER_SAMPLE_PER_POOL =
'The required number of cells is not configured for all studies ' \
'going into pooling on this plate. If not provided, the default ' \
'value %s will be used for the samples of the following studies: ' \
Expand All @@ -33,16 +33,16 @@
def validate(presenter)
return unless presenter.labware.state == 'pending'

studies = source_studies_without_config(presenter, study_required_number_of_cells_key(presenter))
study_required_number_of_cells_per_sample_in_pool_key =
Rails.application.config.scrna_config[:study_required_number_of_cells_per_sample_in_pool_key]
default_value = Rails.application.config.scrna_config[:required_number_of_cells_per_sample_in_pool]

studies = source_studies_without_config(presenter, study_required_number_of_cells_per_sample_in_pool_key)
return if studies.empty?

formatted_string =
format(
STUDIES_WITHOUT_REQUIRED_NUMBER_OF_CELLS,
default_required_number_of_cells(presenter),
studies.join(', ')
)
presenter.errors.add(:required_number_of_cells, formatted_string)
format(STUDIES_WITHOUT_REQUIRED_NUMBER_OF_CELLS_PER_SAMPLE_PER_POOL, default_value, studies.join(', '))
presenter.errors.add(:required_number_of_cells_per_sample_in_pool, formatted_string)

Check warning on line 45 in app/models/validators/required_number_of_cells_validator.rb

View check run for this annotation

Codecov / codecov/patch

app/models/validators/required_number_of_cells_validator.rb#L44-L45

Added lines #L44 - L45 were not covered by tests
end

private
Expand All @@ -65,21 +65,5 @@
.map(&:name)
.uniq
end

# Retrieves the default required number of cells from the purpose config.
#
# @return [Integer] the default required number of cells
# :reek:UtilityFunction { public_methods_only: true }
def default_required_number_of_cells(presenter)
Settings.purposes[presenter.labware.purpose.uuid][:presenter_class][:args][:default_required_number_of_cells]
end

# Retrieves the poly_metadatum key for the study-specific required number of cells.
#
# @return [String] the poly_metadatum key
# :reek:UtilityFunction { public_methods_only: true }
def study_required_number_of_cells_key(presenter)
Settings.purposes[presenter.labware.purpose.uuid][:presenter_class][:args][:study_required_number_of_cells_key]
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ module HasPolyMetadata
# @param key [String] the key of the PolyMetadatum to find
# @return [PolyMetadatum, nil] the found PolyMetadatum object, or nil if no match is found
def poly_metadatum_by_key(key)
# if no poly_metadata exist for this model instance, or no key value is passed, return nil
return nil if poly_metadata.blank? || key.blank?

poly_metadata.find { |pm| pm.key == key }
end
end
Expand Down
1 change: 1 addition & 0 deletions app/sequencescape/sequencescape/api/v2/well.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class Sequencescape::Api::V2::Well < Sequencescape::Api::V2::Base # rubocop:todo Style/Documentation
include Sequencescape::Api::V2::Shared::HasRequests
include Sequencescape::Api::V2::Shared::HasPolyMetadata
has_many :qc_results
has_many :requests_as_source, class_name: 'Sequencescape::Api::V2::Request'
has_many :requests_as_target, class_name: 'Sequencescape::Api::V2::Request'
Expand Down
52 changes: 38 additions & 14 deletions app/views/exports/hamilton_gem_x_5p_chip_loading.csv.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<%# Driver file for Hamilton robot to transfer from LRC PBMC Pools (or Input) to LRC GEM x 5P chip %>
<%= CSV.generate_line [
'Workflow',
'Workflow',
@workflow
],
row_sep: ''
],
row_sep: ''
%>
<%= CSV.generate_line [], row_sep: "" %>
<%= CSV.generate_line [
Expand All @@ -19,14 +19,16 @@
row_sep: ""
%>
<%
# Configuration specific to this export
# Constants from config/initializers/scrna_config.rb
scrna_config = Rails.application.config.scrna_config

required_number_of_cells = 30000
wastage_factor = 0.95238 # accounting for wastage of material when transferring between labware
chip_loading_concentration = 2400
desired_number_of_cells_per_chip_well = 90000
desired_sample_volume = 37.5 # microlitres
volume_taken_for_cell_counting = 10.0 # microlitres
# fetch constants from scrna_config for use in calculations on source wells below
required_number_of_cells_per_sample_in_pool = scrna_config[:required_number_of_cells_per_sample_in_pool]
wastage_factor = scrna_config[:wastage_factor]
desired_chip_loading_concentration = scrna_config[:desired_chip_loading_concentration]
desired_chip_loading_volume = scrna_config[:desired_chip_loading_volume]
volume_taken_for_cell_counting = scrna_config[:volume_taken_for_cell_counting]
number_of_cells_per_chip_well_key = scrna_config[:number_of_cells_per_chip_well_key]

# Destination wells are mapped to numbers: A1 -> 17, A2 -> 18, ..., A8 -> 24
mapping = (1..8).each_with_object({}) { |i, hash| hash["A#{i}"] = (16 + i).to_s }
Expand All @@ -35,11 +37,33 @@
ancestral_plate_wells = @ancestor_plate.wells.index_by(&:location)
rows_array = []
each_source_metadata_for_plate(@plate) do |src_barcode, src_location, dest_well|
number_of_samples = ancestral_plate_wells[src_location].aliquots.length
resuspension_volume = (number_of_samples * required_number_of_cells * wastage_factor).to_f / chip_loading_concentration
src_well = ancestral_plate_wells[src_location]
number_of_samples = src_well.aliquots.length

# Use the source well's number_of_cells_per_chip_well if available, otherwise error.
# Value should have been calculated and stored on the pool well polymetadata.
well_poly_metadatum = src_well.poly_metadatum_by_key(number_of_cells_per_chip_well_key)

# raise error if polymetadata is missing
raise "Missing poly metadata for number of cells per chip well for #{src_barcode} #{src_location}, " \
'cannot generate driver file' unless well_poly_metadatum

# extract value from the polymetadata
number_of_cells_per_chip_well = well_poly_metadatum&.value.to_f.nonzero?

# calculations

# calculate volume we believe is remaining in the source well
resuspension_volume = (number_of_samples * required_number_of_cells_per_sample_in_pool * wastage_factor).to_f / desired_chip_loading_concentration
source_well_volume = resuspension_volume - volume_taken_for_cell_counting
sample_volume = desired_number_of_cells_per_chip_well.to_f/chip_loading_concentration
pbs_volume = desired_sample_volume - sample_volume

# calculate volume of the source sample well to take to load onto the chip
sample_volume = number_of_cells_per_chip_well/desired_chip_loading_concentration

# calculate pbs buffer volume with which to top up the chip loading volume
pbs_volume = desired_chip_loading_volume - sample_volume

# add row to driver file output csv
rows_array << [
src_barcode,
src_location,
Expand Down
Loading
Loading