#!/usr/bin/env perl
=begin -------------------------------------------------------------------------
Generates spreadsheet on Gallium trial cases. Authorised cases only. Requires
ICDO3 or one of following diagnoses: 'no evidence of lymphoma', 'unsuitable
specimen', 'inadequate sample'. Anything else picked up by gallium_pending.pl
# TODO:
how to get diagnosis discrepancy
how to get localisation discrepancy
sections (but not DNA or RNA ones ??)
=cut ---------------------------------------------------------------------------
use strict;
use warnings;
use lib '/home/raj/perl5/lib/perl5';
use FindBin qw($Bin); # warn $Bin;
use lib "$Bin/../../../lib";
use Data::Dumper;
use LIMS::Local::Utils;
use DateTime::Format::MySQL;
use LIMS::Local::GalliumData;
use LIMS::Local::ScriptHelpers;
use Spreadsheet::WriteExcel::Simple;
my $tools = LIMS::Local::ScriptHelpers->new();
my $today = $tools->time_now->ymd(''); # yyyymmdd
################################################################################
my $SUPPRESS_EMAIL = 0;
my $JUST_TESTING = 0; # save locally, email to raj only (if ! $SUPPRESS_EMAIL)
my $subject = sprintf 'BO21223_%s', $today;
my $filename = $subject . '.xls'; # warn $filename;
my @recipients = qw(
hmds.lth@nhs.net
archana.ambily
marvin.marcellana
); # irfan.shaikh ??
use constant DATE_FORMAT => '%d-%b-%Y'; # required format
################################################################################
$tools->test_only($JUST_TESTING);
# xl file headers:
my $col_headers = _col_headers(); # warn Dumper $col_headers; exit;
push @recipients, 'raj' if $JUST_TESTING; # will be sole recipient
# get tools from LIMS::Local::ScriptHelpers:
my $sql_lib = $tools->sql_lib();
my $config = $tools->config();
my $dbix = $tools->dbix();
my $cfg_file = "$Bin/../lib/settings.cfg";
my $settings = Config::Auto::parse($cfg_file); # warn Dumper $settings; exit;
# permitted non-ICDO3 diagnoses:
my $non_icdo3 = $settings->{non_icdo3}; # warn Dumper $non_icdo3;
# ensure case-sensitivity of col names preserved (eg bcl2 & BCL2):
$dbix->lc_columns = 0;
my $nearest = sub { LIMS::Local::Utils::nearest_value(@_) };
my $round = sub { LIMS::Local::Utils::round_value(@_) };
my $requests = do {
my $sql = $sql_lib->retr('gallium_data'); # warn $sql;
$dbix->query($sql, @$non_icdo3); # permitted non-ICDO3 diagnoses
};
my @data = my @skipped = ();
ROW: while ( my $vars = $requests->hash ) { # warn Dumper $vars; next;
my $request_id = $vars->{request_id}; # next unless $request_id == 175235;
# need to check "optional" comment field exists or will get fatal error:
unless ($vars->{comment}) {
push @skipped, $request_id;
next ROW;
}
my $gallium = LIMS::Local::GalliumData->new(data => $vars);
my @row = (); # reset
# 1) Lab ID (always 21223LEEDS)
push @row, '21223LEEDS';
# 2) QLABS accession number:
push @row, $vars->{accession_number};
# 3) lab origin (always Leeds):
push @row, 'LEEDS';
# 4) registration date (DD-MON-YYYY):
push @row, _date_transform($vars->{registered});
# 5) Leeds lab number:
push @row, $vars->{hmds_ref};
# 6) Kiel lab number:
push @row, 'NOT APPLICABLE';
{ # 7) Biopsy type:
my $result = $gallium->get_biopsy_type || 'OTHER';
push @row, uc $result; # warn $result;
}
{ # 8, 9, 10) slides & blocks:
my $h = $gallium->get_external_reference; # warn Dumper $h;
{ # either, both or neither:
if ( $h->{slides} && $h->{blocks} ) {
push @row, 'BOTH';
}
elsif ($h->{slides}) {
push @row, 'SLIDES';
}
elsif ($h->{blocks}) {
push @row, 'BLOCKS';
}
else {
push @row, 'NONE';
}
}
push @row, $h->{slides} || 0; # number of slides
push @row, $h->{blocks} || 0; # number of blocks
}
# 11) specimen quality:
push @row, uc $vars->{specimen_quality};
# 12) date of diagnosis (= authorised; DD-MON-YYYY):
push @row, _date_transform($vars->{authorised});
{ # 13, 14, 15) diagnosis, lymphoma grade, reason for no diagnosis:
my $diagnosis = $gallium->get_diagnosis;
my $grade = $gallium->get_lymphoma_grade; # returns 0 if not FL
my $icdo3 = $gallium->get_icdo3; # returns 0 if not ICDO3
# diagnosis (override entry if no ICDO3):
push @row, $icdo3
? ( join ' - ', $icdo3, uc $diagnosis )
: 'NO DIAGNOSIS MADE';
{ # lymphoma grade - only applicable to FL:
if ($grade) {
my %h = (
'1' => '9695/3',
'2' => '9691/3',
'3a' => '9698/3',
'3b' => '9698/3',
);
push @row, $h{$grade}
? sprintf '%s - FL GRADE %s', $h{$grade}, uc $grade
: 'UNGRADED';
}
elsif ( $icdo3 =~ /^9690/ ) { # FL without defined $grade
push @row, 'UNGRADED';
}
else { push @row, 'NOT APPLICABLE' }
}
{ # reason for no diagnosis - only applicable to non-ICDO3 diagnoses:
push @row, $icdo3
? 'NOT APPLICABLE' # don't need reason
: uc $diagnosis; # will be one of permitted entries in $non_icdo3
}
}
{ # 16) eligible for trial:
my $result = $gallium->get_trial_elegibility;
push @row, uc $result; # default handled by get_trial_elegibility()
}
{ # 17) discrepancy with local diagnosis
my $result = $gallium->is_discrepant_diagnosis; # returns false if not
push @row, uc $result || 'NO'; # default NO
}
{ # 18) adjudicated # query now exclusively Quintiles src so will always be NO
my $result = $gallium->get_lab_role; # warn Dumper $result;
push @row, lc $result eq 'adjudicator' ? 'YES' : 'NO';
}
{ # 19) discrepant localisation
my $result = $gallium->is_discrepant_localisation;
push @row, uc $result || 'NO'; # default NO
}
{ # 20) lymphoma involvement (%)
my $result = $gallium->get_lymphoma_involvement; # warn $result;
push @row, defined $result
? &$nearest(10, $result) # rounds to nearest 10
: 'NOT APPLICABLE'; # could be zero
}
{ # 21 .. 34 # IHC
my @markers = qw( cd3 cd5 cd10 cd20 cd23 cd79 ki67 cyclin_d1 bcl2 bcl6
foxp1 irf4 kappa lambda );
my $results = $gallium->get_test_results(\@markers); # hashref
for my $t(@markers) {
my $result = $results->{$t};
push @row, defined $result ? $result : 'NOT DONE'; # could be zero
}
}
{ # 35) t(14;18) fusion
my $result = $gallium->get_fish_fusion_assay_result('BCL2_IgH');
push @row, uc $result || 'NOT DONE';
}
{ # 36, 37, 38) BCL2, BCL6 & IgH break-apart
for ( qw/BCL2 BCL6 IgH/ ) {
my $result = $gallium->get_fish_break_apart_result($_);
push @row, uc $result || 'NOT DONE';
}
}
{ # 39, 40 BioMed2 IgH Clonality & BP length
my $result = $gallium->get_igh_pcr; # warn Dumper $result; # hashref
my $igh_status = $result->{igh_pcr_assay} || 'NOT DONE';
# if IgH clonality done:
if ( lc $igh_status eq 'done' ) {
push @row, uc $result->{igh_pcr_result} || 'NOT DONE';
push @row, $result->{igh_pcr_bp_length} || 'NOT DONE';
}
else {
push @row, ('NOT DONE', 'NOT DONE');
}
}
{ # 41 .. 46 RNA results:
my $result = $gallium->get_rna_results; # warn Dumper $result; # hashref
# extraction done:
push @row, %$result ? 'YES' : 'NO';
# rna purity (should be to 2dp):
push @row, $result->{rna_purity} ?
sprintf '%.2f', $result->{rna_purity} : 'NOT APPLICABLE';
# rna concentration (integer):
push @row, defined $result->{rna_concentration} ? # probably can't be 0
&$round($result->{rna_concentration}) : 'NOT APPLICABLE';
{ # rna aliquots & volume per aliquot:
no warnings 'uninitialized'; # maybe no $result->{rna_volume}
my ($number, $volume) = split ' x ', $result->{rna_volume}; # eg 2 x 15;
push @row, $number || 'NOT APPLICABLE';
push @row, $volume || 'NOT APPLICABLE';
}
{ # number of rna sections:
no warnings 'uninitialized'; # maybe no $result->{rna_sections}
my ($number, $t) = split ' x ', $result->{rna_sections}; # eg 10 x 5;
push @row, $number || 'NOT APPLICABLE'; # don't need thickness ($t)
}
}
{ # 47 .. 52 DNA results:
my $result = $gallium->get_dna_results; # warn Dumper $result; # hashref
# extraction done:
push @row, %$result ? 'YES' : 'NO';
# dna purity (should be to 2dp):
push @row, $result->{dna_purity} ?
sprintf '%.2f', $result->{dna_purity} : 'NOT APPLICABLE';
# dna concentration (integer):
push @row, defined $result->{dna_concentration} ? # probably can't be 0
&$round($result->{dna_concentration}) : 'NOT APPLICABLE';
{ # dna aliquots & volume per aliquot:
no warnings 'uninitialized'; # maybe no $result->{dna_volume}
my ($number, $volume) = split ' x ', $result->{dna_volume}; # eg 2 x 15;
push @row, $number || 'NOT APPLICABLE';
push @row, $volume || 'NOT APPLICABLE';
}
{ # number of dna sections:
no warnings 'uninitialized'; # maybe no $result->{dna_sections}
my ($number, $t) = split ' x ', $result->{dna_sections}; # eg 10 x 5;
push @row, $number || 'NOT APPLICABLE'; # don't need thickness ($t)
}
}
{ # 53 .. 57 TMA
my $result = $gallium->get_tma_results; # warn Dumper $result; # hashref
# was TMA taken [YES, or reason if not]:
my $tma_status = $result->{tma_status};
# if TMA taken:
if ( $tma_status && $tma_status eq 'YES' ) {
push @row, 'YES'; # TMA taken
push @row, 'NOT APPLICABLE'; # reason
push @row, $result->{tma_cores} || 0; # no. of TMA cores [1/2]
push @row, 'GALLIUM-Leeds-' . $vars->{hmds_ref}; # name of TMA
push @row, $result->{tma_position}; # position on TMA
}
else { # $result->{tma_taken} != YES, or no $result:
push @row, $tma_status ? 'NO' : 'PENDING'; # ie tma_status != YES
push @row, uc $tma_status || 'PENDING'; # reason
push @row, 'NOT APPLICABLE' for (1..3); # no of cores, name & position
}
}
{ # 58, 59, 60 sections (which ones, DNA, RNA or what else?)
push @row, 'NO'; # sections taken?
push @row, 'NOT APPLICABLE'; # reason not taken [or INSUFFICIENT MATERIAL]
push @row, 0; # no. of sections [or NOT APPLICABLE]
}
push @data, \@row; # warn Dumper \@row;
}
if (@skipped) {
my $msg = 'skipped requests: ' . join ',', @skipped;
$tools->mail_admin({ script => $0, msg => $msg });
}
exit if ! @data;
{ # construct excel spreadsheet:
my $xl = Spreadsheet::WriteExcel::Simple->new;
$xl->write_bold_row($col_headers); # arrayref
$xl->write_row($_) for @data;
# save to file if just testing:
$xl->save($Bin.'/'.$filename) if $JUST_TESTING;
_send_mail($xl->data) unless $SUPPRESS_EMAIL;
}
sub _date_transform {
my $date = DateTime::Format::MySQL->parse_date(@_)->strftime(DATE_FORMAT);
$date =~ tr/[a-z]/[A-Z]/; # warn $date; # dd-mon-yyyy -> dd-MON-yyyy
return $date;
}
sub _send_mail {
my $data = shift;
my %mail = (
attachment => $data,
filename => $filename,
subject => $subject,
config => $config,
); # warn Dumper \%mail;
$tools->send_mail(\%mail, \@recipients)
}
sub _col_headers {
my @cols = (
[ NULL => 'lab_id' ], # don't need col header
[ NULL => 'accession_number' ], # don't need col header
[ SDL2086 => 'lab_origin' ],
[ SDL2605 => 'registered' ],
[ SDL2606 => 'hmds_ref' ],
[ SDL2607 => 'kiel_ref' ],
[ SDL2608 => 'biopsy_type' ],
[ SDL2609 => 'slides_or_blocks' ],
[ SDL2610 => 'slides' ],
[ SDL2611 => 'blocks' ],
[ SDL2089 => 'specimen_quality' ],
[ SDL2612 => 'reported' ],
[ SDL2094 => 'diagnosis' ],
[ SDL2090 => 'grade' ],
[ SDL2703 => 'no_diagnosis_reason' ],
[ SDL2613 => 'elegible' ],
[ SDL2095 => 'discrepant_diagnosis' ],
[ SDL2614 => 'adjudicated' ],
[ SDL2615 => 'discrepant_localisation' ],
[ SDL2092 => 'lymphoma_involvement' ],
[ SDL2110 => 'cd3' ],
[ SDL2099 => 'cd5' ],
[ SDL2100 => 'cd10' ],
[ SDL2101 => 'cd20' ],
[ SDL2102 => 'cd23' ],
[ SDL2105 => 'cd79' ],
[ SDL2109 => 'ki67' ],
[ SDL2104 => 'cyclin_d1' ],
[ SDL2103 => 'bcl2' ],
[ SDL2108 => 'bcl6' ],
[ SDL2107 => 'foxp1' ],
[ SDL2106 => 'irf4' ],
[ SDL2123 => 'kappa' ],
[ SDL2134 => 'lambda' ],
[ SDL2117 => 't(14;18)_fusion' ],
[ SDL2115 => 'BCL2_breakapart' ],
[ SDL2116 => 'BCL6_breakapart' ],
[ SDL2638 => 'IgH_breakapart' ],
[ SDL2639 => 'IgH_clonality' ],
[ SDL2113 => 'IgH_BP_length' ],
[ SDL2616 => 'rna_extraction' ],
[ SDL2617 => 'rna_purity' ],
[ SDL2618 => 'rna_concentration' ],
[ SFL2619 => 'rna_aliquots' ],
[ SDL2620 => 'rna_volume' ],
[ SDL2621 => 'rna_sections' ],
[ SDL2623 => 'dna_extraction' ],
[ SDL2624 => 'dna_purity' ],
[ SDL2625 => 'dna_concentration' ],
[ SDL2626 => 'dna_aliquots' ],
[ SDL2627 => 'dna_volume' ],
[ SDL2628 => 'dna_sections' ],
[ SDL2629 => 'tma_taken' ],
[ SDL2630 => 'tma_reason' ],
[ SDL2631 => 'tma_cores' ],
[ SDL2632 => 'tma_name' ],
[ SDL2633 => 'tma_position' ],
[ SDL2634 => 'sections_taken' ],
[ SDL2635 => 'reason' ],
[ SDL2636 => 'sections' ],
);
my @headers = map $_->[0], @cols; # warn Dumper \@headers;
# convert NULL's to empty string:
map { s/NULL//; } @headers; # warn Dumper \@headers;
return \@headers;
}
__DATA__
Qlabs Accession number
Central Pathology Lab [LEEDS]
Registered Date at Central Pathology [DDMMMYYYY]
Accession no in central pathology-Leeds: 5 digit number/2 digit year
Accession no in central pathology-Kiel: [NOT APPLICABLE]
Biopsy [FINE NEEDLE ASPIRATION / CORE BIOPSY / SURGICAL EXCISION / OTHER]
Slides or Blocks [SLIDES / BLOCKS]
Number of Slides Received [1-100]
Number of Blocks Received [1-10]
Specimen Quality [ADEQUATE / INADEQUATE / POOR / NO TISSUE LEFT]
Date of Final Diagnosis by Central Pathology [DDMMMYYYY]
Final Diagnosis by Central Pathology (including Adjudication) According to Modified WHO 2011
FL Grades [9695/3 - FL GRADE 1 / 9691/3 - FL GRADE 2 / 9698/3 - FL GRADE 3A / 9698/3 - FL GRADE 3B / UNGRADED]
Reason Why No Diagnosis Made [INSUFFICIENT MATERIAL FOR CENTRAL PATHOLOGY - QUINTILES TO REQUEST
ANOTHER SAMPLE / UNSUITABLE MATERIAL - QUINTILES TO REQUEST ANOTHER SAMPLE / NO
LYMPHOMA IN SPECIMEN - QUINTILES TO REQUEST ANOTHER SAMPLE / NOT APPLICABLE]
Eligible to Gallium [YES / NO / PENDING]
Discrepancy with Diagnosis of Local Pathology [YES - QUINTILES TO NOTIFY SITE / NO]
Adjudicated [YES / NO]
Discrepant Localization [YES - QUINTILES TO REQUEST ANOTHER SAMPLE / NO]
Bone Marrow Result-% of Lymphoma Involvement [ 0, 10, 20 .. 100 / NOT APPLICABLE]
CD3 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
CD5 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
CD10 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
CD20 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
CD23 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
CD79A [+ or - or +/- / NOT DONE / NOT EVALUABLE]
KI67 [0, 10, 20 .. 100 / NOT DONE / NOT EVALUABLE]
Cyclin D1 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
Bcl-2 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
Bcl-6 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
Foxp1 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
IRF4 [+ or - or +/- / NOT DONE / NOT EVALUABLE]
kappa [+ or - or +/- / NOT DONE / NOT EVALUABLE]
lambda [+ or - or +/- / NOT DONE / NOT EVALUABLE]
t(14;18) Fusion [POSITIVE / NEGATIVE / NOT DONE / NOT EVALUABLE]
Bcl-2 Break-Apart [POSITIVE / NEGATIVE / NOT DONE / NOT EVALUABLE]
Bcl-6 Break-Apart [POSITIVE / NEGATIVE / NOT DONE / NOT EVALUABLE]
IgH Break-Apart [POSITIVE / NEGATIVE / NOT DONE / NOT EVALUABLE]
BioMed2 IgH Clonality [MONOCLONAL / OLIGOCLONAL / POLYCLONAL / NO MARKER / NOT DONE / NOT EVALUABLE]
BioMed2 IgH Clonality Length of Clonal Peak in Base Pairs (if IgH monoclonal) [0-1000 / NOT DONE]
RNA Extraction [YES / INSUFFICIENT MATERIAL / NO / PENDING]
RNA Purity OD 260/280 [n.nn / NOT APPLICABLE]
RNA Concentration (�g/�l) [0-999 / NOT APPLICABLE]
Number of RNA Aliquots [1-9 / NOT APPLICABLE]
RNA Volume per Aliquot in �l [1-20 / NOT APPLICABLE]
Number of Sections for RNA Extraction [1-5 / NOT APPLICABLE]
DNA Extraction [YES / INSUFFICIENT MATERIAL / NO / PENDING]
DNA Purity OD 260/280 [n.nn / NOT APPLICABLE]
DNA concentration (�g/�l) [0-999 / NOT APPLICABLE]
Number of DNA Aliquots (1-9 or NOT APPLICABLE
DNA Volume per Aliquot in �l [1-20 / NOT APPLICABLE]
Number of Sections for DNA Extraction [1-5 / NOT APPLICABLE]
TMA Taken? [YES / NO / PENDING]
Reason TMA Not Taken [INSUFFICIENT MATERIAL / NOT APPLICABLE]
Number of TMA Cores Taken [1 / 2]
Name of TMA [GALLIUM-Leeds-LAB-number] respectively [GALLIUM-Kiel-LAB-number] - what ??
Position on TMA [1-50]
Sections Taken? [YES / NO / PENDING]
Reason Sections Not Taken [INSUFFICIENT MATERIAL / NOT APPLICABLE]
Number of Sections Taken (1-10)