Skip to content

Commit

Permalink
Merge pull request #530 from sanger/develop
Browse files Browse the repository at this point in the history
develop to master for Duplex seq
  • Loading branch information
andrewsparkes authored Oct 21, 2020
2 parents d67c25e + e279eb4 commit ab236fa
Show file tree
Hide file tree
Showing 51 changed files with 2,917 additions and 101 deletions.
9 changes: 9 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,12 @@ Performance/StringInclude: # (new in 1.7)
Enabled: true
Performance/Sum: # (new in 1.8)
Enabled: true
Layout/BeginEndAlignment: # (new in 0.91)
Enabled: true
Lint/ConstantDefinitionInBlock: # (new in 0.91)
Enabled: true
Lint/IdentityComparison: # (new in 0.91)
Enabled: true
Lint/UselessTimes: # (new in 0.91)
Enabled: true

2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,5 @@ group :development do
gem 'travis'
# Ruby jard is a ruby debugger, buit on top of pry and byebug. Invoke it
# with jard
gem 'ruby_jard'
# gem 'ruby_jard'
end
2 changes: 1 addition & 1 deletion app/assets/javascripts/legacy_scripts_a.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
}
}

this['concentration-binned-view'] = {
this['binned-view'] = {
activate: function(){
plateElement.addClass('binning-colours')
},
Expand Down
2 changes: 1 addition & 1 deletion app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
@import "bootstrap";
@import "limber/screen";
@import "limber/plate";
@import "limber/concentration-binning";
@import "limber/binning";
@import "limber/spinner";
@import "limber/pipeline-graph";
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
* This css is for the concentration binned plate presenter.
* In /app/views/aliquots/_concentration_binned_aliquot.html.erb we define two aliquot divs, one specific to the binning
* This css is for the binned plate presenter.
* In /app/views/aliquots/_binned_aliquot.html.erb we define two aliquot divs, one specific to the binning
* tab, and the default pooling one for other tabs. This css shows and hides the relevant aliquot div.
*/
.plate-view {
.concentration-binning-id { display: none; }
.binning-id { display: none; }
&.binning-colours {
.concentration-binning-id { display: inline; }
.binning-id { display: inline; }
}

.aliquot.aliquot-pooled { display: inline-block; }
Expand Down
52 changes: 33 additions & 19 deletions app/controllers/exports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,50 +12,60 @@ class ExportsController < ApplicationController
WELL_QC_SAMPLE_INCLUDES = 'wells.qc_results,wells.aliquots.sample.sample_metadata'
WELL_SRC_ASSET_INCLUDES = 'wells.transfer_requests_as_target.source_asset'

CSVDetail = Struct.new(:csv, :plate_includes, :workflow) do
CSVDetail = Struct.new(:csv, :plate_includes, :workflow, :ancestor_purpose) do
end

CSV_DETAILS = {
'concentrations_ngul' =>
CSVDetail.new('concentrations_ngul', WELL_QC_INCLUDES, nil),
CSVDetail.new('concentrations_ngul', WELL_QC_INCLUDES, nil, nil),
'concentrations_nm' =>
CSVDetail.new('concentrations_nm', WELL_QC_INCLUDES, nil),
CSVDetail.new('concentrations_nm', WELL_QC_INCLUDES, nil, nil),
'duplex_seq_al_lib_concentrations_for_customer' =>
CSVDetail.new('duplex_seq_al_lib_concentrations_for_customer', WELL_QC_SAMPLE_INCLUDES, nil),
CSVDetail.new('duplex_seq_al_lib_concentrations_for_customer', WELL_QC_SAMPLE_INCLUDES, nil, nil),
'duplex_seq_pcr_xp_concentrations_for_custom_pooling' =>
CSVDetail.new('duplex_seq_pcr_xp_concentrations_for_custom_pooling', WELL_QC_INCLUDES, nil, 'LDS AL Lib Dil'),
'hamilton_aggregate_cherrypick' =>
CSVDetail.new('hamilton_aggregate_cherrypick', WELL_SRC_ASSET_INCLUDES, 'Cherry Pick'),
CSVDetail.new('hamilton_aggregate_cherrypick', WELL_SRC_ASSET_INCLUDES, 'Cherry Pick', nil),
'hamilton_cherrypick_to_sample_dilution' =>
CSVDetail.new('hamilton_fixed_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution'),
CSVDetail.new('hamilton_fixed_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution', nil),
'hamilton_gex_dil_to_gex_frag_2xp' =>
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X Post Repair Double SPRI'),
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X Post Repair Double SPRI', nil),
'hamilton_gex_frag_2xp_to_gex_ligxp' =>
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X Post Ligation Single SPRI'),
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X Post Ligation Single SPRI', nil),
'hamilton_cherrypick_to_5p_gex_dilution' =>
CSVDetail.new('hamilton_variable_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution'),
CSVDetail.new('hamilton_variable_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution', nil),
'hamilton_cherrypick_to_bcr_dilution1' =>
CSVDetail.new('hamilton_fixed_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution'),
CSVDetail.new('hamilton_fixed_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution', nil),
'hamilton_lbc_bcr_dil_1_to_lbc_bcr_enrich1_1xspri' =>
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Target Enrichment 1 Single SPRI'),
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Target Enrichment 1 Single SPRI', nil),
'hamilton_lbc_bcr_enrich1_1xspri_to_lbc_bcr_enrich2_2xspri' =>
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Target Enrichment 2 Double SPRI'),
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Target Enrichment 2 Double SPRI', nil),
'hamilton_lbc_bcr_enrich2_2xspri_to_lbc_bcr_dil_2' =>
CSVDetail.new('hamilton_variable_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution'),
CSVDetail.new('hamilton_variable_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution', nil),
'hamilton_lbc_bcr_dil_2_to_lbc_bcr_post_lig_1xspri' =>
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Ligation SPRI'),
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Ligation SPRI', nil),
'hamilton_cherrypick_to_tcr_dilution1' =>
CSVDetail.new('hamilton_fixed_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution'),
CSVDetail.new('hamilton_fixed_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution', nil),
'hamilton_lbc_tcr_dil_1_to_lbc_tcr_enrich1_1xspri' =>
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Target Enrichment 1 Single SPRI'),
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Target Enrichment 1 Single SPRI', nil),
'hamilton_lbc_tcr_enrich1_1xspri_to_lbc_tcr_enrich2_2xspri' =>
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Target Enrichment 2 Double SPRI'),
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Target Enrichment 2 Double SPRI', nil),
'hamilton_lbc_tcr_enrich2_2xspri_to_lbc_tcr_dil_2' =>
CSVDetail.new('hamilton_variable_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution'),
CSVDetail.new('hamilton_variable_volume_dilutions', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution', nil),
'hamilton_lbc_tcr_dil_2_to_lbc_tcr_post_lig_1xspri' =>
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Ligation SPRI')
CSVDetail.new('hamilton_plate_stamp', WELL_SRC_ASSET_INCLUDES, '10X VDJ Post Ligation SPRI', nil),
'hamilton_lds_al_lib_to_qc1' =>
CSVDetail.new('hamilton_plate_stamp_to_qc', WELL_SRC_ASSET_INCLUDES, 'Cherry Pick', nil),
'hamilton_lds_al_lib_to_lds_al_lib_dil' =>
CSVDetail.new('hamilton_variable_volume_dilutions_with_well_diluents', WELL_SRC_ASSET_INCLUDES, 'Sample Dilution', nil)
}.freeze

def show
@workflow = csv_details.workflow
if csv_details.ancestor_purpose.present?
ancestor_result = @plate.ancestors.where(purpose_name: csv_details.ancestor_purpose).first
locate_ancestor(ancestor_result.id) if ancestor_result.present?
end
render csv_details.csv
end

Expand All @@ -79,6 +89,10 @@ def locate_labware
barcode: params[:limber_plate_id])
end

def locate_ancestor(plate_id)
@ancestor_plate = Sequencescape::Api::V2.plate_with_custom_includes(include_parameters, id: plate_id)
end

def include_parameters
csv_details.plate_includes || 'wells'
end
Expand Down
15 changes: 15 additions & 0 deletions app/models/labware_creators/custom_pooled_tubes/csv_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class CustomPooledTubes::CsvFile

def initialize(file)
@data = CSV.parse(file.read)
remove_bom
@parsed = true
rescue StandardError => e
@data = []
Expand Down Expand Up @@ -53,6 +54,20 @@ def header_row

private

# remove byte order marker if present
def remove_bom
return unless @data.present? && @data[0][0].present?

# byte order marker will appear at beginning of in first string in @data array
s = @data[0][0]

# NB. had to make byte order marker string mutable here otherwise get frozen string error
bom = +"\xEF\xBB\xBF"
s_mod = s.gsub!(bom.force_encoding(Encoding::BINARY), '')

@data[0][0] = s_mod unless s_mod.nil?
end

def transfers
@transfers ||= @data[1..].each_with_index.map do |row_data, index|
Row.new(header_row, index + 2, row_data)
Expand Down
149 changes: 149 additions & 0 deletions app/models/labware_creators/pcr_cycles_binned_plate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# frozen_string_literal: true

module LabwareCreators
# Handles the generation of a plate with wells binned according to the number of
# PCR cycles that has been determined by the customer.
# Uploads a file supplied by the customer that has a row per each well and
# includes Sample Volume, Diluent Volume, PCR Cycles, Sub-Pool and Coverage columns.
# Uses the PCR Cycles column to determine the binning arrangement of the wells,
# and the Sample Volume and Diluent Volume columns in the well transfers.
# Sub-Pool and Coverage need to be stored for a later step downstream in the pipeline,
# at the point where custom pooling is performed.
# Wells in the bins are applied to the destination by column order.
# If there is enough space on the destination plate each new bin will start in a new
# column. Otherwise bins will run consecutively without gaps.
#
#
# Source Plate Dest Plate
# +--+--+--~ +--+--+--~
# |A1| pcr_cycles = 12 (bin 2) |B1|A1|C1|
# +--+--+--~ +--+--+--~
# |B1| pcr_cycles = 15 (bin 1) |D1|E1| |
# +--+--+--~ + +--+--+--~
# |C1| pcr_cycles = 10 (bin 3) | |G1| |
# +--+--+--~ +--+--+--~
# |D1| pcr_cycles = 15 (bin 1) | | | |
# +--+--+--~ +--+--+--~
# |E1| pcr_cycles = 12 (bin 2) | | | |
# +--+--+--~ +--+--+--~
# |G1| pcr_cycles = 12 (bin 2) | | | |
class PcrCyclesBinnedPlate < StampedPlate
include LabwareCreators::CustomPage

MISSING_WELL_DETAIL = 'is missing a row for well %s, all wells with content must have a row in the uploaded file.'
PENDING_WELL = 'contains at least one pending well %s, the plate and all wells in it should be passed before creating the child plate.'

self.page = 'pcr_cycles_binned_plate'
self.attributes += [:file]

attr_accessor :file

# delegate method to return well values to csv file handler class
delegate :well_details, to: :csv_file

validates :file, presence: true
validates_nested :csv_file, if: :file
validate :wells_have_required_information?

PARENT_PLATE_INCLUDES =
'wells.aliquots,wells.qc_results,wells.requests_as_source.request_type,wells.aliquots.request.request_type'

CHILD_PLATE_INCLUDES =
'wells.aliquots'

def parent
@parent ||= Sequencescape::Api::V2.plate_with_custom_includes(PARENT_PLATE_INCLUDES, uuid: parent_uuid)
end

def parent_v1
@parent_v1 ||= api.plate.find(parent_uuid)
end

# Configurations from the plate purpose.
def csv_file_upload_config
@csv_file_upload_config ||= purpose_config.fetch(:csv_file_upload)
end

def dilutions_config
@dilutions_config ||= purpose_config.fetch(:dilutions)
end

def save
# NB. need the && true!!
super && upload_file && true
end

def after_transfer!
# called as part of the 'super' call in the 'save' method
# retrieve child plate through v2 api, using uuid got through v1 api
child_v2 = Sequencescape::Api::V2.plate_with_custom_includes(CHILD_PLATE_INCLUDES, uuid: child.uuid)

# update fields on each well with various metadata
fields_to_update = %w[diluent_volume pcr_cycles submit_for_sequencing sub_pool coverage]

child_wells_by_location = child_v2.wells.index_by(&:location)

well_details.each do |parent_location, details|
child_position = transfer_hash[parent_location]['dest_locn']
child_well = child_wells_by_location[child_position]

update_well_with_metadata(child_well, details, fields_to_update)
end
end

def update_well_with_metadata(well, metadata, fields_to_update)
options = fields_to_update.each_with_object({}) { |field, obj| obj[field] = metadata[field] }
well.update(options)
end

def wells_have_required_information?
filtered_wells.each do |well|
next if well.aliquots.empty?

errors.add(:csv_file, format(MISSING_WELL_DETAIL, well.location)) unless well_details.include? well.location
errors.add(:csv_file, format(PENDING_WELL, well.location)) if well.pending?
end
end

def dilutions_calculator
@dilutions_calculator ||= Utility::PcrCyclesBinningCalculator.new(well_details)
end

private

# Returns the parent wells selected to be taken forward.
def filtered_wells
well_filter.filtered.each_with_object([]) do |well_filter_details, wells|
wells << well_filter_details[0]
end
end

#
# Upload the csv file onto the plate via api v1
#
def upload_file
parent_v1.qc_files.create_from_file!(file, 'duplex_seq_customer_file.csv')
end

# Create class that will parse and validate the uploaded file
def csv_file
@csv_file ||= CsvFile.new(file, csv_file_upload_config, parent.human_barcode)
end

# Override this method in sub-class if required.
def request_hash(source_well, child_plate, additional_parameters)
{
'source_asset' => source_well.uuid,
'target_asset' => child_plate.wells.detect do |child_well|
child_well.location == transfer_hash[source_well.location]['dest_locn']
end&.uuid,
'volume' => transfer_hash[source_well.location]['volume'].to_s
}.merge(additional_parameters)
end

# Uses the calculator to generate the hash of transfers to be performed on the parent plate
def transfer_hash
@transfer_hash ||= dilutions_calculator.compute_well_transfers(parent)
end
end
end
Loading

0 comments on commit ab236fa

Please sign in to comment.