From 5c42e0fb2b05ddf75ab818e705be3acca20b53c6 Mon Sep 17 00:00:00 2001 From: Ian Sealy Date: Wed, 31 Jul 2013 13:47:49 +0100 Subject: [PATCH] Initial code import. --- MANIFEST.SKIP | 2 + dist.ini | 25 + lib/DETCT.pm | 21 + lib/DETCT/Analysis.pm | 1569 ++++++++++++++++++++ lib/DETCT/Gene.pm | 574 ++++++++ lib/DETCT/GeneFinder.pm | 417 ++++++ lib/DETCT/Misc/BAM.pm | 1332 +++++++++++++++++ lib/DETCT/Misc/Output.pm | 738 ++++++++++ lib/DETCT/Misc/PeakHMM.pm | 520 +++++++ lib/DETCT/Misc/R.pm | 271 ++++ lib/DETCT/Misc/Tag.pm | 269 ++++ lib/DETCT/Pipeline.pm | 1280 +++++++++++++++++ lib/DETCT/Pipeline/Job.pm | 697 +++++++++ lib/DETCT/Pipeline/Stage.pm | 236 ++++ lib/DETCT/Pipeline/WithDiffExprStages.pm | 1236 ++++++++++++++++ lib/DETCT/Sample.pm | 367 +++++ lib/DETCT/Sequence.pm | 161 +++ lib/DETCT/Transcript.pm | 516 +++++++ perlcritic.rc | 12 + pod-stop-words.txt | 0 script/detag_fastq.pl | 163 +++ script/make_test_fasta.pl | 164 +++ script/make_test_fastq.pl | 276 ++++ script/make_test_sam.pl | 630 +++++++++ script/run_de_pipeline.pl | 236 ++++ script/run_deseq.R | 124 ++ src/quince_chiphmmnew.cpp | 596 ++++++++ t/analysis.t | 490 +++++++ t/data/test1.bam | Bin 0 -> 77231 bytes t/data/test1.bam.bai | Bin 0 -> 416 bytes t/data/test12.fa | 400 ++++++ t/data/test12.fa.fai | 5 + t/data/test1_1.fastq | 400 ++++++ t/data/test1_2.fastq | 400 ++++++ t/data/test2.bam | Bin 0 -> 87517 bytes t/data/test2.bam.bai | Bin 0 -> 416 bytes t/data/test2_1.fastq | 400 ++++++ t/data/test2_2.fastq | 400 ++++++ t/data/test3.bam | Bin 0 -> 97371 bytes t/data/test3.bam.bai | Bin 0 -> 416 bytes t/data/test3.fa | 360 +++++ t/data/test3.fa.fai | 5 + t/data/test_analysis1122.yaml | 43 + t/data/test_analysis12.yaml | 28 + t/data/test_analysis13.yaml | 28 + t/data/test_de.yaml | 75 + t/gene.t | 119 ++ t/genefinder.t | 311 ++++ t/misc-bam.t | 1652 ++++++++++++++++++++++ t/misc-output.t | 98 ++ t/misc-peakhmm.t | 572 ++++++++ t/misc-tag.t | 260 ++++ t/pipeline-job.t | 102 ++ t/pipeline-stage.t | 64 + t/sample.t | 81 ++ t/sequence.t | 36 + t/transcript.t | 102 ++ 57 files changed, 18863 insertions(+) create mode 100644 MANIFEST.SKIP create mode 100644 dist.ini create mode 100644 lib/DETCT.pm create mode 100644 lib/DETCT/Analysis.pm create mode 100644 lib/DETCT/Gene.pm create mode 100644 lib/DETCT/GeneFinder.pm create mode 100644 lib/DETCT/Misc/BAM.pm create mode 100644 lib/DETCT/Misc/Output.pm create mode 100644 lib/DETCT/Misc/PeakHMM.pm create mode 100644 lib/DETCT/Misc/R.pm create mode 100644 lib/DETCT/Misc/Tag.pm create mode 100644 lib/DETCT/Pipeline.pm create mode 100644 lib/DETCT/Pipeline/Job.pm create mode 100644 lib/DETCT/Pipeline/Stage.pm create mode 100644 lib/DETCT/Pipeline/WithDiffExprStages.pm create mode 100644 lib/DETCT/Sample.pm create mode 100644 lib/DETCT/Sequence.pm create mode 100644 lib/DETCT/Transcript.pm create mode 100644 perlcritic.rc create mode 100644 pod-stop-words.txt create mode 100644 script/detag_fastq.pl create mode 100644 script/make_test_fasta.pl create mode 100644 script/make_test_fastq.pl create mode 100644 script/make_test_sam.pl create mode 100644 script/run_de_pipeline.pl create mode 100644 script/run_deseq.R create mode 100644 src/quince_chiphmmnew.cpp create mode 100644 t/analysis.t create mode 100644 t/data/test1.bam create mode 100644 t/data/test1.bam.bai create mode 100644 t/data/test12.fa create mode 100644 t/data/test12.fa.fai create mode 100644 t/data/test1_1.fastq create mode 100644 t/data/test1_2.fastq create mode 100644 t/data/test2.bam create mode 100644 t/data/test2.bam.bai create mode 100644 t/data/test2_1.fastq create mode 100644 t/data/test2_2.fastq create mode 100644 t/data/test3.bam create mode 100644 t/data/test3.bam.bai create mode 100644 t/data/test3.fa create mode 100644 t/data/test3.fa.fai create mode 100644 t/data/test_analysis1122.yaml create mode 100644 t/data/test_analysis12.yaml create mode 100644 t/data/test_analysis13.yaml create mode 100644 t/data/test_de.yaml create mode 100644 t/gene.t create mode 100644 t/genefinder.t create mode 100644 t/misc-bam.t create mode 100644 t/misc-output.t create mode 100644 t/misc-peakhmm.t create mode 100644 t/misc-tag.t create mode 100644 t/pipeline-job.t create mode 100644 t/pipeline-stage.t create mode 100644 t/sample.t create mode 100644 t/sequence.t create mode 100644 t/transcript.t diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP new file mode 100644 index 0000000..2133859 --- /dev/null +++ b/MANIFEST.SKIP @@ -0,0 +1,2 @@ +bin/quince_chiphmmnew +tmp diff --git a/dist.ini b/dist.ini new file mode 100644 index 0000000..383a95e --- /dev/null +++ b/dist.ini @@ -0,0 +1,25 @@ +name = DETCT +author = James Morris +author = Ian Sealy +license = GPL_3 +copyright_holder = Genome Research Ltd +copyright_year = 2013 +version = 0.1.0 + +[@Basic] +[ExecDir] +dir = script +[FileFinder::ByName / ScriptNotR] +dir = script +skip = .*\.R$ +[ModuleBuild] +[PodWeaver] +finder = :InstallModules +finder = ScriptNotR +[PodCoverageTests] +[PodSyntaxTests] +[Test::Perl::Critic] +[PerlTidy] +[AutoPrereqs] +[PkgVersion] +[Test::Compile] diff --git a/lib/DETCT.pm b/lib/DETCT.pm new file mode 100644 index 0000000..8777d00 --- /dev/null +++ b/lib/DETCT.pm @@ -0,0 +1,21 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT; +## use critic + +# ABSTRACT: Transcript Counting API + +## Author : is1 +## Maintainer : is1 +## Created : 2012-09-18 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +1; diff --git a/lib/DETCT/Analysis.pm b/lib/DETCT/Analysis.pm new file mode 100644 index 0000000..0899cd4 --- /dev/null +++ b/lib/DETCT/Analysis.pm @@ -0,0 +1,1569 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Analysis; +## use critic + +# ABSTRACT: Object representing an analysis of a collection of samples + +## Author : is1 +## Maintainer : is1 +## Created : 2012-09-19 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Readonly; +use Class::InsideOut qw( private register id ); +use List::MoreUtils qw( uniq ); +use YAML::Tiny; +use Data::Compare; +use DETCT::Sample; +use DETCT::Sequence; +use DETCT::Misc::BAM; + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Attributes: +private name => my %name; # e.g. zmp_ph1 +private sample => my %sample; # arrayref of samples +private sequence => my %sequence; # arrayref of sequences +private read1_length => my %read1_length; # e.g. 30 +private read2_length => my %read2_length; # e.g. 54 +private mismatch_threshold => my %mismatch_threshold; # e.g. 2 +private bin_size => my %bin_size; # e.g. 100 +private peak_buffer_width => my %peak_buffer_width; # e.g. 100 +private hmm_sig_level => my %hmm_sig_level; # e.g. 0.001 +private hmm_binary => my %hmm_binary; # e.g. ~/quince_chiphmmnew +private r_binary => my %r_binary; # e.g. R +private deseq_script => my %deseq_script; # e.g. ~/run_deseq.R +private output_sig_level => my %output_sig_level; # e.g. 0.05 +private ref_fasta => my %ref_fasta; # e.g. zv9.fa +private fasta_index => my %fasta_index; # Bio::DB::Sam::Fai +private ensembl_host => my %ensembl_host; # e.g. ensembldb.ensembl.org +private ensembl_port => my %ensembl_port; # e.g. 3306 +private ensembl_user => my %ensembl_user; # e.g. anonymous +private ensembl_pass => my %ensembl_pass; # e.g. secret +private ensembl_name => my %ensembl_name; # e.g. zv9_core +private ensembl_species => my %ensembl_species; # e.g. danio_rerio +private slice_adaptor => my %slice_adaptor; # Bio::EnsEMBL::DBSQL::SliceAdaptor +private chunk_total => my %chunk_total; # e.g. 20 +private chunk => my %chunk; # arrayref of arrayrefs of sequences +private test_chunk => my %test_chunk; # e.g. 1 + +# Constants +Readonly our $MAX_NAME_LENGTH => 128; +Readonly our $DEFAULT_ENSEMBL_HOST => 'ensembldb.ensembl.org'; +Readonly our $DEFAULT_ENSEMBL_USER => 'anonymous'; + +=method new + + Usage : my $analysis = DETCT::Analysis->new( { + name => 'zmp_ph1', + read1_length => 30, + read2_length => 54, + mismatch_threshold => 2, + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + hmm_binary => 'bin/quince_chiphmmnew', + r_binary => 'R', + deseq_script => 'script/run_deseq.R', + output_sig_level => 0.05, + chunk_total => 20, + } ); + Purpose : Constructor for analysis objects + Returns : DETCT::Analysis + Parameters : Hashref { + name => String, + read1_length => Int, + read2_length => Int, + mismatch_threshold => Int, + bin_size => Int, + peak_buffer_width => Int, + hmm_sig_level => Float, + hmm_binary => String, + r_binary => String, + deseq_script => String, + output_sig_level => Float, + ref_fasta => String or undef, + ensembl_host => String or undef, + ensembl_port => Int or undef, + ensembl_user => String or undef, + ensembl_pass => String or undef, + ensembl_name => String or undef, + ensembl_species => String or undef, + chunk_total => Int, + test_chunk => Int or undef, + } + Throws : No exceptions + Comments : None + +=cut + +sub new { + my ( $class, $arg_ref ) = @_; + my $self = register($class); + $self->set_name( $arg_ref->{name} ); + $self->set_read1_length( $arg_ref->{read1_length} ); + $self->set_read2_length( $arg_ref->{read2_length} ); + $self->set_mismatch_threshold( $arg_ref->{mismatch_threshold} ); + $self->set_bin_size( $arg_ref->{bin_size} ); + $self->set_peak_buffer_width( $arg_ref->{peak_buffer_width} ); + $self->set_hmm_sig_level( $arg_ref->{hmm_sig_level} ); + $self->set_hmm_binary( $arg_ref->{hmm_binary} ); + $self->set_r_binary( $arg_ref->{r_binary} ); + $self->set_deseq_script( $arg_ref->{deseq_script} ); + $self->set_output_sig_level( $arg_ref->{output_sig_level} ); + $self->set_ref_fasta( $arg_ref->{ref_fasta} ); + $self->set_ensembl_host( $arg_ref->{ensembl_host} ); + $self->set_ensembl_port( $arg_ref->{ensembl_port} ); + $self->set_ensembl_user( $arg_ref->{ensembl_user} ); + $self->set_ensembl_pass( $arg_ref->{ensembl_pass} ); + $self->set_ensembl_name( $arg_ref->{ensembl_name} ); + $self->set_ensembl_species( $arg_ref->{ensembl_species} ); + $self->set_chunk_total( $arg_ref->{chunk_total} ); + $self->set_test_chunk( $arg_ref->{test_chunk} ); + return $self; +} + +=method new_from_yaml + + Usage : my $analysis = DETCT::Analysis->new_from_yaml( 'zmp_ph1.yaml' ); + Purpose : Constructor for creating analysis objects from a YAML file + Returns : DETCT::Analysis + Parameters : String (the YAML file) + Throws : If YAML file is missing or not readable + Comments : None + +=cut + +sub new_from_yaml { + my ( $class, $yaml_file ) = @_; + my $self = register($class); + + confess "YAML file ($yaml_file) does not exist or cannot be read" + if !-r $yaml_file; + + my $yaml = YAML::Tiny->read($yaml_file); + + $self->set_name( $yaml->[0]->{name} ); + $self->set_read1_length( $yaml->[0]->{read1_length} ); + $self->set_read2_length( $yaml->[0]->{read2_length} ); + $self->set_mismatch_threshold( $yaml->[0]->{mismatch_threshold} ); + $self->set_bin_size( $yaml->[0]->{bin_size} ); + $self->set_peak_buffer_width( $yaml->[0]->{peak_buffer_width} ); + $self->set_hmm_sig_level( $yaml->[0]->{hmm_sig_level} ); + $self->set_hmm_binary( $yaml->[0]->{hmm_binary} ); + $self->set_r_binary( $yaml->[0]->{r_binary} ); + $self->set_deseq_script( $yaml->[0]->{deseq_script} ); + $self->set_output_sig_level( $yaml->[0]->{output_sig_level} ); + $self->set_ref_fasta( $yaml->[0]->{ref_fasta} ); + $self->set_ensembl_host( $yaml->[0]->{ensembl_host} ); + $self->set_ensembl_port( $yaml->[0]->{ensembl_port} ); + $self->set_ensembl_user( $yaml->[0]->{ensembl_user} ); + $self->set_ensembl_pass( $yaml->[0]->{ensembl_pass} ); + $self->set_ensembl_name( $yaml->[0]->{ensembl_name} ); + $self->set_ensembl_species( $yaml->[0]->{ensembl_species} ); + $self->set_chunk_total( $yaml->[0]->{chunk_total} ); + $self->set_test_chunk( $yaml->[0]->{test_chunk} ); + + foreach my $sample_hash ( @{ $yaml->[0]->{samples} } ) { + my $sample = DETCT::Sample->new( + { + name => $sample_hash->{name}, + description => $sample_hash->{description}, + condition => $sample_hash->{condition}, + group => $sample_hash->{group}, + tag => $sample_hash->{tag}, + bam_file => $sample_hash->{bam_file}, + } + ); + $self->add_sample( $sample, 1 ); # 1 = do not validate + } + + $self->_validate(); + + return $self; +} + +=method name + + Usage : my $name = $analysis->name; + Purpose : Getter for name attribute + Returns : String (e.g. "zmp_ph1") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub name { + my ($self) = @_; + return $name{ id $self}; +} + +=method set_name + + Usage : $analysis->set_name('zmp_ph1'); + Purpose : Setter for name attribute + Returns : undef + Parameters : String (the name) + Throws : No exceptions + Comments : None + +=cut + +sub set_name { + my ( $self, $arg ) = @_; + $name{ id $self} = _check_name($arg); + return; +} + +# Usage : $name = _check_name($name); +# Purpose : Check for valid name +# Returns : String (the valid name) +# Parameters : String (the name) +# Throws : If name is missing +# If name is empty +# If name > $MAX_NAME_LENGTH characters +# Comments : None +sub _check_name { + my ($name) = @_; + + confess 'No name specified' if !defined $name; + confess 'Empty name specified' if !length $name; + confess "Name ($name) longer than $MAX_NAME_LENGTH characters" + if length $name > $MAX_NAME_LENGTH; + + return $name; +} + +=method add_sample + + Usage : $analysis->add_sample($sample); + Purpose : Add a sample to an analysis + Returns : undef + Parameters : DETCT::Sample + Defined or undef (indicating if validation is needed) + Throws : If sample is missing or invalid (i.e. not a DETCT::Sample + object) + Comments : None + +=cut + +sub add_sample { + my ( $self, $sample, $no_validaton ) = @_; + + confess 'No sample specified' if !defined $sample; + confess 'Class of sample (', ref $sample, ') not DETCT::Sample' + if !$sample->isa('DETCT::Sample'); + + if ( !exists $sample{ id $self} ) { + $sample{ id $self} = [$sample]; + $self->add_all_sequences( $sample->bam_file ); # Because first sample + } + else { + push @{ $sample{ id $self} }, $sample; + } + + if ( !defined $no_validaton ) { + $self->_validate(); + } + + return; +} + +=method get_all_samples + + Usage : $samples = $analysis->get_all_samples(); + Purpose : Get all samples of an analysis + Returns : Arrayref of DETCT::Sample objects + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub get_all_samples { + my ($self) = @_; + + return $sample{ id $self} || []; +} + +=method add_all_sequences + + Usage : $analysis->add_all_sequences($bam_file); + Purpose : Add all sequences (sorted by decreasing length) to an analysis + Returns : undef + Parameters : String (the BAM file) + Throws : No exceptions + Comments : None + +=cut + +sub add_all_sequences { + my ( $self, $bam_file ) = @_; + + $bam_file = DETCT::Sample::check_bam_file($bam_file); + + $sequence{ id $self} = []; + + my %len = DETCT::Misc::BAM::get_reference_sequence_lengths($bam_file); + + foreach my $name ( reverse sort { $len{$a} <=> $len{$b} } keys %len ) { + my $sequence = DETCT::Sequence->new( + { + name => $name, + bp => $len{$name}, + } + ); + + push @{ $sequence{ id $self} }, $sequence; + } + + # Group sequences into chunks + $self->add_all_chunks(); + + return; +} + +=method get_all_sequences + + Usage : $sequences = $analysis->get_all_sequences(); + Purpose : Get all sequences (sorted by decreasing length) of an analysis + Returns : Arrayref of DETCT::Sequence objects + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub get_all_sequences { + my ($self) = @_; + + return $sequence{ id $self} || []; +} + +# Usage : $analysis->_validate(); +# Purpose : Check analysis +# Returns : 1 +# Parameters : None +# Throws : If reference sequences don't match +# Comments : None +sub _validate { + my ($self) = @_; + + my @bam_files = $self->list_all_bam_files(); + + # Compare reference sequence from first BAM file to all other BAM files + my $first_bam_file = shift @bam_files; + my %first_bam_length = + DETCT::Misc::BAM::get_reference_sequence_lengths($first_bam_file); + foreach my $bam_file (@bam_files) { + my %bam_length = + DETCT::Misc::BAM::get_reference_sequence_lengths($bam_file); + if ( !Compare( \%first_bam_length, \%bam_length ) ) { + confess "$first_bam_file and $bam_file use different reference"; + } + } + + return 1; +} + +=method read1_length + + Usage : my $read1_length = $analysis->read1_length; + Purpose : Getter for read 1 length attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub read1_length { + my ($self) = @_; + return $read1_length{ id $self}; +} + +=method set_read1_length + + Usage : $analysis->set_read1_length(20); + Purpose : Setter for read 1 length attribute + Returns : undef + Parameters : +ve Int (the read 1 length) + Throws : No exceptions + Comments : None + +=cut + +sub set_read1_length { + my ( $self, $arg ) = @_; + $read1_length{ id $self} = _check_read1_length($arg); + return; +} + +# Usage : $read1_length = _check_read1_length($read1_length); +# Purpose : Check for valid read 1 length +# Returns : +ve Int (the valid read 1 length) +# Parameters : +ve Int (the read 1 length) +# Throws : If read 1 length is missing or not a positive integer +# Comments : None +sub _check_read1_length { + my ($read1_length) = @_; + return $read1_length + if defined $read1_length && $read1_length =~ m/\A \d+ \z/xms; + confess 'No read 1 length specified' if !defined $read1_length; + confess "Invalid read 1 length ($read1_length) specified"; +} + +=method read2_length + + Usage : my $read2_length = $analysis->read2_length; + Purpose : Getter for read 2 length attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub read2_length { + my ($self) = @_; + return $read2_length{ id $self}; +} + +=method set_read2_length + + Usage : $analysis->set_read2_length(20); + Purpose : Setter for read 2 length attribute + Returns : undef + Parameters : +ve Int (the read 2 length) + Throws : No exceptions + Comments : None + +=cut + +sub set_read2_length { + my ( $self, $arg ) = @_; + $read2_length{ id $self} = _check_read2_length($arg); + return; +} + +# Usage : $read2_length = _check_read2_length($read2_length); +# Purpose : Check for valid read 2 length +# Returns : +ve Int (the valid read 2 length) +# Parameters : +ve Int (the read 2 length) +# Throws : If read 2 length is missing or not a positive integer +# Comments : None +sub _check_read2_length { + my ($read2_length) = @_; + return $read2_length + if defined $read2_length && $read2_length =~ m/\A \d+ \z/xms; + confess 'No read 2 length specified' if !defined $read2_length; + confess "Invalid read 2 length ($read2_length) specified"; +} + +=method mismatch_threshold + + Usage : my $mismatch_threshold = $analysis->mismatch_threshold; + Purpose : Getter for mismatch threshold attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub mismatch_threshold { + my ($self) = @_; + return $mismatch_threshold{ id $self}; +} + +=method set_mismatch_threshold + + Usage : $analysis->set_mismatch_threshold(20); + Purpose : Setter for mismatch threshold attribute + Returns : undef + Parameters : +ve Int (the mismatch threshold) + Throws : No exceptions + Comments : None + +=cut + +sub set_mismatch_threshold { + my ( $self, $arg ) = @_; + $mismatch_threshold{ id $self} = _check_mismatch_threshold($arg); + return; +} + +# Usage : $mismatch_threshold +# = _check_mismatch_threshold($mismatch_threshold); +# Purpose : Check for valid mismatch threshold +# Returns : +ve Int (the valid mismatch threshold) +# Parameters : +ve Int (the mismatch threshold) +# Throws : If mismatch threshold is missing or not a positive integer +# Comments : None +sub _check_mismatch_threshold { + my ($mismatch_threshold) = @_; + return $mismatch_threshold + if defined $mismatch_threshold && $mismatch_threshold =~ m/\A \d+ \z/xms; + confess 'No mismatch threshold specified' if !defined $mismatch_threshold; + confess "Invalid mismatch threshold ($mismatch_threshold) specified"; +} + +=method bin_size + + Usage : my $bin_size = $analysis->bin_size; + Purpose : Getter for bin size attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub bin_size { + my ($self) = @_; + return $bin_size{ id $self}; +} + +=method set_bin_size + + Usage : $analysis->set_bin_size(100); + Purpose : Setter for bin size attribute + Returns : undef + Parameters : +ve Int (the bin size) + Throws : No exceptions + Comments : None + +=cut + +sub set_bin_size { + my ( $self, $arg ) = @_; + $bin_size{ id $self} = _check_bin_size($arg); + return; +} + +# Usage : $bin_size = _check_bin_size($bin_size); +# Purpose : Check for valid bin size +# Returns : +ve Int (the valid bin size) +# Parameters : +ve Int (the bin size) +# Throws : If bin size is missing or not a positive integer +# Comments : None +sub _check_bin_size { + my ($bin_size) = @_; + return $bin_size + if defined $bin_size && $bin_size =~ m/\A \d+ \z/xms; + confess 'No bin size specified' if !defined $bin_size; + confess "Invalid bin size ($bin_size) specified"; +} + +=method peak_buffer_width + + Usage : my $peak_buffer_width = $analysis->peak_buffer_width; + Purpose : Getter for peak buffer width attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub peak_buffer_width { + my ($self) = @_; + return $peak_buffer_width{ id $self}; +} + +=method set_peak_buffer_width + + Usage : $analysis->set_peak_buffer_width(100); + Purpose : Setter for peak buffer width attribute + Returns : undef + Parameters : +ve Int (the peak buffer width) + Throws : No exceptions + Comments : None + +=cut + +sub set_peak_buffer_width { + my ( $self, $arg ) = @_; + $peak_buffer_width{ id $self} = _check_peak_buffer_width($arg); + return; +} + +# Usage : $peak_buffer_width = _check_peak_buffer_width($peak_buffer_width); +# Purpose : Check for valid peak buffer width +# Returns : +ve Int (the valid peak buffer width) +# Parameters : +ve Int (the peak buffer width) +# Throws : If peak buffer width is missing or not a positive integer +# Comments : None +sub _check_peak_buffer_width { + my ($peak_buffer_width) = @_; + return $peak_buffer_width + if defined $peak_buffer_width && $peak_buffer_width =~ m/\A \d+ \z/xms; + confess 'No peak buffer width specified' if !defined $peak_buffer_width; + confess "Invalid peak buffer width ($peak_buffer_width) specified"; +} + +=method hmm_sig_level + + Usage : my $hmm_sig_level = $analysis->hmm_sig_level; + Purpose : Getter for HMM significance level attribute + Returns : +ve Float + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub hmm_sig_level { + my ($self) = @_; + return $hmm_sig_level{ id $self}; +} + +=method set_hmm_sig_level + + Usage : $analysis->set_hmm_sig_level(0.001); + Purpose : Setter for HMM significance level attribute + Returns : undef + Parameters : +ve Float (the HMM significance level) + Throws : No exceptions + Comments : None + +=cut + +sub set_hmm_sig_level { + my ( $self, $arg ) = @_; + $hmm_sig_level{ id $self} = _check_hmm_sig_level($arg); + return; +} + +# Usage : $hmm_sig_level = _check_hmm_sig_level($hmm_sig_level); +# Purpose : Check for valid HMM significance level +# Returns : +ve Float (the valid HMM significance level) +# Parameters : +ve Float (the HMM significance level) +# Throws : If HMM significance level is missing or not a positive float +# Comments : None +sub _check_hmm_sig_level { + my ($hmm_sig_level) = @_; + return $hmm_sig_level + if defined $hmm_sig_level && $hmm_sig_level =~ m/\A \d* [.] \d+ \z/xms; + confess 'No HMM significance level specified' if !defined $hmm_sig_level; + confess "Invalid HMM significance level ($hmm_sig_level) specified"; +} + +=method hmm_binary + + Usage : my $hmm_binary = $analysis->hmm_binary; + Purpose : Getter for HMM binary attribute + Returns : String + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub hmm_binary { + my ($self) = @_; + return $hmm_binary{ id $self}; +} + +=method set_hmm_binary + + Usage : $analysis->set_hmm_binary('bin/quince_chiphmmnew'); + Purpose : Setter for HMM binary attribute + Returns : undef + Parameters : String (the HMM binary) + Throws : No exceptions + Comments : None + +=cut + +sub set_hmm_binary { + my ( $self, $arg ) = @_; + $hmm_binary{ id $self} = _check_hmm_binary($arg); + return; +} + +# Usage : $hmm_binary = _check_hmm_binary($hmm_binary); +# Purpose : Check for valid HMM binary +# Returns : String (the valid HMM binary) +# Parameters : String (the HMM binary) +# Throws : If HMM binary is missing or not readable +# Comments : None +sub _check_hmm_binary { + my ($hmm_binary) = @_; + return $hmm_binary if defined $hmm_binary && -r $hmm_binary; + confess 'No HMM binary specified' if !defined $hmm_binary; + confess "HMM binary ($hmm_binary) does not exist or cannot be read"; +} + +=method r_binary + + Usage : my $r_binary = $analysis->r_binary; + Purpose : Getter for R binary attribute + Returns : String + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub r_binary { + my ($self) = @_; + return $r_binary{ id $self}; +} + +=method set_r_binary + + Usage : $analysis->set_r_binary('R'); + Purpose : Setter for R binary attribute + Returns : undef + Parameters : String (the R binary) + Throws : No exceptions + Comments : None + +=cut + +sub set_r_binary { + my ( $self, $arg ) = @_; + $r_binary{ id $self} = _check_r_binary($arg); + return; +} + +# Usage : $r_binary = _check_r_binary($r_binary); +# Purpose : Check for valid R binary +# Returns : String (the valid R binary) +# Parameters : String (the R binary) +# Throws : If R binary is missing +# Comments : None +sub _check_r_binary { + my ($r_binary) = @_; + return $r_binary if defined $r_binary; + confess 'No R binary specified'; +} + +=method deseq_script + + Usage : my $deseq_script = $analysis->deseq_script; + Purpose : Getter for DESeq script attribute + Returns : String + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub deseq_script { + my ($self) = @_; + return $deseq_script{ id $self}; +} + +=method set_deseq_script + + Usage : $analysis->set_deseq_script('script/run_deseq.R'); + Purpose : Setter for DESeq script attribute + Returns : undef + Parameters : String (the DESeq script) + Throws : No exceptions + Comments : None + +=cut + +sub set_deseq_script { + my ( $self, $arg ) = @_; + $deseq_script{ id $self} = _check_deseq_script($arg); + return; +} + +# Usage : $deseq_script = _check_deseq_script($deseq_script); +# Purpose : Check for valid DESeq script +# Returns : String (the valid DESeq script) +# Parameters : String (the DESeq script) +# Throws : If DESeq script is missing or not readable +# Comments : None +sub _check_deseq_script { + my ($deseq_script) = @_; + return $deseq_script if defined $deseq_script && -r $deseq_script; + confess 'No DESeq script specified' if !defined $deseq_script; + confess "DESeq script ($deseq_script) does not exist or cannot be read"; +} + +=method output_sig_level + + Usage : my $output_sig_level = $analysis->output_sig_level; + Purpose : Getter for output significance level attribute + Returns : +ve Float + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub output_sig_level { + my ($self) = @_; + return $output_sig_level{ id $self}; +} + +=method set_output_sig_level + + Usage : $analysis->set_output_sig_level(0.001); + Purpose : Setter for output significance level attribute + Returns : undef + Parameters : +ve Float (the output significance level) + Throws : No exceptions + Comments : None + +=cut + +sub set_output_sig_level { + my ( $self, $arg ) = @_; + $output_sig_level{ id $self} = _check_output_sig_level($arg); + return; +} + +# Usage : $output_sig_level = _check_output_sig_level($output_sig_level); +# Purpose : Check for valid output significance level +# Returns : +ve Float (the valid output significance level) +# Parameters : +ve Float (the output significance level) +# Throws : If output significance level is missing or not a positive float +# Comments : None +sub _check_output_sig_level { + my ($output_sig_level) = @_; + return $output_sig_level + if defined $output_sig_level + && $output_sig_level =~ m/\A \d* [.] \d+ \z/xms; + confess 'No output significance level specified' + if !defined $output_sig_level; + confess "Invalid output significance level ($output_sig_level) specified"; +} + +=method ref_fasta + + Usage : my $ref_fasta = $analysis->ref_fasta; + Purpose : Getter for reference FASTA attribute + Returns : String + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub ref_fasta { + my ($self) = @_; + return $ref_fasta{ id $self}; +} + +=method set_ref_fasta + + Usage : $analysis->set_ref_fasta('zv9.fa'); + Purpose : Setter for reference FASTA attribute + Returns : undef + Parameters : String (the reference FASTA) + Throws : No exceptions + Comments : None + +=cut + +sub set_ref_fasta { + my ( $self, $arg ) = @_; + $ref_fasta{ id $self} = _check_ref_fasta($arg); + return; +} + +# Usage : $ref_fasta = _check_ref_fasta($ref_fasta); +# Purpose : Check for valid reference FASTA +# Returns : String (the valid reference FASTA) +# Parameters : String (the reference FASTA) +# Throws : If reference FASTA is defined but not readable +# Comments : None +sub _check_ref_fasta { + my ($ref_fasta) = @_; + return $ref_fasta if !defined $ref_fasta || -r $ref_fasta; + confess "Reference FASTA ($ref_fasta) cannot be read"; +} + +=method ensembl_host + + Usage : my $ensembl_host = $analysis->ensembl_host; + Purpose : Getter for Ensembl host attribute + Returns : String + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +=method fasta_index + + Usage : my $fai = $analysis->fasta_index; + Purpose : Getter for FASTA index attribute + Returns : Bio::DB::Sam::Fai + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub fasta_index { + my ($self) = @_; + + if ( !defined $fasta_index{ id $self} && $self->ref_fasta ) { + + # We can create a FASTA index object + $self->set_fasta_index( Bio::DB::Sam::Fai->load( $self->ref_fasta ) ); + } + + return $fasta_index{ id $self}; +} + +=method set_fasta_index + + Usage : $analysis->set_fasta_index($fai); + Purpose : Setter for FASTA index attribute + Returns : undef + Parameters : Bio::DB::Sam::Fai + Throws : No exceptions + Comments : None + +=cut + +sub set_fasta_index { + my ( $self, $arg ) = @_; + $fasta_index{ id $self} = _check_fasta_index($arg); + return; +} + +# Usage : $fai = _check_fasta_index($fai); +# Purpose : Check for valid FASTA index +# Returns : Bio::DB::Sam::Fai +# Parameters : Bio::DB::Sam::Fai +# Throws : If FASTA index is missing or invalid (i.e. not a +# Bio::DB::Sam::Fai object) +# Comments : None +sub _check_fasta_index { + my ($fasta_index) = @_; + return $fasta_index + if defined $fasta_index && $fasta_index->isa('Bio::DB::Sam::Fai'); + confess 'No FASTA index specified' if !defined $fasta_index; + confess 'Class of FASTA index (', ref $fasta_index, + ') not Bio::DB::Sam::Fai'; +} + +sub ensembl_host { + my ($self) = @_; + return $ensembl_host{ id $self}; +} + +=method set_ensembl_host + + Usage : $analysis->set_ensembl_host('ensembldb.ensembl.org'); + Purpose : Setter for Ensembl host attribute + Returns : undef + Parameters : String (the Ensembl host) + Throws : No exceptions + Comments : None + +=cut + +sub set_ensembl_host { + my ( $self, $arg ) = @_; + $ensembl_host{ id $self} = $arg; + return; +} + +=method ensembl_port + + Usage : my $ensembl_port = $analysis->ensembl_port; + Purpose : Getter for Ensembl port attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub ensembl_port { + my ($self) = @_; + return $ensembl_port{ id $self}; +} + +=method set_ensembl_port + + Usage : $analysis->set_ensembl_port(3306); + Purpose : Setter for Ensembl port attribute + Returns : undef + Parameters : +ve Int (the Ensembl port) + Throws : No exceptions + Comments : None + +=cut + +sub set_ensembl_port { + my ( $self, $arg ) = @_; + $ensembl_port{ id $self} = _check_ensembl_port($arg); + return; +} + +# Usage : $ensembl_port = _check_ensembl_port($ensembl_port); +# Purpose : Check for valid Ensembl port +# Returns : +ve Int (the valid Ensembl port) +# Parameters : +ve Int (the Ensembl port) +# Throws : If Ensembl port is defined but not a positive integer +# Comments : None +sub _check_ensembl_port { + my ($ensembl_port) = @_; + return $ensembl_port + if !defined $ensembl_port || $ensembl_port =~ m/\A \d+ \z/xms; + confess "Invalid Ensembl port ($ensembl_port) specified"; +} + +=method ensembl_user + + Usage : my $ensembl_user = $analysis->ensembl_user; + Purpose : Getter for Ensembl username attribute + Returns : String + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub ensembl_user { + my ($self) = @_; + return $ensembl_user{ id $self}; +} + +=method set_ensembl_user + + Usage : $analysis->set_ensembl_user('anonymous'); + Purpose : Setter for Ensembl username attribute + Returns : undef + Parameters : String (the Ensembl username) + Throws : No exceptions + Comments : None + +=cut + +sub set_ensembl_user { + my ( $self, $arg ) = @_; + $ensembl_user{ id $self} = $arg; + return; +} + +=method ensembl_pass + + Usage : my $ensembl_pass = $analysis->ensembl_pass; + Purpose : Getter for Ensembl password attribute + Returns : String + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub ensembl_pass { + my ($self) = @_; + return $ensembl_pass{ id $self}; +} + +=method set_ensembl_pass + + Usage : $analysis->set_ensembl_pass('secret'); + Purpose : Setter for Ensembl password attribute + Returns : undef + Parameters : String (the Ensembl password) + Throws : No exceptions + Comments : None + +=cut + +sub set_ensembl_pass { + my ( $self, $arg ) = @_; + $ensembl_pass{ id $self} = $arg; + return; +} + +=method ensembl_name + + Usage : my $ensembl_name = $analysis->ensembl_name; + Purpose : Getter for Ensembl database name attribute + Returns : String + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub ensembl_name { + my ($self) = @_; + return $ensembl_name{ id $self}; +} + +=method set_ensembl_name + + Usage : $analysis->set_ensembl_name('zv9_core'); + Purpose : Setter for Ensembl database name attribute + Returns : undef + Parameters : String (the Ensembl database name) + Throws : No exceptions + Comments : None + +=cut + +sub set_ensembl_name { + my ( $self, $arg ) = @_; + $ensembl_name{ id $self} = $arg; + return; +} + +=method ensembl_species + + Usage : my $ensembl_species = $analysis->ensembl_species; + Purpose : Getter for Ensembl species attribute + Returns : String + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub ensembl_species { + my ($self) = @_; + return $ensembl_species{ id $self}; +} + +=method set_ensembl_species + + Usage : $analysis->set_ensembl_species('danio_rerio'); + Purpose : Setter for Ensembl species attribute + Returns : undef + Parameters : String (the Ensembl species) + Throws : No exceptions + Comments : None + +=cut + +sub set_ensembl_species { + my ( $self, $arg ) = @_; + $ensembl_species{ id $self} = $arg; + return; +} + +=method slice_adaptor + + Usage : my $slice_adaptor = $analysis->slice_adaptor; + Purpose : Getter for Ensembl slice adaptor attribute + Returns : Bio::EnsEMBL::DBSQL::SliceAdaptor + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub slice_adaptor { + my ($self) = @_; + + if ( !defined $slice_adaptor{ id $self} + && ( $self->ensembl_species || $self->ensembl_name ) ) + { + # We can create an Ensembl slice adaptor + $self->_create_slice_adaptor(); + } + + return $slice_adaptor{ id $self}; +} + +=method set_slice_adaptor + + Usage : $analysis->set_slice_adaptor($slice_adaptor); + Purpose : Setter for Ensembl slice adaptor attribute + Returns : undef + Parameters : Bio::EnsEMBL::DBSQL::SliceAdaptor + Throws : No exceptions + Comments : None + +=cut + +sub set_slice_adaptor { + my ( $self, $arg ) = @_; + $slice_adaptor{ id $self} = _check_slice_adaptor($arg); + return; +} + +# Usage : $slice_adaptor = _check_slice_adaptor($slice_adaptor); +# Purpose : Check for valid Ensembl slice adaptor +# Returns : Bio::EnsEMBL::DBSQL::SliceAdaptor +# Parameters : Bio::EnsEMBL::DBSQL::SliceAdaptor +# Throws : If slice adaptor is missing or invalid (i.e. not a +# Bio::EnsEMBL::DBSQL::SliceAdaptor object) +# Comments : None +sub _check_slice_adaptor { + my ($slice_adaptor) = @_; + return $slice_adaptor + if defined $slice_adaptor + && $slice_adaptor->isa('Bio::EnsEMBL::DBSQL::SliceAdaptor'); + confess 'No Ensembl slice adaptor specified' if !defined $slice_adaptor; + confess 'Class of Ensembl slice adaptor (', ref $slice_adaptor, + ') not Bio::EnsEMBL::DBSQL::SliceAdaptor'; +} + +=method chunk_total + + Usage : my $chunk_total = $analysis->chunk_total; + Purpose : Getter for chunk total attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub chunk_total { + my ($self) = @_; + return $chunk_total{ id $self}; +} + +=method set_chunk_total + + Usage : $analysis->set_chunk_total(20); + Purpose : Setter for chunk total attribute + Returns : undef + Parameters : +ve Int (the chunk total) + Throws : No exceptions + Comments : None + +=cut + +sub set_chunk_total { + my ( $self, $arg ) = @_; + $chunk_total{ id $self} = _check_chunk_total($arg); + + # Recalculate chunks if necessary + if ( scalar @{ $self->get_all_samples() } ) { + $self->add_all_chunks(); + } + + return; +} + +# Usage : $chunk_total = _check_chunk_total($chunk_total); +# Purpose : Check for valid chunk total +# Returns : +ve Int (the valid chunk total) +# Parameters : +ve Int (the chunk total) +# Throws : If chunk total is missing or not a positive integer +# Comments : None +sub _check_chunk_total { + my ($chunk_total) = @_; + return $chunk_total + if defined $chunk_total && $chunk_total =~ m/\A \d+ \z/xms; + confess 'No chunk total specified' if !defined $chunk_total; + confess "Invalid chunk total ($chunk_total) specified"; +} + +=method test_chunk + + Usage : my $test_chunk = $analysis->test_chunk; + Purpose : Getter for test chunk attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub test_chunk { + my ($self) = @_; + return $test_chunk{ id $self}; +} + +=method set_test_chunk + + Usage : $analysis->set_test_chunk(1); + Purpose : Setter for test chunk attribute + Returns : undef + Parameters : +ve Int (the test chunk) + Throws : No exceptions + Comments : None + +=cut + +sub set_test_chunk { + my ( $self, $arg ) = @_; + $test_chunk{ id $self} = $arg; + return; +} + +=method add_all_chunks + + Usage : $analysis->add_all_chunks(); + Purpose : Add all chunks (groups of sequences) to an analysis + Returns : undef + Parameters : None + Throws : No exceptions + Comments : Groups all sequences into a specific number of (roughly equally + sized) chunks + +=cut + +sub add_all_chunks { + my ($self) = @_; + + my @seqs = @{ $self->get_all_sequences() }; + + # Get total sequence length + my $total_bp = 0; + foreach my $seq (@seqs) { + $total_bp += $seq->bp; + } + + # Get chunk target size (+ 1 to ensure slight overestimate) + my $target_chunk_size = int( $total_bp / $self->chunk_total + 1 ); + + my @chunks; + my @chunk_size = map { 0 } 1 .. $self->chunk_total; + + # Iterate over sequences + SEQ: foreach my $seq (@seqs) { + + # Iterate over each chunk + foreach my $chunk_index ( 0 .. $self->chunk_total - 1 ) { + + # Add sequence to chunk if there's room or if the chunk is empty + if ( $chunk_size[$chunk_index] + $seq->bp <= $target_chunk_size + || $chunk_size[$chunk_index] == 0 ) + { + push @{ $chunks[$chunk_index] }, $seq; + $chunk_size[$chunk_index] += $seq->bp; + next SEQ; # Next sequence + } + } + + # Sequence hasn't been added to a chunk, so add to chunk with most room + my $roomy_chunk_index = 0; + foreach my $chunk_index ( 0 .. $self->chunk_total - 1 ) { + if ( $chunk_size[$chunk_index] < $chunk_size[$roomy_chunk_index] ) { + $roomy_chunk_index = $chunk_index; + } + } + push @{ $chunks[$roomy_chunk_index] }, $seq; + $chunk_size[$roomy_chunk_index] += $seq->bp; + } + + # Iterate over empty chunks in order to attempt to add sequences to them + foreach my $empty_chunk_index ( 0 .. $self->chunk_total - 1 ) { + next if defined $chunks[$empty_chunk_index]; # Only want empty chunks + + # Find chunk with highest number of sequences (but more than one) + my $max_seqs_chunk_index; + my $max_seqs; + foreach my $chunk_index ( 0 .. $self->chunk_total - 1 ) { + next if !defined $chunks[$chunk_index]; # Only want non-empty chunks + my $seqs = scalar @{ $chunks[$chunk_index] }; + if ( $seqs > 1 && ( !defined $max_seqs || $seqs > $max_seqs ) ) { + $max_seqs_chunk_index = $chunk_index; + $max_seqs = $seqs; + } + } + + last if !defined $max_seqs; # No splittable chunks + + # Split chosen chunk into empty chunk + my $split_index = int( $max_seqs / 2 ); + @{ $chunks[$empty_chunk_index] } = + splice @{ $chunks[$max_seqs_chunk_index] }, 0, $split_index; + } + + $chunk{ id $self} = \@chunks; + + # Number of chunks may be smaller than requested chunk total, so adjust + $chunk_total{ id $self} = scalar @chunks; + + return; +} + +=method get_all_chunks + + Usage : $chunks = $analysis->get_all_chunks(); + Purpose : Get all chunks (groups of sequences) of an analysis + Returns : Arrayref of arrayrefs of DETCT::Sequence objects + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub get_all_chunks { + my ($self) = @_; + + my $chunks = $chunk{ id $self} || []; + + # If a test chunk is specified then only return that chunk not all chunks + if ( $self->test_chunk && exists $chunks->[ $self->test_chunk - 1 ] ) { + $chunks = [ $chunks->[ $self->test_chunk - 1 ] ]; + } + + return $chunks; +} + +=method list_all_bam_files + + Usage : @bam_files = $analysis->list_all_bam_files(); + Purpose : Get all BAM files used in an analysis + Returns : Arrayref of strings + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub list_all_bam_files { + my ($self) = @_; + + my $samples = $self->get_all_samples(); + + my @bam_files = map { $_->bam_file } @{$samples}; + + return uniq( sort @bam_files ); +} + +=method list_all_tags_by_bam_file + + Usage : @tags = $analysis->list_all_tags_by_bam_file(); + Purpose : Get all tags used in an analysis in a particular BAM file + Returns : Arrayref of strings + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub list_all_tags_by_bam_file { + my ( $self, $bam_file ) = @_; + + my $samples = $self->get_all_samples(); + + my @tags = map { $_->tag } grep { $_->bam_file eq $bam_file } @{$samples}; + + return uniq( sort @tags ); +} + +=method get_subsequence + + Usage : $seq = $analysis->get_subsequence('1', 1, 10); + Purpose : Get subsequence from reference + Returns : String (sequence) + Parameters : String (the sequence name) + Int (the sequence start) + Int (the sequence end) + Int (the sequence strand) + Throws : If sequence name is missing + If sequence start is missing + If sequence end is missing + If sequence strand is missing + Comments : None + +=cut + +sub get_subsequence { + my ( $self, $seq_name, $start, $end, $strand ) = @_; + + confess 'No sequence name specified' if !defined $seq_name; + confess 'No sequence start specified' if !defined $start; + confess 'No sequence end specified' if !defined $end; + confess 'No sequence strand specified' if !defined $strand; + + # Avoid negative positions (but don't worry if end is larger than sequence) + if ( $start < 1 ) { + $start = 1; + } + if ( $end < 1 ) { + $end = 1; + } + + my $subseq; + + if ( $self->fasta_index ) { + $subseq = DETCT::Misc::BAM::get_sequence( + { + fasta_index => $self->fasta_index, + seq_name => $seq_name, + start => $start, + end => $end, + strand => $strand, + } + ); + } + elsif ( $self->slice_adaptor ) { + $subseq = + $self->slice_adaptor->fetch_by_region( 'toplevel', $seq_name, $start, + $end, $strand )->seq; + } + else { + confess 'No reference FASTA or Ensembl database'; + } + + return uc $subseq; +} + +# Usage : $self->_create_slice_adaptor(); +# Purpose : Create an Ensembl slice adaptor +# Returns : Undef +# Parameters : None +# Throws : No exceptions +# Comments : None +sub _create_slice_adaptor { + my ($self) = @_; + + my $host = + $self->ensembl_host ? $self->ensembl_host : $DEFAULT_ENSEMBL_HOST; + my $port = $self->ensembl_port; + my $user = + $self->ensembl_user ? $self->ensembl_user : $DEFAULT_ENSEMBL_USER; + my $pass = $self->ensembl_pass; + my $slice_adaptor; + if ( !$self->ensembl_name ) { + + # Get slice adaptor via registry + require Bio::EnsEMBL::Registry; + Bio::EnsEMBL::Registry->load_registry_from_db( + -host => $host, + -port => $port, + -user => $user, + -pass => $pass, + -species => $self->ensembl_species, + ); + $slice_adaptor = + Bio::EnsEMBL::Registry->get_adaptor( $self->ensembl_species, 'core', + 'slice' ); + } + else { + # Get slice adaptor from specific database + require Bio::EnsEMBL::DBSQL::DBAdaptor; + my $ensembl_db = Bio::EnsEMBL::DBSQL::DBAdaptor->new( + -host => $host, + -port => $port, + -user => $user, + -pass => $pass, + -dbname => $self->ensembl_name, + ); + $slice_adaptor = $ensembl_db->get_SliceAdaptor(); + } + + $self->set_slice_adaptor($slice_adaptor); + + return; +} + +1; diff --git a/lib/DETCT/Gene.pm b/lib/DETCT/Gene.pm new file mode 100644 index 0000000..4bc5d88 --- /dev/null +++ b/lib/DETCT/Gene.pm @@ -0,0 +1,574 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Gene; +## use critic + +# ABSTRACT: Object representing a gene + +## Author : is1 +## Maintainer : is1 +## Created : 2012-11-24 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Readonly; +use Class::InsideOut qw( private register id ); +use Scalar::Util qw( weaken ); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Attributes: +private genebuild_version => my %genebuild_version; # e.g. e69 +private stable_id => my %stable_id; # e.g. ENSDARG00000095747 +private name => my %name; # e.g. cxc64 +private description => my %description; # e.g. CXC chemokine 64... +private biotype => my %biotype; # e.g. protein_coding +private seq_name => my %seq_name; # e.g. 5 +private start => my %start; # e.g. 40352744 +private end => my %end; # e.g. 40354399 +private strand => my %strand; # e.g. 1 +private transcript => my %transcript; # DETCT::Transcript + +# Constants +Readonly our $MAX_NAME_LENGTH => 128; + +=method new + + Usage : my $gene = DETCT::Gene->new( { + genebuild_version => 'e61', + stable_id => 'ENSDARG00000095747', + biotype => 'protein_coding', + seq_name => '5', + start => 40352744, + end => 40354399, + strand => 1, + } ); + Purpose : Constructor for gene objects + Returns : DETCT::Gene + Parameters : Hashref { + genebuild_version => String, + stable_id => String, + name => String or undef, + description => String or undef, + biotype => String, + seq_name => String, + start => +ve Int, + end => +ve Int, + strand => Int (1 or -1), + } + Throws : No exceptions + Comments : None + +=cut + +sub new { + my ( $class, $arg_ref ) = @_; + my $self = register($class); + $self->set_genebuild_version( $arg_ref->{genebuild_version} ); + $self->set_stable_id( $arg_ref->{stable_id} ); + $self->set_name( $arg_ref->{name} ); + $self->set_description( $arg_ref->{description} ); + $self->set_biotype( $arg_ref->{biotype} ); + $self->set_seq_name( $arg_ref->{seq_name} ); + $self->set_start( $arg_ref->{start} ); + $self->set_end( $arg_ref->{end} ); + $self->set_strand( $arg_ref->{strand} ); + return $self; +} + +=method genebuild_version + + Usage : my $gv = $gene->genebuild_version; + Purpose : Getter for genebuild version attribute + Returns : String (e.g. "e61" for Ensembl 61) + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub genebuild_version { + my ($self) = @_; + return $genebuild_version{ id $self}; +} + +=method set_genebuild_version + + Usage : $gene->set_genebuild_version('e61'); + Purpose : Setter for genebuild version attribute + Returns : undef + Parameters : String (the genebuild version) + Throws : No exceptions + Comments : None + +=cut + +sub set_genebuild_version { + my ( $self, $arg ) = @_; + $genebuild_version{ id $self} = check_genebuild_version($arg); + return; +} + +=method check_genebuild_version + + Usage : $gv = check_genebuild_version($gv); + Purpose : Check for valid genebuild version + Returns : String (the valid genebuild version) + Parameters : String (the genebuild version) + Throws : If genebuild version is missing or invalid (i.e. not + alphanumeric) + Comments : None + +=cut + +sub check_genebuild_version { + my ($genebuild_version) = @_; + return $genebuild_version + if defined $genebuild_version && $genebuild_version =~ m/\A \w+ \z/xms; + confess 'No genebuild version specified' if !defined $genebuild_version; + confess "Invalid genebuild version ($genebuild_version) specified"; +} + +=method stable_id + + Usage : my $stable_id = $gene->stable_id; + Purpose : Getter for stable id attribute + Returns : String (e.g. "ENSDARG00000095747") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub stable_id { + my ($self) = @_; + return $stable_id{ id $self}; +} + +=method set_stable_id + + Usage : $gene->set_stable_id('ENSDARG00000095747'); + Purpose : Setter for stable id attribute + Returns : undef + Parameters : String (the stable id) + Throws : No exceptions + Comments : None + +=cut + +sub set_stable_id { + my ( $self, $arg ) = @_; + $stable_id{ id $self} = check_stable_id($arg); + return; +} + +=method check_stable_id + + Usage : $stable_id = check_stable_id($stable_id); + Purpose : Check for valid stable id + Returns : String (the valid stable id) + Parameters : String (the stable id) + Throws : If stable id is missing or invalid + Comments : None + +=cut + +sub check_stable_id { + my ($stable_id) = @_; + return $stable_id + if defined $stable_id && $stable_id =~ m/\A [[:upper:]]+ \d{11} \z/xms; + confess 'No stable id specified' if !defined $stable_id; + confess "Invalid stable id ($stable_id) specified"; +} + +=method name + + Usage : my $name = $gene->name; + Purpose : Getter for name attribute + Returns : String (e.g. "cxc64") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub name { + my ($self) = @_; + return $name{ id $self}; +} + +=method set_name + + Usage : $gene->set_name('cxc64'); + Purpose : Setter for name attribute + Returns : undef + Parameters : String (the name) + Throws : No exceptions + Comments : None + +=cut + +sub set_name { + my ( $self, $arg ) = @_; + $name{ id $self} = _check_name($arg); + return; +} + +# Usage : $name = _check_name($name); +# Purpose : Check for valid name +# Returns : String (the valid name) +# Parameters : String (the name) +# Throws : If name > $MAX_NAME_LENGTH characters +# Comments : None +sub _check_name { + my ($name) = @_; + return $name + if !defined $name + || ( length $name > 0 && length $name <= $MAX_NAME_LENGTH ); + confess 'Name is empty' if !length $name; + confess "Name ($name) longer than $MAX_NAME_LENGTH characters"; +} + +=method description + + Usage : my $description = $gene->description; + Purpose : Getter for description attribute + Returns : String (e.g. "CXC chemokine 64") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub description { + my ($self) = @_; + return $description{ id $self}; +} + +=method set_description + + Usage : $gene->set_description('CXC chemokine 64'); + Purpose : Setter for description attribute + Returns : undef + Parameters : String (the description) + Throws : No exceptions + Comments : None + +=cut + +sub set_description { + my ( $self, $arg ) = @_; + $description{ id $self} = $arg; + return; +} + +=method biotype + + Usage : my $biotype = $gene->biotype; + Purpose : Getter for biotype attribute + Returns : String (e.g. "protein_coding") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub biotype { + my ($self) = @_; + return $biotype{ id $self}; +} + +=method set_biotype + + Usage : $gene->set_biotype('protein_coding'); + Purpose : Setter for biotype attribute + Returns : undef + Parameters : String (the biotype) + Throws : No exceptions + Comments : None + +=cut + +sub set_biotype { + my ( $self, $arg ) = @_; + $biotype{ id $self} = check_biotype($arg); + return; +} + +=method check_biotype + + Usage : $biotype = check_biotype($biotype); + Purpose : Check for valid biotype + Returns : String (the valid biotype) + Parameters : String (the biotype) + Throws : If biotype is missing or invalid (i.e. not alphanumeric) + Comments : None + +=cut + +sub check_biotype { + my ($biotype) = @_; + return $biotype if defined $biotype && $biotype =~ m/\A \w+ \z/xms; + confess 'No biotype specified' if !defined $biotype; + confess "Invalid biotype ($biotype) specified"; +} + +=method seq_name + + Usage : my $seq_name = $gene->seq_name; + Purpose : Getter for sequence name attribute + Returns : String (e.g. "5") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub seq_name { + my ($self) = @_; + return $seq_name{ id $self}; +} + +=method set_seq_name + + Usage : $gene->set_seq_name('5'); + Purpose : Setter for sequence name attribute + Returns : undef + Parameters : String (the sequence name) + Throws : No exceptions + Comments : None + +=cut + +sub set_seq_name { + my ( $self, $arg ) = @_; + $seq_name{ id $self} = check_seq_name($arg); + return; +} + +=method check_seq_name + + Usage : $seq_name = check_seq_name($seq_name); + Purpose : Check for valid sequence name + Returns : String (the valid sequence name) + Parameters : String (the sequence name) + Throws : If sequence name is missing or invalid (i.e. not alphanumeric) + Comments : None + +=cut + +sub check_seq_name { + my ($seq_name) = @_; + return $seq_name if defined $seq_name && $seq_name =~ m/\A \w+ \z/xms; + confess 'No sequence name specified' if !defined $seq_name; + confess "Invalid sequence name ($seq_name) specified"; +} + +=method start + + Usage : my $start = $gene->start; + Purpose : Getter for start attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub start { + my ($self) = @_; + return $start{ id $self}; +} + +=method set_start + + Usage : $gene->set_start(40352744); + Purpose : Setter for start attribute + Returns : undef + Parameters : +ve Int (the start) + Throws : No exceptions + Comments : None + +=cut + +sub set_start { + my ( $self, $arg ) = @_; + $start{ id $self} = check_start($arg); + return; +} + +=method check_start + + Usage : $start = check_start($start); + Purpose : Check for valid start + Returns : +ve Int (the valid start) + Parameters : +ve Int (the start) + Throws : If start is missing or not a positive integer + Comments : None + +=cut + +sub check_start { + my ($start) = @_; + return $start if defined $start && $start =~ m/\A \d+ \z/xms; + confess 'No start specified' if !defined $start; + confess "Invalid start ($start) specified"; +} + +=method end + + Usage : my $end = $gene->end; + Purpose : Getter for end attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub end { + my ($self) = @_; + return $end{ id $self}; +} + +=method set_end + + Usage : $gene->set_end(40352744); + Purpose : Setter for end attribute + Returns : undef + Parameters : +ve Int (the end) + Throws : No exceptions + Comments : None + +=cut + +sub set_end { + my ( $self, $arg ) = @_; + $end{ id $self} = check_end($arg); + return; +} + +=method check_end + + Usage : $end = check_end($end); + Purpose : Check for valid end + Returns : +ve Int (the valid end) + Parameters : +ve Int (the end) + Throws : If end is missing or not a positive integer + Comments : None + +=cut + +sub check_end { + my ($end) = @_; + return $end if defined $end && $end =~ m/\A \d+ \z/xms; + confess 'No end specified' if !defined $end; + confess "Invalid end ($end) specified"; +} + +=method strand + + Usage : my $strand = $gene->strand; + Purpose : Getter for strand attribute + Returns : Int (1 or -1) + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub strand { + my ($self) = @_; + return $strand{ id $self}; +} + +=method set_strand + + Usage : $gene->set_strand(1); + Purpose : Setter for strand attribute + Returns : undef + Parameters : Int (the strand) + Throws : No exceptions + Comments : None + +=cut + +sub set_strand { + my ( $self, $arg ) = @_; + $strand{ id $self} = _check_strand($arg); + return; +} + +# Usage : $strand = _check_strand($strand); +# Purpose : Check for valid strand +# Returns : Int (1 or -1) (the valid strand) +# Parameters : Int (1 or -1) (the strand) +# Throws : If strand is missing or not 1 or -1 +# Comments : None +sub _check_strand { + my ($strand) = @_; + return $strand if defined $strand && $strand =~ m/\A \-? 1 \z/xms; + confess 'No strand specified' if !defined $strand; + confess "Invalid strand ($strand) specified"; +} + +=method add_transcript + + Usage : $gene->add_transcript($transcript); + Purpose : Add a transcript to a gene + Returns : undef + Parameters : DETCT::Transcript + Throws : If transcript is missing or invalid (i.e. not a + DETCT::Transcript object) + Comments : None + +=cut + +sub add_transcript { + my ( $self, $transcript ) = @_; + + confess 'No transcript specified' if !defined $transcript; + confess 'Class of transcript (', ref $transcript, ') not DETCT::Transcript' + if !$transcript->isa('DETCT::Transcript'); + + weaken($transcript); # Avoid circular references + + if ( !exists $transcript{ id $self} ) { + $transcript{ id $self} = [$transcript]; + } + else { + push @{ $transcript{ id $self} }, $transcript; + } + + return; +} + +=method get_all_transcripts + + Usage : $transcripts = $gene->get_all_transcripts(); + Purpose : Get all transcripts of a gene + Returns : Arrayref of DETCT::Transcript objects + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub get_all_transcripts { + my ($self) = @_; + + return $transcript{ id $self} || []; +} + +1; diff --git a/lib/DETCT/GeneFinder.pm b/lib/DETCT/GeneFinder.pm new file mode 100644 index 0000000..33269e4 --- /dev/null +++ b/lib/DETCT/GeneFinder.pm @@ -0,0 +1,417 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::GeneFinder; +## use critic + +# ABSTRACT: Object for finding genes (and transcripts) by location + +## Author : is1 +## Maintainer : is1 +## Created : 2012-11-24 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Class::InsideOut qw( private register id ); +use DETCT::Gene; +use DETCT::Transcript; + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Attributes: +private slice_adaptor => my %slice_adaptor; # Bio::EnsEMBL::DBSQL::SliceAdaptor +private cache => my %cache; # Hashref + +=method new + + Usage : my $gene_finder = DETCT::GeneFinder->new( { + slice_adaptor => $slice_adaptor, + } ); + Purpose : Constructor for gene finder objects + Returns : DETCT::GeneFinder + Parameters : Hashref { + slice_adaptor => Bio::EnsEMBL::DBSQL::SliceAdaptor, + } + Throws : No exceptions + Comments : None + +=cut + +sub new { + my ( $class, $arg_ref ) = @_; + my $self = register($class); + $self->set_slice_adaptor( $arg_ref->{slice_adaptor} ); + return $self; +} + +=method slice_adaptor + + Usage : my $slice_adaptor = $analysis->slice_adaptor; + Purpose : Getter for Ensembl slice adaptor attribute + Returns : Bio::EnsEMBL::DBSQL::SliceAdaptor + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub slice_adaptor { + my ($self) = @_; + return $slice_adaptor{ id $self}; +} + +=method set_slice_adaptor + + Usage : $analysis->set_slice_adaptor($slice_adaptor); + Purpose : Setter for Ensembl slice adaptor attribute + Returns : undef + Parameters : Bio::EnsEMBL::DBSQL::SliceAdaptor + Throws : No exceptions + Comments : None + +=cut + +sub set_slice_adaptor { + my ( $self, $arg ) = @_; + $slice_adaptor{ id $self} = _check_slice_adaptor($arg); + return; +} + +# Usage : $slice_adaptor = _check_slice_adaptor($slice_adaptor); +# Purpose : Check for valid Ensembl slice adaptor +# Returns : Bio::EnsEMBL::DBSQL::SliceAdaptor +# Parameters : Bio::EnsEMBL::DBSQL::SliceAdaptor +# Throws : If slice adaptor is missing or invalid (i.e. not a +# Bio::EnsEMBL::DBSQL::SliceAdaptor object) +# Comments : None +sub _check_slice_adaptor { + my ($slice_adaptor) = @_; + return $slice_adaptor + if defined $slice_adaptor + && $slice_adaptor->isa('Bio::EnsEMBL::DBSQL::SliceAdaptor'); + confess 'No Ensembl slice adaptor specified' if !defined $slice_adaptor; + confess 'Class of Ensembl slice adaptor (', ref $slice_adaptor, + ') not Bio::EnsEMBL::DBSQL::SliceAdaptor'; +} + +=method get_nearest_transcripts + + Usage : $gene_finder->get_nearest_transcripts($seq_name, $pos, $strand); + Purpose : Retrieve the nearest transcripts to a 3' end + Returns : Arrayref (of DETCT::Transcript objects) + Int (distance) + Int (nearest 3' end position) + Parameters : String (the 3' end sequence name) + Int (the 3' end position) + Int (the 3' end strand) + Throws : No exceptions + Comments : Distance is positive if downstream of 3' end and negative if + upstream + +=cut + +sub get_nearest_transcripts { + my ( $self, $seq_name, $pos, $strand ) = @_; + + # Ensure cache is filled + $self->_fill_cache_from_ensembl($seq_name); + + my $nearest_distance; + my $nearest_transcripts = []; + my $nearest_end_pos; + + # Iterate over all 3' end transcript positions in relevant portion of cache + my @transcript_positions = keys %{ $cache{ id $self}->{$seq_name} }; + + # Favour upstream if get two transcripts same distance upstream and + # downstream (and strand is known) + @transcript_positions = sort { $a <=> $b } @transcript_positions; + ## no critic (ProhibitMagicNumbers) + if ( defined $strand && $strand == -1 ) { + ## use critic + @transcript_positions = reverse @transcript_positions; + } + + foreach my $transcript_position (@transcript_positions) { + my @transcripts = + @{ $cache{ id $self}->{$seq_name}->{$transcript_position} }; + + # Only consider transcripts matching strand (if specified) + if ( defined $strand ) { + @transcripts = grep { $_->strand == $strand } @transcripts; + } + next if !@transcripts; + + my $distance = $pos - $transcript_position; + ## no critic (ProhibitMagicNumbers) + if ( $transcripts[0]->strand == -1 ) { + ### use critic + $distance = -$distance; + } + + # Keep transcripts if nearer than seen before + if ( !defined $nearest_distance + || abs $distance < abs $nearest_distance ) + { + $nearest_transcripts = \@transcripts; + $nearest_distance = $distance; + $nearest_end_pos = $transcript_position; + } + } + + # Sort by stable id + @{$nearest_transcripts} = + sort { $a->stable_id cmp $b->stable_id } @{$nearest_transcripts}; + + return $nearest_transcripts, $nearest_distance, $nearest_end_pos; +} + +=method get_nearest_genes + + Usage : $gene_finder->get_nearest_genes($seq_name, $pos, $strand); + Purpose : Retrieve the nearest genes to a 3' end + Returns : Arrayref (of DETCT::Gene objects) + Int (distance) + Int (nearest 3' end position) + Parameters : String (the 3' end sequence name) + Int (the 3' end position) + Int (the 3' end strand) + Throws : No exceptions + Comments : Distance is positive if downstream of 3' end and negative if + upstream + +=cut + +sub get_nearest_genes { + my ( $self, $seq_name, $pos, $strand ) = @_; + + my ( $transcripts, $distance, $nearest_end_pos ) = + $self->get_nearest_transcripts( $seq_name, $pos, $strand ); + + my %tmp_cache; # Temporarily store genes by stable id + + # Get all genes corresponding to these transcripts + foreach my $transcript ( @{$transcripts} ) { + $tmp_cache{ $transcript->gene->stable_id } = $transcript->gene; + } + + my $nearest_genes = [ values %tmp_cache ]; + + # Sort by stable id + @{$nearest_genes} = + sort { $a->stable_id cmp $b->stable_id } @{$nearest_genes}; + + return $nearest_genes, $distance, $nearest_end_pos; +} + +# Usage : $self->_fill_cache_from_ensembl( $seq_name ); +# Purpose : Fill the cache from Ensembl for a particular sequence +# Returns : undef +# Parameters : String (the sequence name) +# Throws : No exceptions +# Comments : Cache is a hashref (keyed by sequence name) of hashrefs (keyed +# by 3' end position) of arrayrefs of transcripts + +sub _fill_cache_from_ensembl { + my ( $self, $seq_name ) = @_; + + # Skip if cache already filled + return if exists $cache{ id $self}->{$seq_name}; + + # Make sure default key exists (in case there are no genes) + $cache{ id $self}->{$seq_name} = {}; + + my $slice = $self->slice_adaptor->fetch_by_region( 'toplevel', $seq_name ); + + require Bio::EnsEMBL::ApiVersion; + my $genebuild_version = 'e' . Bio::EnsEMBL::ApiVersion::software_version(); + + my $ens_genes = $slice->get_all_Genes( undef, undef, 1 ); # Plus transcripts + foreach my $ens_gene ( @{$ens_genes} ) { + my $gene = DETCT::Gene->new( + { + genebuild_version => $genebuild_version, + stable_id => $ens_gene->stable_id, + name => $ens_gene->external_name, + description => $ens_gene->description, + biotype => $ens_gene->biotype, + seq_name => $seq_name, + start => $ens_gene->seq_region_start, + end => $ens_gene->seq_region_end, + strand => $ens_gene->seq_region_strand, + } + ); + + # Get 3' end position for each transcript + my $ens_transcripts = $ens_gene->get_all_Transcripts(); + foreach my $ens_transcript ( @{$ens_transcripts} ) { + my $transcript = DETCT::Transcript->new( + { + stable_id => $ens_transcript->stable_id, + name => $ens_transcript->external_name, + description => $ens_transcript->description, + biotype => $ens_transcript->biotype, + seq_name => $seq_name, + start => $ens_transcript->seq_region_start, + end => $ens_transcript->seq_region_end, + strand => $ens_transcript->seq_region_strand, + gene => $gene, + } + ); + $gene->add_transcript($transcript); + + my $pos = + $ens_transcript->seq_region_strand == 1 + ? $ens_transcript->seq_region_end + : $ens_transcript->seq_region_start; + + push @{ $cache{ id $self}->{$seq_name}->{$pos} }, $transcript; + } + } + + return; +} + +=method add_gene_annotation + + Usage : my $regions_ref + = $gene_finder->add_gene_annotation($regions_ary_ref); + Purpose : Add gene annotation to regions with 3' ends + Returns : Arrayref [ + Arrayref [ + String (region sequence name), + Int (region start), + Int (region end), + Int (region maximum read count), + Float (region log probability sum), + String (3' end sequence name) or undef, + Int (3' end position) or undef, + Int (3' end strand) or undef, + Int (3' end read count) or undef, + Arrayref [ + Int (count) + ... + ], + Arrayref [ + Int (normalised count) + ... + ], + Int (p value) or undef, + Int (adjusted p value) or undef, + Arrayref [ + Int (condition fold change) or undef, + Int (log2 condition fold change) or undef, + ], + Arrayref [ + Arrayref [ + Int (group fold change) or undef, + Int (log2 group fold change) or undef, + ], + ... (groups) + ], + Hashref { + String (genebuild version) => Arrayref [ + Arrayref [ + String (gene stable id), + String (gene name) or undef, + String (gene description) or undef, + String (gene biotype), + Int (distance to 3' end), + Arrayref [ + Arrayref [ + String (transcript stable id), + String (transcript biotype), + ], + ... (transcripts) + ], + ], + ... (genes) + ], + } + ], + ... (regions) + } + Parameters : Arrayref (of regions) + Throws : If regions are missing + Comments : None + +=cut + +sub add_gene_annotation { + my ( $self, $regions ) = @_; + + confess 'No regions specified' if !defined $regions; + + my @output; + + foreach my $region ( @{$regions} ) { + + # Get details for region and 3' end + my $region_seq_name = $region->[0]; + my $region_start = $region->[1]; + my $region_end = $region->[2]; + ## no critic (ProhibitMagicNumbers) + my $three_prime_seq_name = $region->[5]; + my $three_prime_pos = $region->[6]; + my $three_prime_strand = $region->[7]; + ## use critic + + my %gene_annotation = (); + my $genes; + my $distance; + my $nearest_end_pos; + + if ( defined $three_prime_seq_name ) { + + # Find nearest genes to 3' end (taking strand into account) + ( $genes, $distance, $nearest_end_pos ) = + $self->get_nearest_genes( $three_prime_seq_name, $three_prime_pos, + $three_prime_strand ); + } + + # Add annotation if got genes + foreach my $gene ( @{$genes} ) { + my @transcripts; + foreach my $transcript ( @{ $gene->get_all_transcripts() } ) { + + # Only add those transcripts nearest to 3' end + ## no critic (ProhibitMagicNumbers) + if ( + ( + $transcript->strand == 1 + && $transcript->end == $nearest_end_pos + ) + || ( $transcript->strand == -1 + && $transcript->start == $nearest_end_pos ) + ) + { + ## use critic + push @transcripts, + [ $transcript->stable_id, $transcript->biotype, ]; + } + } + push @{ $gene_annotation{ $gene->genebuild_version } }, + [ + $gene->stable_id, $gene->name, $gene->description, + $gene->biotype, $distance, \@transcripts, + ]; + } + + push @{$region}, \%gene_annotation; + push @output, $region; + } + + return \@output; +} + +1; diff --git a/lib/DETCT/Misc/BAM.pm b/lib/DETCT/Misc/BAM.pm new file mode 100644 index 0000000..ae059a0 --- /dev/null +++ b/lib/DETCT/Misc/BAM.pm @@ -0,0 +1,1332 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Misc::BAM; +## use critic + +# ABSTRACT: Miscellaneous functions for interacting with BAM files + +## Author : is1 +## Maintainer : is1 +## Created : 2012-09-20 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Readonly; +use Bio::DB::Sam; +use List::Util qw( min ); +use Data::Compare; +use DETCT::Misc::Tag; + +use base qw( Exporter ); +our @EXPORT_OK = qw( + get_reference_sequence_lengths + get_sequence + count_tags + bin_reads + get_read_peaks + get_three_prime_ends + merge_three_prime_ends + filter_three_prime_ends + choose_three_prime_end + count_reads + merge_read_counts +); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Constants + +# Regexps for checking for polyA +Readonly our @POLYA_REGEXP => ( + qr/\A AAA.AAA... \z/xms, + qr/\A AAA.AA.A.. \z/xms, + qr/\A AAA.A.AA.. \z/xms, + qr/\A AA.AAAA... \z/xms, + qr/\A AA.AAA.A.. \z/xms, + qr/\A AA.A.AAA.. \z/xms, + qr/\A A.AAAAA... \z/xms, + qr/\A A.AAAA.A.. \z/xms, + qr/\A A.AAA.AA.. \z/xms, + qr/\A A.AA.AAA.. \z/xms, + qr/\A A.A.AAAA.. \z/xms, + qr/\A AA.AA.AA.. \z/xms, +); + +=func get_reference_sequence_lengths + + Usage : my %length_of + = DETCT::Misc::BAM::get_reference_sequence_lengths($bam_file); + Purpose : Get length of each reference sequence from a BAM file + Returns : Hash ( + seq_region => length + ) + Parameters : String (the BAM file) + Throws : If BAM file is missing + Comments : None + +=cut + +sub get_reference_sequence_lengths { + my ($bam_file) = @_; + + confess 'No BAM file specified' if !defined $bam_file; + + my $sam = Bio::DB::Sam->new( -bam => $bam_file ); + + my %length_of; + + foreach my $seq_id ( $sam->seq_ids ) { + $length_of{$seq_id} = $sam->length($seq_id); + } + + return %length_of; +} + +=func get_sequence + + Usage : my $seq = DETCT::Misc::BAM::get_sequence( { + fasta_index => $fai, + seq_name => '1', + start => 1, + end => 1000, + strand => 1, + } ); + Purpose : Get sequence from FASTA file + Returns : String (sequence) + Parameters : Hashref { + fasta_index => Bio::DB::Sam::Fai + ref_fasta => String (the FASTA file) + seq_name => String (the sequence name) + start => Int (the sequence start) + end => Int (the sequence end) + strand => Int (the sequence strand) + } + Throws : If FASTA index and file are both missing + If sequence name is missing + If sequence start is missing + If sequence end is missing + If sequence strand is missing + Comments : None + +=cut + +sub get_sequence { + my ($arg_ref) = @_; + + confess 'No FASTA index or FASTA file specified' + if !defined $arg_ref->{fasta_index} && !defined $arg_ref->{ref_fasta}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No sequence start specified' if !defined $arg_ref->{start}; + confess 'No sequence end specified' if !defined $arg_ref->{end}; + confess 'No sequence strand specified' if !defined $arg_ref->{strand}; + + my $fai = + $arg_ref->{fasta_index} + ? $arg_ref->{fasta_index} + : Bio::DB::Sam::Fai->load( $arg_ref->{ref_fasta} ); + + my $query = sprintf '%s:%d-%d', $arg_ref->{seq_name}, $arg_ref->{start}, + $arg_ref->{end}; + + my $seq = uc $fai->fetch($query); + + if ( $arg_ref->{strand} == -1 ) { ## no critic (ProhibitMagicNumbers) + $seq = reverse $seq; + $seq =~ tr/ACGT/TGCA/; + } + + return $seq; +} + +=func count_tags + + Usage : my $count_ref = DETCT::Misc::BAM::count_tags( { + bam_file => $bam_file, + mismatch_threshold => 2, + seq_name => '1', + start => 1, + end => 1000, + tags => ['NNNNBGAGGC', 'NNNNBAGAAG'], + } ); + Purpose : Count tags and random bases in a BAM file + Returns : Hashref { + String (tag) => Hashref { + String (random bases) => Int (count) + } + } + Parameters : Hashref { + bam_file => String (the BAM file) + mismatch_threshold => Int (the mismatch threshold) + seq_name => String (the sequence name) + start => Int (the start) or undef + end => Int (the end) or undef + tags => Arrayref of strings (the tags) + } + Throws : If BAM file is missing + If mismatch threshold is missing + If tags are missing + Comments : None + +=cut + +sub count_tags { + my ($arg_ref) = @_; + + confess 'No BAM file specified' if !defined $arg_ref->{bam_file}; + confess 'No mismatch threshold specified' + if !defined $arg_ref->{mismatch_threshold}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No tags specified' if !defined $arg_ref->{tags}; + + my @tags = @{ $arg_ref->{tags} }; + + # Convert tags to regular expressions + my %re_for = DETCT::Misc::Tag::convert_tag_to_regexp(@tags); + + # Count random bases per tag + my %random_count_for; + foreach my $tag (@tags) { + my @random = $tag =~ m/[NRYKMSWBDHV]/xmsg; + $random_count_for{$tag} = scalar @random; + } + + my $sam = Bio::DB::Sam->new( -bam => $arg_ref->{bam_file} ); + + my %count; + + # Callback for filtering + my $callback = sub { + my ($alignment) = @_; + return if !is_read2($alignment); + return if is_duplicate($alignment); + return if $alignment->unmapped; + return + if above_mismatch_threshold( $alignment, + $arg_ref->{mismatch_threshold} ); + + # Match tag + my ($tag_in_read) = $alignment->query->name =~ m/[#] ([AGCT]+) \z/xmsg; + return if !$tag_in_read; + TAG: foreach my $tag ( sort keys %re_for ) { + my $regexps = $re_for{$tag}; + foreach my $re ( @{$regexps} ) { + if ( $tag_in_read =~ $re ) { + my $random = substr $tag_in_read, 0, + $random_count_for{$tag}; + $count{$tag}{$random}++; + last TAG; + } + } + } + + return; + }; + + # Construct region + my $region = $arg_ref->{seq_name}; + if ( exists $arg_ref->{start} ) { + $region .= q{:} . $arg_ref->{start}; + if ( exists $arg_ref->{end} ) { + $region .= q{-} . $arg_ref->{end}; + } + } + + $sam->fetch( $region, $callback ); + + return \%count; +} + +=func bin_reads + + Usage : my $bin_ref = DETCT::Misc::BAM::bin_reads( { + bam_file => $bam_file, + mismatch_threshold => 2, + bin_size => 100, + seq_name => '1', + tags => ['NNNNBGAGGC', 'NNNNBAGAAG'], + } ); + Purpose : Bin reads in a BAM file + Returns : Hashref { + Int (bin) => Int (count) + } + Parameters : Hashref { + bam_file => String (the BAM file) + mismatch_threshold => Int (the mismatch threshold) + bin_size => Int (the bin size) + seq_name => String (the sequence name) + tags => Arrayref of strings (the tags) + } + Throws : If BAM file is missing + If mismatch threshold is missing + If bin size is missing + If sequence name is missing + If tags are missing + Comments : None + +=cut + +sub bin_reads { + my ($arg_ref) = @_; + + confess 'No BAM file specified' if !defined $arg_ref->{bam_file}; + confess 'No mismatch threshold specified' + if !defined $arg_ref->{mismatch_threshold}; + confess 'No bin size specified' if !defined $arg_ref->{bin_size}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No tags specified' if !defined $arg_ref->{tags}; + + my @tags = @{ $arg_ref->{tags} }; + + # Convert tags to regular expressions + my %re_for = DETCT::Misc::Tag::convert_tag_to_regexp(@tags); + + my $sam = Bio::DB::Sam->new( -bam => $arg_ref->{bam_file} ); + + my %read_count_for; + + # Callback for filtering + my $callback = sub { + my ($alignment) = @_; + return if !is_read2($alignment); + return if is_duplicate($alignment); + return if $alignment->unmapped; + return + if above_mismatch_threshold( $alignment, + $arg_ref->{mismatch_threshold} ); + return if !matched_tag( $alignment, \%re_for ); + + # Read can span multiple bins + my $start_bin = int( ( $alignment->start - 1 ) / $arg_ref->{bin_size} ); + my $end_bin = int( ( $alignment->end - 1 ) / $arg_ref->{bin_size} ); + + foreach my $bin ( $start_bin .. $end_bin ) { + $read_count_for{$bin}++; + } + + return; + }; + + $sam->fetch( $arg_ref->{seq_name}, $callback ); + + return { $arg_ref->{seq_name} => \%read_count_for }; +} + +=func get_read_peaks + + Usage : my $peaks_ref = DETCT::Misc::BAM::get_read_peaks( { + bam_file => $bam_file, + mismatch_threshold => 2, + peak_buffer_width => 100, + seq_name => '1', + tags => ['NNNNBGAGGC', 'NNNNBAGAAG'], + } ); + Purpose : Get read peaks (overlapping reads) for a BAM file + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (peak start), + Int (peak end), + Int (peak read count), + ], + ... (peaks) + ] + } + Parameters : Hashref { + bam_file => String (the BAM file) + mismatch_threshold => Int (the mismatch threshold) + peak_buffer_width => Int (the peak buffer size), + seq_name => String (the sequence name) + tags => Arrayref of strings (the tags) + } + Throws : If BAM file is missing + If mismatch threshold is missing + If peak buffer width is missing + If sequence name is missing + If tags are missing + Comments : BAM file must be sorted by coordinate + +=cut + +sub get_read_peaks { + my ($arg_ref) = @_; + + confess 'No BAM file specified' if !defined $arg_ref->{bam_file}; + confess 'No mismatch threshold specified' + if !defined $arg_ref->{mismatch_threshold}; + confess 'No peak buffer width specified' + if !defined $arg_ref->{peak_buffer_width}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No tags specified' if !defined $arg_ref->{tags}; + + my @tags = @{ $arg_ref->{tags} }; + + # Convert tags to regular expressions + my %re_for = DETCT::Misc::Tag::convert_tag_to_regexp(@tags); + + my $sam = Bio::DB::Sam->new( -bam => $arg_ref->{bam_file} ); + + # Peak variables + my @peaks; + my $current_peak_read_count; + my $current_peak_start; + my $current_peak_end; + + # Read variables + my $current_read_start; + my $current_read_end; + + # Callback for filtering + my $callback = sub { + my ($alignment) = @_; + return if !is_read2($alignment); + return if is_duplicate($alignment); + return if $alignment->unmapped; + return + if above_mismatch_threshold( $alignment, + $arg_ref->{mismatch_threshold} ); + return if !matched_tag( $alignment, \%re_for ); + + $current_read_start = $alignment->start; + $current_read_end = $alignment->end; + + # We're starting the first peak + if ( !defined $current_peak_start ) { + $current_peak_start = $current_read_start; + $current_peak_end = $current_read_end; + $current_peak_read_count = 1; + return; + } + + # Extend or finish current peak? + if ( $current_read_start - $current_peak_end < + $arg_ref->{peak_buffer_width} ) + { + # Extend current peak + $current_peak_end = $current_read_end; + $current_peak_read_count++; + } + else { + # Finish current peak + push @peaks, + [ + $current_peak_start, $current_peak_end, + $current_peak_read_count + ]; + + # Start new peak + $current_peak_start = $current_read_start; + $current_peak_end = $current_read_end; + $current_peak_read_count = 1; + } + + return; + }; + + # Identify peaks (where peaks are read 2s separated by a buffer of specific + # size) + $sam->fetch( $arg_ref->{seq_name}, $callback ); + + # Finish last peak + if ($current_peak_read_count) { + push @peaks, + [ $current_peak_start, $current_peak_end, $current_peak_read_count ]; + } + + return { $arg_ref->{seq_name} => \@peaks }; +} + +=func get_three_prime_ends + + Usage : my $three_prime_ref = DETCT::Misc::BAM::get_three_prime_ends( { + bam_file => $bam_file, + mismatch_threshold => 2, + seq_name => '1', + tags => ['NNNNBGAGGC', 'NNNNBAGAAG'], + regions => $regions_ary_ref, + } ); + Purpose : Get all 3' ends for a list of regions + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (region start), + Int (region end), + Int (region maximum read count), + Float (region log probability sum), + Arrayref [ + Arrayref [ + String (3' end sequence name), + Int (3' end position), + Int (3' end strand), + Int (3' end read count), + ], + ... (3' ends) + ], + ], + ... (regions) + } + Parameters : Hashref { + bam_file => String (the BAM file) + mismatch_threshold => Int (the mismatch threshold) + seq_name => String (the sequence name) + tags => Arrayref of strings (the tags) + regions => Arrayref (of regions) + } + Throws : If BAM file is missing + If mismatch threshold is missing + If sequence name is missing + If tags are missing + If regions are missing + Comments : regions parameter is a list of regions, unlike the regions + parameter for merge_three_prime_ends where it is a list of lists + of regions + +=cut + +sub get_three_prime_ends { + my ($arg_ref) = @_; + + confess 'No BAM file specified' if !defined $arg_ref->{bam_file}; + confess 'No mismatch threshold specified' + if !defined $arg_ref->{mismatch_threshold}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No tags specified' if !defined $arg_ref->{tags}; + confess 'No regions specified' if !defined $arg_ref->{regions}; + + my @tags = @{ $arg_ref->{tags} }; + + # Convert tags to regular expressions + my %re_for = DETCT::Misc::Tag::convert_tag_to_regexp(@tags); + + my $sam = Bio::DB::Sam->new( -bam => $arg_ref->{bam_file} ); + + my @regions_with_three_prime_ends; + + foreach my $region ( @{ $arg_ref->{regions} } ) { + my ( $start, $end, $max_read_count, $log_prob_sum ) = @{$region}; + + my %count_for; + + # Get all second reads in region + my $read2_alignments = $sam->features( + -seq_id => $arg_ref->{seq_name}, + -start => $start, + -end => $end, + -flags => { SECOND_MATE => 1 }, + -iterator => 1, + ); + + # Get all 3' ends + while ( my $alignment = $read2_alignments->next_seq ) { + next if is_duplicate($alignment); + + # next if $alignment->unmapped; # Not needed; always mapped + next if $alignment->munmapped; # Want read 1 mapped too + next + if above_mismatch_threshold( $alignment, + $arg_ref->{mismatch_threshold} ); + next if !matched_tag( $alignment, \%re_for ); + + # Skip if 3' end is on a different chromosome + # Hopefully not significant number of real 3' ends on different + # chromosomes because are hard to deal with + # If reads are on different chromosomes then TLEN will be 0 and + # mate_end will return undefined (i.e. can't get 3' end position + # without querying by read name, which is slow for a BAM file + # sorted by coordinate) + next if $alignment->mate_seq_id ne $arg_ref->{seq_name}; + + # Identify 3' end position and strand based on alignment of read 1 + my $three_prime_seq = $alignment->mate_seq_id; + my $three_prime_pos; + my $three_prime_strand; + if ( $alignment->mstrand == 1 ) { + $three_prime_pos = $alignment->mate_start; + $three_prime_strand = -1; ## no critic (ProhibitMagicNumbers) + } + else { + $three_prime_pos = $alignment->mate_end; + $three_prime_strand = 1; + } + + # Count number of reads supporting each 3' end + my $three_prime = join q{:}, $three_prime_seq, $three_prime_pos, + $three_prime_strand; + $count_for{$three_prime}++; + } + + # Turn counts into an array + my @three_prime_ends; + foreach my $three_prime ( + reverse sort { $count_for{$a} <=> $count_for{$b} } + keys %count_for + ) + { + my ( $seq, $pos, $strand ) = split /:/xms, $three_prime; + push @three_prime_ends, + [ $seq, $pos, $strand, $count_for{$three_prime} ]; + } + + # Add three prime ends to regions + push @regions_with_three_prime_ends, + [ $start, $end, $max_read_count, $log_prob_sum, \@three_prime_ends, ]; + } + + return { $arg_ref->{seq_name} => \@regions_with_three_prime_ends }; +} + +=func merge_three_prime_ends + + Usage : my $three_prime_ref + = DETCT::Misc::BAM::merge_three_prime_ends( { + seq_name => '1', + regions => $regions_ary_ref, + } ); + Purpose : Merge multiple lists of regions with 3' ends + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (region start), + Int (region end), + Int (region maximum read count), + Float (region log probability sum), + Arrayref [ + Arrayref [ + String (3' end sequence name), + Int (3' end position), + Int (3' end strand), + Int (3' end read count), + ], + ... (3' ends) + ], + ], + ... (regions) + } + Parameters : Hashref { + seq_name => String (the sequence name) + regions => Arrayref (of arrayrefs of regions) + } + Throws : If sequence name is missing + If regions are missing + If each list of regions doesn't have same number of regions + If regions are not in the same order or not the same in each + list + Comments : regions parameter is a list of lists of regions, unlike + the regions parameter for get_three_prime_ends where it is a + list of regions + +=cut + +sub merge_three_prime_ends { + my ($arg_ref) = @_; + + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No regions specified' if !defined $arg_ref->{regions}; + + my @list_of_lists_of_regions = @{ $arg_ref->{regions} }; + + # No need to merge if only one list of regions + my $num_lists = scalar @list_of_lists_of_regions; + if ( $num_lists == 1 ) { + return { $arg_ref->{seq_name} => $list_of_lists_of_regions[0] }; + } + + # Ensure each list has same number of regions as first list + my $num_regions1 = scalar @{ $list_of_lists_of_regions[0] }; + foreach my $list_index ( 1 .. $num_lists - 1 ) { + my $num_regions2 = scalar @{ $list_of_lists_of_regions[$list_index] }; + if ( $num_regions1 != $num_regions2 ) { + confess 'Number of regions does not match in all lists'; + } + } + + my @regions_with_three_prime_ends; + + # Merge all lists + foreach my $region_index ( 0 .. $num_regions1 - 1 ) { + + # Ensure region from first list is same in each list + my $region1 = $list_of_lists_of_regions[0]->[$region_index]; + my ( $start1, $end1, $max_read_count1, $log_prob_sum1 ) = @{$region1}; + foreach my $list_index ( 1 .. $num_lists - 1 ) { + my $region2 = + $list_of_lists_of_regions[$list_index]->[$region_index]; + my ( $start2, $end2, $max_read_count2, $log_prob_sum2 ) = + @{$region2}; + if ( $start1 != $start2 + || $end1 != $end2 + || $max_read_count1 != $max_read_count2 + || $log_prob_sum1 != $log_prob_sum2 ) + { + confess + 'Regions not in the same order or not the same in each list'; + } + } + + # Get all the 3' ends + my @unmerged_three_prime_ends; + foreach my $list_index ( 0 .. $num_lists - 1 ) { + my $list = $list_of_lists_of_regions[$list_index]; + my $region = $list->[$region_index]; + my ( undef, undef, undef, undef, $three_prime_ends ) = @{$region}; + push @unmerged_three_prime_ends, @{$three_prime_ends}; + } + + # Add up counts for identical 3' ends + my %count_for; + foreach my $three_prime_end (@unmerged_three_prime_ends) { + my ( $seq, $pos, $strand, $read_count ) = @{$three_prime_end}; + my $three_prime = join q{:}, $seq, $pos, $strand; + $count_for{$three_prime} += $read_count; + } + + # Turn counts into an array + my @three_prime_ends; + foreach my $three_prime ( + reverse sort { $count_for{$a} <=> $count_for{$b} } + keys %count_for + ) + { + my ( $seq, $pos, $strand ) = split /:/xms, $three_prime; + push @three_prime_ends, + [ $seq, $pos, $strand, $count_for{$three_prime} ]; + } + + # Add three prime ends to regions + push @regions_with_three_prime_ends, + [ + $start1, $end1, $max_read_count1, + $log_prob_sum1, \@three_prime_ends, + ]; + + $region_index++; + } + + return { $arg_ref->{seq_name} => \@regions_with_three_prime_ends }; +} + +=func filter_three_prime_ends + + Usage : my $three_prime_ref + = DETCT::Misc::BAM::filter_three_prime_ends( { + analysis => $analysis, + seq_name => '1', + regions => $regions_ary_ref, + } ); + Purpose : Filter list of regions with 3' ends + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (region start), + Int (region end), + Int (region maximum read count), + Float (region log probability sum), + Arrayref [ + Arrayref [ + String (3' end sequence name), + Int (3' end position), + Int (3' end strand), + Int (3' end read count), + ], + ... (3' ends) + ], + ], + ... (regions) + } + Parameters : Hashref { + analysis => DETCT::Analysis + seq_name => String (the sequence name) + regions => Arrayref (of regions) + } + Throws : If analysis is missing + If sequence name is missing + If regions are missing + Comments : regions parameter is a list of regions, unlike the regions + parameter for merge_three_prime_ends where it is a list of lists + of regions + +=cut + +sub filter_three_prime_ends { + my ($arg_ref) = @_; + + confess 'No analysis specified' if !defined $arg_ref->{analysis}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No regions specified' if !defined $arg_ref->{regions}; + + my @regions_with_three_prime_ends; + + # Iterate over regions + foreach my $region ( @{ $arg_ref->{regions} } ) { + my ( $region_start, $region_end, $region_max_read_count, + $region_log_prob_sum, $unfiltered_three_prime_ends ) + = @{$region}; + + # Filter 3' ends + my @three_prime_ends; + foreach my $three_prime_end ( @{$unfiltered_three_prime_ends} ) { + my ( $seq_name, $pos, $strand, $read_count ) = @{$three_prime_end}; + + # Must be supported by more than 3 reads + next if $read_count <= 3; ## no critic (ProhibitMagicNumbers) + + # Check 10 bp downstream of 3' end for polyA + my $ten_bp_start; + my $ten_bp_end; + if ( $strand == 1 ) { + $ten_bp_start = $pos + 1; + $ten_bp_end = $pos + 10; ## no critic (ProhibitMagicNumbers) + } + else { + $ten_bp_start = $pos - 10; ## no critic (ProhibitMagicNumbers) + $ten_bp_end = $pos - 1; + } + my $ten_bp_seq = + $arg_ref->{analysis} + ->get_subsequence( $seq_name, $ten_bp_start, $ten_bp_end, + $strand ); + + # Check if 10 bp downstream is polyA + next if is_polya($ten_bp_seq); + + push @three_prime_ends, $three_prime_end; + } + + # Add three prime ends to regions + push @regions_with_three_prime_ends, + [ + $region_start, $region_end, + $region_max_read_count, $region_log_prob_sum, + \@three_prime_ends, + ]; + } + + return { $arg_ref->{seq_name} => \@regions_with_three_prime_ends }; +} + +=func choose_three_prime_end + + Usage : my $three_prime_ref + = DETCT::Misc::BAM::choose_three_prime_end( { + seq_name => '1', + regions => $regions_ary_ref, + } ); + Purpose : Filter and adjust list of regions and choose best 3' end + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (region start), + Int (region end), + Int (region maximum read count), + Float (region log probability sum), + String (3' end sequence name) or undef, + Int (3' end position) or undef, + Int (3' end strand) or undef, + Int (3' end read count) or undef, + ], + ... (regions) + } + Parameters : Hashref { + seq_name => String (the sequence name) + regions => Arrayref (of regions) + } + Throws : If sequence name is missing + If regions are missing + Comments : regions parameter is a list of regions, unlike the regions + parameter for merge_three_prime_ends where it is a list of lists + of regions + +=cut + +sub choose_three_prime_end { + my ($arg_ref) = @_; + + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No regions specified' if !defined $arg_ref->{regions}; + + my @regions_with_three_prime_ends; + + # Iterate over regions + foreach my $region ( @{ $arg_ref->{regions} } ) { + my ( $region_start, $region_end, $region_max_read_count, + $region_log_prob_sum, $three_prime_ends ) + = @{$region}; + + my ( + $three_prime_seq_name, $three_prime_pos, + $three_prime_strand, $three_prime_read_count + ); + + @{$three_prime_ends} = reverse sort { + _sort_three_prime_end( $a, $b, $arg_ref->{seq_name}, $region_start, + $region_end ) + } @{$three_prime_ends}; + + # Get best 3' end (highest read count) + if ( @{$three_prime_ends} ) { + ( + $three_prime_seq_name, $three_prime_pos, + $three_prime_strand, $three_prime_read_count + ) = @{ $three_prime_ends->[0] }; + } + + # Reduce size of region if appropriate + ## no critic (ProhibitMagicNumbers) + if ( defined $three_prime_seq_name + && $three_prime_seq_name eq $arg_ref->{seq_name} ) + { + if ( $three_prime_strand == 1 + && $three_prime_pos < $region_end + && $three_prime_pos > $region_start ) + { + $region_end = $three_prime_pos; + } + elsif ($three_prime_strand == -1 + && $three_prime_pos > $region_start + && $three_prime_pos < $region_end ) + { + $region_start = $three_prime_pos; + } + } + ## use critic + + # Add three prime ends to regions + push @regions_with_three_prime_ends, + [ + $region_start, $region_end, + $region_max_read_count, $region_log_prob_sum, + $three_prime_seq_name, $three_prime_pos, + $three_prime_strand, $three_prime_read_count, + ]; + } + + return { $arg_ref->{seq_name} => \@regions_with_three_prime_ends }; +} + +# Sort by read count then distance to region +sub _sort_three_prime_end { + my ( $a, $b, $seq_name, $region_start, $region_end ) = @_; + + my $seq_name_a = $a->[0]; + my $seq_name_b = $b->[0]; + my $pos_a = $a->[1]; + my $pos_b = $b->[1]; + ## no critic (ProhibitMagicNumbers) + my $read_count_a = $a->[3]; + my $read_count_b = $b->[3]; + ## use critic + + # Get minimum distance to region + my $dist_a = min( abs $region_start - $pos_a, abs $region_end - $pos_a ); + my $dist_b = min( abs $region_start - $pos_b, abs $region_end - $pos_b ); + + # Make sure 3' end is on same chromosome as region + # (1e+100 is bigger than any chromosome can be to ensure sorting last) + if ( $seq_name_a ne $seq_name ) { + $dist_a = 1e+100; ## no critic (ProhibitMagicNumbers) + } + if ( $seq_name_b ne $seq_name ) { + $dist_b = 1e+100; ## no critic (ProhibitMagicNumbers) + } + + return $read_count_a <=> $read_count_b || $dist_b <=> $dist_a; +} + +=func count_reads + + Usage : my $count_ref = DETCT::Misc::BAM::count_reads( { + bam_file => $bam_file, + mismatch_threshold => 2, + seq_name => '1', + regions => $regions_ary_ref, + tags => ['NNNNBGAGGC', 'NNNNBAGAAG'], + } ); + Purpose : Count reads in regions of a BAM file + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (region start), + Int (region end), + Int (region maximum read count), + Float (region log probability sum), + String (3' end sequence name) or undef, + Int (3' end position) or undef, + Int (3' end strand) or undef, + Int (3' end read count) or undef, + Hashref { + String (tag) => Int (count) + } + ], + ... (regions) + } + Parameters : Hashref { + bam_file => String (the BAM file) + mismatch_threshold => Int (the mismatch threshold) + seq_name => String (the sequence name) or undef + regions => Arrayref (of regions) + tags => Arrayref of strings (the tags) + } + Throws : If BAM file is missing + If mismatch threshold is missing + If sequence name is missing + If regions are missing + If tags are missing + Comments : regions parameter is a list of regions, unlike the regions + parameter for merge_read_counts where it is a hash keyed by BAM + file with values being lists of regions + +=cut + +sub count_reads { + my ($arg_ref) = @_; + + confess 'No BAM file specified' if !defined $arg_ref->{bam_file}; + confess 'No mismatch threshold specified' + if !defined $arg_ref->{mismatch_threshold}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No regions specified' if !defined $arg_ref->{regions}; + confess 'No tags specified' if !defined $arg_ref->{tags}; + + my @tags = @{ $arg_ref->{tags} }; + + # Convert tags to regular expressions + my %re_for = DETCT::Misc::Tag::convert_tag_to_regexp(@tags); + + my $sam = Bio::DB::Sam->new( -bam => $arg_ref->{bam_file} ); + + my @regions_with_three_prime_ends; + + # Iterate over regions + foreach my $region ( @{ $arg_ref->{regions} } ) { + my ( + $region_start, $region_end, + $region_max_read_count, $region_log_prob_sum, + $three_prime_seq_name, $three_prime_pos, + $three_prime_strand, $three_prime_read_count + ) = @{$region}; + + my %count = map { $_ => 0 } @tags; + + # Get first read from each pair + my $read2_alignments = $sam->features( + -seq_id => $arg_ref->{seq_name}, + -start => $region_start, + -end => $region_end, + -flags => { SECOND_MATE => 1 }, + -iterator => 1, + ); + while ( my $alignment = $read2_alignments->next_seq ) { + next if is_duplicate($alignment); + + #next if $alignment->unmapped; # Not needed; always mapped + next + if above_mismatch_threshold( $alignment, + $arg_ref->{mismatch_threshold} ); + + # Match tag + my ($tag_in_read) = + $alignment->query->name =~ m/[#] ([AGCT]+) \z/xmsg; + next if !$tag_in_read; + TAG: foreach my $tag ( sort keys %re_for ) { + my $regexps = $re_for{$tag}; + foreach my $re ( @{$regexps} ) { + if ( $tag_in_read =~ $re ) { + $count{$tag}++; + last TAG; + } + } + } + } + + # Add read counts to regions + push @regions_with_three_prime_ends, + [ + $region_start, $region_end, + $region_max_read_count, $region_log_prob_sum, + $three_prime_seq_name, $three_prime_pos, + $three_prime_strand, $three_prime_read_count, + \%count, + ]; + } + + return { $arg_ref->{seq_name} => \@regions_with_three_prime_ends }; +} + +=func merge_read_counts + + Usage : my $count_ref + = DETCT::Misc::BAM::merge_read_counts( { + seq_name => '1', + regions => $regions_hash_ref, + samples => $samples_ary_ref, + } ); + Purpose : Merge multiple lists of regions with read counts + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (region start), + Int (region end), + Int (region maximum read count), + Float (region log probability sum), + String (3' end sequence name) or undef, + Int (3' end position) or undef, + Int (3' end strand) or undef, + Int (3' end read count) or undef, + Arrayref [ + Int (count) + ... + ] + ], + ... (regions) + } + Parameters : Hashref { + seq_name => String (the sequence name) + regions => Arrayref (of arrayrefs of regions) + samples => Arrayref (of samples) + } + Throws : If sequence name is missing + If regions are missing + If samples are missing + If each list of regions doesn't have same number of regions + If regions are not in the same order or not the same in each + list + Comments : regions parameter is a hash keyed by BAM file with values being + lists of regions, unlike the regions parameter for count_reads + where it is a list of regions + +=cut + +sub merge_read_counts { + my ($arg_ref) = @_; + + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No regions specified' if !defined $arg_ref->{regions}; + confess 'No samples specified' if !defined $arg_ref->{samples}; + + my %hash_of_lists_of_regions = %{ $arg_ref->{regions} }; + + # Ensure each list has same number of regions + my @bam_files = keys %hash_of_lists_of_regions; + my $num_regions1 = scalar @{ $hash_of_lists_of_regions{ $bam_files[0] } }; + foreach my $list_index ( 1 .. scalar @bam_files - 1 ) { + my $num_regions2 = + scalar @{ $hash_of_lists_of_regions{ $bam_files[$list_index] } }; + if ( $num_regions1 != $num_regions2 ) { + confess 'Number of regions does not match in all lists'; + } + } + + # Get index for each sample + my %sample_index_for; + my $index = 0; + foreach my $sample ( @{ $arg_ref->{samples} } ) { + my $bam_file = $sample->bam_file; + my $tag = $sample->tag; + $sample_index_for{$bam_file}{$tag} = $index; + $index++; + } + + my @regions_with_three_prime_ends; + + # Merge all lists + foreach my $region_index ( 0 .. $num_regions1 - 1 ) { + + # Ensure regions are the same in each list + my $region1 = + $hash_of_lists_of_regions{ $bam_files[0] }->[$region_index]; + my @region1 = @{$region1}[ 0 .. 7 ]; ## no critic (ProhibitMagicNumbers) + foreach my $list_index ( 1 .. scalar @bam_files - 1 ) { + my $region2 = + $hash_of_lists_of_regions{ $bam_files[$list_index] } + ->[$region_index]; + + # Check first 8 fields of each region are identical + my @region2 = + @{$region2}[ 0 .. 7 ]; ## no critic (ProhibitMagicNumbers) + if ( !Compare( \@region1, \@region2 ) ) { + confess + 'Regions not in the same order or not the same in each list'; + } + } + + my @read_counts; + + # Get read count for each BAM file / tag + foreach my $bam_file (@bam_files) { + my $region = $hash_of_lists_of_regions{$bam_file}->[$region_index]; + my $read_counts_ref = $region->[-1]; # Read counts are last field + foreach my $tag ( keys %{$read_counts_ref} ) { + my $read_count = $read_counts_ref->{$tag}; + if ( !exists $sample_index_for{$bam_file}{$tag} ) { + confess "Unknown BAM file ($bam_file) / tag ($tag) pair"; + } + my $sample_index = $sample_index_for{$bam_file}{$tag}; + $read_counts[$sample_index] = $read_count; + } + } + + push @regions_with_three_prime_ends, [ @region1, \@read_counts ]; + + $region_index++; + } + + return { $arg_ref->{seq_name} => \@regions_with_three_prime_ends }; +} + +=func matched_tag + + Usage : next if !matched_tag($alignment, \%re_for); + Purpose : Check if alignment doesn't match required tags + Returns : 1 or 0 + Parameters : Bio::DB::Bam::Alignment or Bio::DB::Bam::AlignWrapper + : Hashref of regular expressions + Throws : No exceptions + Comments : None + +=cut + +sub matched_tag { + my ( $alignment, $re_for ) = @_; + + my $got_match = 0; + + # Match tag + my ($tag_in_read) = $alignment->query->name =~ m/[#] ([AGCT]+) \z/xmsg; + if ($tag_in_read) { + TAG: foreach my $tag ( sort keys %{$re_for} ) { + my $regexps = $re_for->{$tag}; + foreach my $re ( @{$regexps} ) { + if ( $tag_in_read =~ $re ) { + $got_match = 1; + last TAG; + } + } + } + } + + return $got_match; +} + +=func is_read2 + + Usage : next if is_read2($alignment); + Purpose : Check if alignment is from read 2 (not read 1) + Returns : 1 or 0 + Parameters : Bio::DB::Bam::AlignWrapper + Throws : No exceptions + Comments : None + +=cut + +sub is_read2 { + my ($alignment) = @_; + + return ( $alignment->get_tag_values('FLAGS') =~ m/\bSECOND_MATE\b/xms ) + ? 1 + : 0; +} + +=func is_duplicate + + Usage : next if is_duplicate($alignment); + Purpose : Check if alignment is marked as a duplicate + Returns : 1 or 0 + Parameters : Bio::DB::Bam::AlignWrapper + Throws : No exceptions + Comments : None + +=cut + +sub is_duplicate { + my ($alignment) = @_; + + return ( $alignment->get_tag_values('FLAGS') =~ m/\bDUPLICATE\b/xms ) + ? 1 + : 0; +} + +=func above_mismatch_threshold + + Usage : next if above_mismatch_threshold($alignment, 2); + Purpose : Check if alignment has too many mismatches + Returns : 1 or 0 + Parameters : Bio::DB::Bam::Alignment or Bio::DB::Bam::AlignWrapper + : Int (mismatch threshold) + Throws : No exceptions + Comments : None + +=cut + +sub above_mismatch_threshold { + my ( $alignment, $threshold ) = @_; + + # Count soft clipped bases + my $cigar_ref = $alignment->cigar_array; + my $soft_clipped_bases = 0; + foreach my $pair_ref ( @{$cigar_ref} ) { + my ( $op, $count ) = @{$pair_ref}; + if ( $op eq q{S} ) { + $soft_clipped_bases += $count; + } + } + + # Get edit distance / number of mismatches + my $nm = $alignment->aux_get('NM'); + + # Check if above mismatch threshold + return ( $nm + $soft_clipped_bases > $threshold ) ? 1 : 0; +} + +=func is_polya + + Usage : next if is_polya($seq); + Purpose : Check if sequence contains polyA + Returns : 1 or 0 + Parameters : String (sequence) + Throws : No exceptions + Comments : None + +=cut + +sub is_polya { + my ($seq) = @_; + + my $is_polya = 0; + + # Check for more than 3 As at start + if ( $seq =~ m/\A AAAA /xms ) { + $is_polya = 1; + } + + # Check for more than 6 As in total + if ( !$is_polya ) { + my $a = $seq =~ tr/A/A/; + if ( $a > 6 ) { ## no critic (ProhibitMagicNumbers) + $is_polya = 1; + } + } + + # Check specific patterns for polyA + if ( !$is_polya ) { + foreach my $regexp (@POLYA_REGEXP) { + if ( $seq =~ $regexp ) { + $is_polya = 1; + last; + } + } + } + + return $is_polya; +} + +1; diff --git a/lib/DETCT/Misc/Output.pm b/lib/DETCT/Misc/Output.pm new file mode 100644 index 0000000..18fb537 --- /dev/null +++ b/lib/DETCT/Misc/Output.pm @@ -0,0 +1,738 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Misc::Output; +## use critic + +# ABSTRACT: Miscellaneous functions for outputting data + +## Author : is1 +## Maintainer : is1 +## Created : 2012-11-25 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Readonly; +use File::Spec; +use File::Path qw( make_path ); +use Sort::Naturally; +use List::MoreUtils qw( uniq all ); + +use base qw( Exporter ); +our @EXPORT_OK = qw( + dump_as_table +); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Constants + +# Types +Readonly our $STRING => 1; +Readonly our $INT => 2; +Readonly our $FLOAT => 3; + +# Output formats +Readonly our @FORMATS => qw( csv tsv html ); + +=func dump_as_table + + Usage : DETCT::Misc::Output::dump_as_table( { + analysis => $analysis, + dir => '.', + regions => $regions_hash_ref, + } ); + Purpose : Dump regions in tabular format + Returns : undef + Parameters : Hashref { + analysis => DETCT::Analysis, + dir => String (the working directory), + regions => Arrayref (of regions), + } + Throws : If analysis is missing + If regions are missing + If directory is missing + Comments : None + +=cut + +sub dump_as_table { + my ($arg_ref) = @_; + + confess 'No analysis specified' if !defined $arg_ref->{analysis}; + confess 'No directory specified' if !defined $arg_ref->{dir}; + confess 'No regions specified' if !defined $arg_ref->{regions}; + + # Get conditions and groups + my @samples = @{ $arg_ref->{analysis}->get_all_samples() }; + my @conditions = uniq( nsort( map { $_->condition } @samples ) ); + my @groups = + grep { defined $_ } uniq( nsort( map { $_->group } @samples ) ); + + # Get regions sorted by p value then location + my $regions = sort_regions( $arg_ref->{regions} ); + + # Get genebuild version + my $genebuild_version; + foreach my $region ( @{$regions} ) { + ## no critic (ProhibitMagicNumbers) + ($genebuild_version) = ( sort keys %{ $region->[15] } )[-1]; # Highest + ## use critic + last if $genebuild_version; + } + + # Get definition for all columns (which determines formatting) + my $definition = get_definition( $arg_ref->{analysis}->ensembl_species, + $genebuild_version, \@samples, \@conditions, \@groups ); + + # Make sure working directory exists + if ( !-d $arg_ref->{dir} ) { + make_path( $arg_ref->{dir} ); + } + + # Open filehandles and begin all output files + my $fh = begin_output( $arg_ref->{dir}, $definition ); + + foreach my $region ( @{$regions} ) { + my @row; + + # Region + my $seq_name = $region->[0]; + my $start = $region->[1]; + my $end = $region->[2]; + push @row, [ $seq_name, [ $seq_name, $start, $end, $seq_name ] ]; + push @row, [ $start, [ $seq_name, $start, $end, $start ] ]; + push @row, [ $end, [ $seq_name, $start, $end, $end ] ]; + + # 3' end + ## no critic (ProhibitMagicNumbers) + my $tpe_seq_name = $region->[5]; + my $tpe_pos = $region->[6]; + my $tpe_strand = $region->[7]; + my $tpe_read_count = $region->[8]; + ## use critic + push @row, + [ $tpe_pos, [ $tpe_seq_name, $tpe_pos, $tpe_pos, $tpe_pos ] ]; + push @row, [$tpe_strand]; + push @row, [$tpe_read_count]; + + # p values + ## no critic (ProhibitMagicNumbers) + my $pval = $region->[11]; + my $padj = $region->[12]; + ## use critic + push @row, [$pval]; + push @row, [$padj]; + + # Gene details + ## no critic (ProhibitMagicNumbers) + my %gene = %{ $region->[15] }; + my ($genebuild) = ( sort keys %gene )[-1]; # Highest + ## use critic + my @distance; + my ( @gene_stable_id, @gene_stable_id_to_link ); + my @gene_biotype; + my ( @transcript_stable_id, @transcript_stable_id_to_link ); + my @transcript_biotype; + my ( @name, @name_to_link ); + my @description; + + if ($genebuild) { + foreach my $gene ( @{ $gene{$genebuild} } ) { + my ( $gene_stable_id, $name, $description, $gene_biotype, + $distance, $transcripts ) + = @{$gene}; + push @distance, $distance; + push @gene_stable_id, $gene_stable_id; + push @gene_stable_id_to_link, + [ $gene_stable_id, $gene_stable_id ]; + push @gene_biotype, $gene_biotype; + foreach my $transcript ( @{$transcripts} ) { + my ( $transcript_stable_id, $transcript_biotype ) = + @{$transcript}; + push @transcript_stable_id, $transcript_stable_id; + push @transcript_stable_id_to_link, + [ $transcript_stable_id, $transcript_stable_id ]; + push @transcript_biotype, $transcript_biotype; + } + push @name, $name; + push @name_to_link, [ $gene_stable_id, $name ]; + push @description, $description; + } + } + push @row, [ \@distance ]; + push @row, [ \@gene_stable_id, [@gene_stable_id_to_link] ]; + push @row, [ \@gene_biotype ]; + push @row, [ \@transcript_stable_id, [@transcript_stable_id_to_link] ]; + push @row, [ \@transcript_biotype ]; + push @row, [ \@name, [@name_to_link] ]; + push @row, [ \@description ]; + + # Counts and normalised counts + ## no critic (ProhibitMagicNumbers) + my @counts = map { [$_] } @{ $region->[9] }; + my @normalised_counts = map { [$_] } @{ $region->[10] }; + ## use critic + foreach my $count (@counts) { + push @row, [$count]; + } + foreach my $normalised_count (@normalised_counts) { + push @row, [$normalised_count]; + } + + # Condition fold changes + if ( scalar @conditions == 2 ) { + ## no critic (ProhibitMagicNumbers) + my ( $condition_fold_change, $log2_condition_fold_change ) = + @{ $region->[13] }; + ## use critic + push @row, [$log2_condition_fold_change]; + } + + # Group fold changes + if ( scalar @conditions == 2 && scalar @groups > 1 ) { + ## no critic (ProhibitMagicNumbers) + my @group_fold_changes = @{ $region->[14] }; + ## use critic + foreach my $group_fold_change (@group_fold_changes) { + my ( + $condition_group_fold_change, + $log2_condition_group_fold_change + ) = @{$group_fold_change}; + push @row, [$log2_condition_group_fold_change]; + } + } + + # Dump row in all required formats + my @levels = qw( all ); + if ( defined $padj + && $padj ne 'NA' + && $padj < $arg_ref->{analysis}->output_sig_level ) + { + push @levels, 'sig'; + } + dump_output( \@levels, $fh, $definition, \@row ); + } + + # End all output files and close filehandles + end_output($fh); + + return; +} + +=func sort_regions + + Usage : $regions = sort_regions( $regions ); + Purpose : Sort regions by p value then location + Returns : Arrayref (of regions) + Parameters : Arrayref of regions + Throws : No exceptions + Comments : None + +=cut + +sub sort_regions { + my ($regions) = @_; + + # Separate regions with no p value from rest + my @regions_with_pval; + my @regions_no_pval; + foreach my $region ( @{$regions} ) { + ## no critic (ProhibitMagicNumbers) + if ( defined $region->[11] && $region->[11] ne 'NA' ) { + ## use critic + push @regions_with_pval, $region; + } + else { + push @regions_no_pval, $region; + } + } + + # Sort by adjusted p value and p value then regions without p value + ## no critic (ProhibitMagicNumbers) + my @regions = + sort { $a->[12] <=> $b->[12] || $a->[11] <=> $b->[11] } + @regions_with_pval; + ## use critic + push @regions, @regions_no_pval; # Sorted by location already + + return \@regions; +} + +=func get_definition + + Usage : $definition = get_definition($genebuild_version, $samples, + $conditions, $groups); + Purpose : Return the definitions for all columns of the table + Returns : Arrayref (of column definitions) + Parameters : String (Ensembl species) + String (genebuild version) + Arrayref of samples + Arrayref of conditions + Arrayref of groups + Throws : No exceptions + Comments : None + +=cut + +sub get_definition { + my ( $species, $genebuild_version, $samples, $conditions, $groups ) = @_; + + # Ensembl links + my $loc_link = + $species + ? qq{%s} + : undef; + my $gene_link = + q{%s}; + + my @def; + + push @def, [ 'Chr', $STRING, $loc_link, ]; + push @def, [ 'Region start', $INT, $loc_link, ]; + push @def, [ 'Region end', $INT, $loc_link, ]; + push @def, [ q{3' end position}, $INT, $loc_link, ]; + push @def, [ q{3' end strand}, $INT, ]; + push @def, [ q{3' end read count}, $INT, ]; + push @def, [ 'p value', $FLOAT, ]; + push @def, [ 'Adjusted p value', $FLOAT, ]; + push @def, [ q{Distance to 3' end }, $INT, ]; + push @def, + [ $genebuild_version . ' Ensembl Gene ID', $STRING, $gene_link, ]; + push @def, [ 'Gene type', $STRING, ]; + push @def, + [ $genebuild_version . ' Ensembl Transcript ID', $STRING, $gene_link, ]; + push @def, [ 'Transcript type', $STRING, ]; + push @def, [ 'Gene name', $STRING, $gene_link, ]; + push @def, [ 'Gene description', $STRING, ]; + + foreach my $sample ( @{$samples} ) { + push @def, [ $sample->name . ' count', $INT ]; + } + foreach my $sample ( @{$samples} ) { + push @def, [ $sample->name . ' normalised count', $FLOAT ]; + } + + if ( scalar @{$conditions} == 2 ) { + my $heading = sprintf 'Log2 fold change (%s/%s)', $conditions->[0], + $conditions->[1]; + push @def, [ $heading, $FLOAT ]; + } + + if ( scalar @{$conditions} == 2 && scalar @{$groups} > 1 ) { + foreach my $group ( @{$groups} ) { + my $heading = sprintf 'Log2 fold change (%s/%s) for group %s', + $conditions->[0], $conditions->[1], $group; + push @def, [ $heading, $FLOAT ]; + } + } + + return \@def; +} + +=func begin_output + + Usage : my $fh = begin_output( $dir, $defintion ); + Purpose : Open filehandles and begin all output files + Returns : undef + Parameters : String (the directory) + Arrayref (the definition) + Throws : No exceptions + Comments : None + +=cut + +sub begin_output { + my ( $dir, $definition ) = @_; + + my %fh; + foreach my $format (@FORMATS) { + + # Level determines whether output all regions or just significant ones + foreach my $level (qw( all sig )) { + my $file = File::Spec->catfile( $dir, $level . q{.} . $format ); + open my $fh, '>', $file; ## no critic (RequireBriefOpen) + $fh{$format}{$level} = $fh; + my $begin_sub_name = 'begin_' . $format; + my $sub_ref = \&{$begin_sub_name}; + &{$sub_ref}( $fh, $definition ); + } + } + + return \%fh; +} + +=func end_output + + Usage : end_output( $fh ); + Purpose : End all output files and close filehandles + Returns : undef + Parameters : Hashref (of filehandles) + Throws : No exceptions + Comments : None + +=cut + +sub end_output { + my ($fh) = @_; + + foreach my $format (@FORMATS) { + foreach my $level (qw( all sig )) { + my $end_sub_name = 'end_' . $format; + my $sub_ref = \&{$end_sub_name}; + &{$sub_ref}( $fh->{$format}{$level} ); + close $fh->{$format}{$level}; + } + } + + return; +} + +=func dump_output + + Usage : dump_output( $levels, $fh, $definition, $row ); + Purpose : Dump row in all required formats at all required levels + Returns : undef + Parameters : Arrayref (of levels) + Hashref (of filehandles) + Arrayref (the definition) + Arrayref (of row data) + Throws : No exceptions + Comments : None + +=cut + +sub dump_output { + my ( $levels, $fh, $definition, $row ) = @_; + + foreach my $format (@FORMATS) { + foreach my $level ( @{$levels} ) { + my $dump_sub_name = 'dump_' . $format; + my $sub_ref = \&{$dump_sub_name}; + &{$sub_ref}( $fh->{$format}{$level}, $definition, $row ); + } + } + + return; +} + +=func begin_csv + + Usage : begin_csv( $fh, $defintion ); + Purpose : Begin CSV table + Returns : undef + Parameters : Filehandle + Arrayref (the definition) + Throws : No exceptions + Comments : None + +=cut + +sub begin_csv { + my ( $fh, $definition ) = @_; + + my @headings; + foreach my $column ( @{$definition} ) { + my ($heading) = @{$column}; + $heading =~ s/"/""/xmsg; + push @headings, q{"} . $heading . q{"}; + } + print {$fh} ( join q{,}, @headings ), "\r\n"; + + return; +} + +=func end_csv + + Usage : end_csv( $fh ); + Purpose : End CSV table + Returns : undef + Parameters : Filehandle + Throws : No exceptions + Comments : None + +=cut + +sub end_csv { + return; +} + +=func dump_csv + + Usage : dump_csv( $fh, $definition, $row ); + Purpose : Dump the data in a CSV table + Returns : undef + Parameters : Filehandle + Arrayref (the defintion) + Arrayref (the row data) + Throws : No exceptions + Comments : None + +=cut + +sub dump_csv { + my ( $fh, $definition, $row ) = @_; + + my @output_row; + my $i = 0; # Index to definition + foreach my $cell ( @{$row} ) { + my $type = $definition->[$i]->[1]; + my ($data) = @{$cell}; + + # Turn into a list of data, even if just one + if ( ref $data ne 'ARRAY' ) { + $data = [$data]; + } + + # Substitute default if undefined + my @output_cell; + foreach my $datum ( @{$data} ) { + $datum = defined $datum ? $datum : q{}; + push @output_cell, $datum; + } + + # Add default if necessary + if ( !@output_cell ) { + push @output_cell, q{}; + } + + my $output_cell = join q{,}, @output_cell; + + # Strings and lists need quoting + if ( $type == $STRING || scalar @output_cell > 1 ) { + $output_cell =~ s/"/""/xmsg; + $output_cell = q{"} . $output_cell . q{"}; + } + + push @output_row, $output_cell; + + $i++; + } + print {$fh} ( join q{,}, @output_row ), "\n"; + + return; +} + +=func begin_tsv + + Usage : begin_tsv( $fh, $defintion ); + Purpose : Begin TSV table + Returns : undef + Parameters : Filehandle + Arrayref (the definition) + Throws : No exceptions + Comments : None + +=cut + +sub begin_tsv { + my ( $fh, $definition ) = @_; + + my @headings = map { $_->[0] } @{$definition}; + print {$fh} q{#}, ( join "\t", @headings ), "\n"; + + return; +} + +=func end_tsv + + Usage : end_tsv( $fh ); + Purpose : End TSV table + Returns : undef + Parameters : Filehandle + Throws : No exceptions + Comments : None + +=cut + +sub end_tsv { + return; +} + +=func dump_tsv + + Usage : dump_tsv( $fh, $definition, $row ); + Purpose : Dump the data in a TSV table + Returns : undef + Parameters : Filehandle + Arrayref (the defintion) + Arrayref (the row data) + Throws : No exceptions + Comments : None + +=cut + +sub dump_tsv { + my ( $fh, $definition, $row ) = @_; + + my @output_row; + my $i = 0; # Index to definition + foreach my $cell ( @{$row} ) { + my $type = $definition->[$i]->[1]; + my ($data) = @{$cell}; + + # Turn into a list of data, even if just one + if ( ref $data ne 'ARRAY' ) { + $data = [$data]; + } + + # Substitute default if undefined + my @output_cell; + foreach my $datum ( @{$data} ) { + $datum = defined $datum && length $datum > 0 ? $datum : q{-}; + push @output_cell, $datum; + } + + # Add default if necessary + if ( !@output_cell ) { + push @output_cell, q{-}; + } + + push @output_row, ( join q{,}, @output_cell ); + + $i++; + } + print {$fh} ( join "\t", @output_row ), "\n"; + + return; +} + +=func begin_html + + Usage : begin_html( $fh, $defintion ); + Purpose : Begin HTML table + Returns : undef + Parameters : Filehandle + Arrayref (the definition) + Throws : No exceptions + Comments : None + +=cut + +sub begin_html { + my ( $fh, $definition ) = @_; + + print {$fh} <<'HTML'; + + + + DETCT + + + + + + +HTML + + foreach my $column ( @{$definition} ) { + my ($heading) = @{$column}; + print {$fh} '', "\n"; + } + + print {$fh} <<'HTML'; + + + +HTML + + return; +} + +=func end_html + + Usage : end_html( $fh ); + Purpose : End HTML table + Returns : undef + Parameters : Filehandle + Throws : No exceptions + Comments : None + +=cut + +sub end_html { + my ($fh) = @_; + + print {$fh} <<'HTML'; + +
', $heading, '
+ + +HTML + + return; +} + +=func dump_html + + Usage : dump_html( $fh, $definition, $row ); + Purpose : Dump the data in an HTML table + Returns : undef + Parameters : Filehandle + Arrayref (the defintion) + Arrayref (the row data) + Throws : No exceptions + Comments : None + +=cut + +sub dump_html { + my ( $fh, $definition, $row ) = @_; + + print {$fh} '', "\n"; + + my $i = 0; # Index to definition + foreach my $cell ( @{$row} ) { + my ( undef, $type, $link ) = @{ $definition->[$i] }; + my ( $data, $data_to_link ) = @{$cell}; + + # Turn into a list of data, even if just one + if ( ref $data ne 'ARRAY' ) { + $data = [$data]; + $data_to_link = [$data_to_link]; + } + + print {$fh} ''; + + my @data; + my $j = 0; # Index to each item when multiple items in one table cell + foreach my $datum ( @{$data} ) { + my $datum_to_link = $data_to_link->[$j]; + + $datum = defined $datum ? $datum : q{}; + + # Make a link if there's a link and all data for the link is defined + if ( $link && $datum_to_link && all { defined $_ } + @{$datum_to_link} ) + { + $datum = sprintf $link, @{$datum_to_link}; + } + + push @data, $datum; + + $j++; + } + + print {$fh} join '
', @data; + + print {$fh} '', "\n"; + + $i++; + } + + print {$fh} '', "\n"; + + return; +} + +1; diff --git a/lib/DETCT/Misc/PeakHMM.pm b/lib/DETCT/Misc/PeakHMM.pm new file mode 100644 index 0000000..24d3ed9 --- /dev/null +++ b/lib/DETCT/Misc/PeakHMM.pm @@ -0,0 +1,520 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Misc::PeakHMM; +## use critic + +# ABSTRACT: Miscellaneous functions for running peaks HMM + +## Author : is1 +## Maintainer : is1 +## Created : 2012-10-29 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use English qw( -no_match_vars ); +use POSIX qw( WIFEXITED); +use File::Slurp; +use File::Spec; +use File::Path qw( make_path ); +use Memoize qw( memoize flush_cache ); + +use base qw( Exporter ); +our @EXPORT_OK = qw( + merge_read_peaks + summarise_read_peaks + run_peak_hmm + join_hmm_bins +); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +=func merge_read_peaks + + Usage : my $peaks_ref = DETCT::Misc::PeakHMM::merge_read_peaks( { + peak_buffer_width => 100, + seq_name => '1', + peaks => $peaks_ary_ref, + } ); + Purpose : Merge read peaks (overlapping reads) + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (peak start), + Int (peak end), + Int (peak read count) + ], + ... (peaks) + ] + } + Parameters : Hashref { + peak_buffer_width => Int (the peak buffer size), + seq_name => String (the sequence name), + peaks => Arrayref (of peaks), + } + Throws : If peak buffer width is missing + If sequence name is missing + If peaks are missing + Comments : None + +=cut + +sub merge_read_peaks { + my ($arg_ref) = @_; + + confess 'No peak buffer width specified' + if !defined $arg_ref->{peak_buffer_width}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No peaks specified' if !defined $arg_ref->{peaks}; + + my $peaks_ref = $arg_ref->{peaks}; + + # Sort peaks by start then end + @{$peaks_ref} = + sort { $a->[0] <=> $b->[0] || $a->[1] <=> $b->[1] } @{$peaks_ref}; + + # Peak variables + my @merged_peaks; + my $current_merged_peak_read_count; + my $current_merged_peak_start; + my $current_merged_peak_end; + + # Merge peaks + foreach my $peak ( @{$peaks_ref} ) { + my ( $peak_start, $peak_end, $peak_read_count ) = @{$peak}; + + # We're starting the first merged peak + if ( !defined $current_merged_peak_start ) { + $current_merged_peak_start = $peak_start; + $current_merged_peak_end = $peak_end; + $current_merged_peak_read_count = $peak_read_count; + next; + } + + # Extend or finish current merged peak? + if ( $peak_start - $current_merged_peak_end < + $arg_ref->{peak_buffer_width} ) + { + # Extend current merged peak + $current_merged_peak_end = $peak_end; + $current_merged_peak_read_count += $peak_read_count; + } + else { + # Finish current merged peak + push @merged_peaks, + [ + $current_merged_peak_start, $current_merged_peak_end, + $current_merged_peak_read_count + ]; + + # Start new merged peak + $current_merged_peak_start = $peak_start; + $current_merged_peak_end = $peak_end; + $current_merged_peak_read_count = $peak_read_count; + } + } + + # Finish last merged peak + if ($current_merged_peak_read_count) { + push @merged_peaks, + [ + $current_merged_peak_start, $current_merged_peak_end, + $current_merged_peak_read_count + ]; + } + + return { $arg_ref->{seq_name} => \@merged_peaks }; +} + +=func summarise_read_peaks + + Usage : my $summary_ref = DETCT::Misc::PeakHMM::summarise_read_peaks( { + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_name => '1', + seq_bp => 10_000_000, + read_length => 54, + peaks => $peaks_ary_ref, + } ); + Purpose : Summarise read peak distribution for HMM + Returns : Hashref { + String (sequence name) => Hashref { + total_read_count_per_mb => Float, + total_sig_read_count_per_mb => Float, + total_sig_peak_width_in_mb => Float, + median_sig_peak_width => Int, + total_sig_peaks => Int, + peak_buffer_width => Int, + read_threshold => Int, + bin_size => Int, + num_bins => Int, + } + } + Parameters : Hashref { + bin_size => Int (the bin size), + peak_buffer_width => Int (the peak buffer size), + hmm_sig_level => Float (the HMM significance level), + seq_name => String (the sequence name), + seq_bp => Int (the sequence bp), + read_length => Int (the read length), + peaks => Arrayref (of peaks), + } + Throws : If bin size is missing + If peak buffer width is missing + If HMM significance level is missing + If sequence name is missing + If sequence bp is missing + if read length is missing + If peaks are missing + Comments : Source of logic is summary.pl from + http://www.sph.umich.edu/csg/qin/HPeak/ + +=cut + +sub summarise_read_peaks { + my ($arg_ref) = @_; + + confess 'No bin size specified' if !defined $arg_ref->{bin_size}; + confess 'No peak buffer width specified' + if !defined $arg_ref->{peak_buffer_width}; + confess 'No HMM significance level specified' + if !defined $arg_ref->{hmm_sig_level}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No sequence bp specified' if !defined $arg_ref->{seq_bp}; + confess 'No read length specified' if !defined $arg_ref->{read_length}; + confess 'No peaks specified' if !defined $arg_ref->{peaks}; + + my $total_peaks = scalar @{ $arg_ref->{peaks} }; + + if ( !$total_peaks ) { + + # No peaks so won't be running HMM + return { $arg_ref->{seq_name} => {} }; + } + + # Get total read count + my $total_read_count = 0; + foreach my $peak ( @{ $arg_ref->{peaks} } ) { + my ( $start, $end, $read_count ) = @{$peak}; + $total_read_count += $read_count; + } + + # Get avg reads/bp + my $avg_reads_per_bp = $total_read_count / $arg_ref->{seq_bp}; + + # Identify significant peaks + memoize('_calc_log_sum'); + my @sig_peak_widths; + my $total_sig_read_count = 0; + my $total_sig_peak_width = 0; + foreach my $peak ( @{ $arg_ref->{peaks} } ) { + my ( $start, $end, $read_count ) = @{$peak}; + my $width = $end - $start + 1; + my $avg_reads = $avg_reads_per_bp * $width; + my $log_avg_reads = log $avg_reads; + my $exp_avg_reads = exp $avg_reads; + + # Gather info for significant peaks + my $sum = 1; + my $i = 1; + while ( $i < $read_count ) { + $sum += exp _calc_log_sum( $i, $log_avg_reads ); + last if $sum >= $exp_avg_reads; + $i++; + } + my $prob = 1 - exp( -$avg_reads ) * $sum; + if ( $prob < $arg_ref->{hmm_sig_level} / $total_peaks ) { + push @sig_peak_widths, $width; + $total_sig_read_count += $read_count; + $total_sig_peak_width += $width; + } + + # Expire Memoize cache for each peak + flush_cache('_calc_log_sum'); + } + + # Calculate hit threshold + my $proportion_bp_in_peaks = + $total_read_count * $arg_ref->{read_length} / $arg_ref->{seq_bp}; + my $read_threshold = 0; + my $prob = 1; + my $sum = 1; + while ( $prob > $arg_ref->{hmm_sig_level} / $total_peaks ) { + $read_threshold++; + my $log_sum = 0; + foreach my $i ( 1 .. $read_threshold ) { + $log_sum += log($proportion_bp_in_peaks) - log $i; + } + $sum += exp $log_sum; + $prob = 1 - exp( -$proportion_bp_in_peaks ) * $sum; + } + + # Sort widths and get median + my $total_sig_peaks = scalar @sig_peak_widths; + @sig_peak_widths = sort { $a <=> $b } @sig_peak_widths; + my $median_sig_peak_width = $sig_peak_widths[ int( $total_sig_peaks / 2 ) ]; + + ## no critic (ProhibitMagicNumbers) + my $num_bins = int( $arg_ref->{seq_bp} / $arg_ref->{bin_size} + 0.5 ); + ## use critic + + ## no critic (ProhibitMagicNumbers) + my %summary = ( + total_read_count_per_mb => $total_read_count / 1_000_000, + total_sig_read_count_per_mb => $total_sig_read_count / 1_000_000, + total_sig_peak_width_in_mb => $total_sig_peak_width / 1_000_000, + median_sig_peak_width => $median_sig_peak_width || 0, + total_sig_peaks => $total_sig_peaks, + peak_buffer_width => $arg_ref->{peak_buffer_width}, + read_threshold => $read_threshold, + bin_size => $arg_ref->{bin_size}, + num_bins => $num_bins, + ); + ## use critic + + return { $arg_ref->{seq_name} => \%summary }; +} + +# Calculate log sum +sub _calc_log_sum { + my ( $i, $log ) = @_; + + if ( $i == 0 ) { + return 0; + } + else { + return $log - log($i) + _calc_log_sum( $i - 1, $log ); + } +} + +=func run_peak_hmm + + Usage : my $hmm_ref = DETCT::Misc::PeakHMM::run_peak_hmm( { + dir => '.', + hmm_sig_level => 0.001, + seq_name => '1', + read_bins => $read_bins_hash_ref, + summary => $summary_hash_ref, + hmm_binary => 'bin/quince_chiphmmnew', + } ); + Purpose : Run peak HMM + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (bin), + Int (read count), + Float (log probability), + ], + ... (peaks) + } + Parameters : Hashref { + dir => String (the working directory), + hmm_sig_level => Float (the HMM significance level), + seq_name => String (the sequence name), + read_bins => Hashref (of read bins), + summary => Hashref (of summary), + hmm_binary => String (the HMM binary) + } + Throws : If directory is missing + If HMM significance level is missing + If sequence name is missing + If read bins are missing + If summary is missing + If HMM binary is missing + If command line can't be run + Comments : None + +=cut + +sub run_peak_hmm { + my ($arg_ref) = @_; + + confess 'No directory specified' if !defined $arg_ref->{dir}; + confess 'No HMM significance level specified' + if !defined $arg_ref->{hmm_sig_level}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No read bins specified' if !defined $arg_ref->{read_bins}; + confess 'No summary specified' if !defined $arg_ref->{summary}; + confess 'No HMM binary specified' if !defined $arg_ref->{hmm_binary}; + + if ( !scalar keys %{ $arg_ref->{summary} } ) { + + # No summary (i.e. no peaks), so won't run HMM + return { $arg_ref->{seq_name} => [] }; + } + + # Make sure working directory exists + if ( !-d $arg_ref->{dir} ) { + make_path( $arg_ref->{dir} ); + } + + # Sanitise sequence name for using in filenames + my $safe_seq_name = $arg_ref->{seq_name}; + $safe_seq_name =~ s/\W+//xmsg; + + # Write read bins to file + my $bin_file = + File::Spec->catfile( $arg_ref->{dir}, $safe_seq_name . '.bins' ); + open my $bin_fh, '>', $bin_file; + foreach my $bin ( sort { $a <=> $b } keys %{ $arg_ref->{read_bins} } ) { + print {$bin_fh} $bin, "\t", $arg_ref->{read_bins}->{$bin}, "\n"; + } + close $bin_fh; + + # Write summary to file + my $sum_file = + File::Spec->catfile( $arg_ref->{dir}, $safe_seq_name . '.params' ); + ## no critic (RequireBriefOpen) + open my $sum_fh, '>', $sum_file; + print {$sum_fh} $arg_ref->{summary}->{total_read_count_per_mb}, "\n"; + print {$sum_fh} $arg_ref->{summary}->{total_sig_read_count_per_mb}, "\n"; + print {$sum_fh} $arg_ref->{summary}->{total_sig_peak_width_in_mb}, "\n"; + print {$sum_fh} $arg_ref->{summary}->{median_sig_peak_width}, "\n"; + print {$sum_fh} $arg_ref->{summary}->{total_sig_peaks}, "\n"; + print {$sum_fh} $arg_ref->{summary}->{peak_buffer_width}, "\n"; + print {$sum_fh} $arg_ref->{summary}->{read_threshold}, "\n"; + print {$sum_fh} $arg_ref->{summary}->{bin_size}, "\n"; + print {$sum_fh} $arg_ref->{summary}->{num_bins}, "\n"; + close $sum_fh; + ## use critic + + my $hmm_file = + File::Spec->catfile( $arg_ref->{dir}, $safe_seq_name . '.hmm' ); + my $stdout_file = + File::Spec->catfile( $arg_ref->{dir}, $safe_seq_name . '.o' ); + my $stderr_file = + File::Spec->catfile( $arg_ref->{dir}, $safe_seq_name . '.e' ); + + my $cmd = join q{ }, $arg_ref->{hmm_binary}, $bin_file, $sum_file, + $hmm_file; + $cmd .= ' 1>' . $stdout_file; + $cmd .= ' 2>' . $stderr_file; + WIFEXITED( system $cmd) or confess "Couldn't run $cmd ($OS_ERROR)"; + + # Reformat output into array of arrayrefs + my $log_hmm_sig_level = log $arg_ref->{hmm_sig_level}; + my @hmm_output = (); + if ( -r $hmm_file ) { # Peak HMM can fail + foreach my $line ( read_file($hmm_file) ) { + chomp $line; + my ( $bin, undef, undef, $read_count, $log_prob ) = split /\t/xms, + $line; + next if $log_prob >= $log_hmm_sig_level; + push @hmm_output, [ $bin, $read_count, $log_prob ]; + } + } + + return { $arg_ref->{seq_name} => \@hmm_output }; +} + +=func join_hmm_bins + + Usage : my $regions_ref = DETCT::Misc::PeakHMM::join_hmm_bins( { + bin_size => 100, + seq_name => '1', + hmm_bins => $hmm_bins_ary_ref, + } ); + Purpose : Join reads bins output by peak HMM into regions + Returns : Hashref { + String (sequence name) => Arrayref [ + Arrayref [ + Int (region start), + Int (region end), + Int (region maximum read count), + Float (region log probability sum), + ], + ... (regions) + } + Parameters : Hashref { + bin_size => Int (the bin size), + seq_name => String (the sequence name), + hmm_bins => Arrayref (of HMM bins), + } + Throws : If bin size is missing + If sequence name is missing + If HMM bins are missing + Comments : None + +=cut + +sub join_hmm_bins { + my ($arg_ref) = @_; + + confess 'No bin size specified' if !defined $arg_ref->{bin_size}; + confess 'No sequence name specified' if !defined $arg_ref->{seq_name}; + confess 'No HMM bins specified' if !defined $arg_ref->{hmm_bins}; + + my @regions; + + # Region variables (where a region is a set of merged bins) + my $region_bin_start; + my $region_bin_end; + my $region_max_read_count; + my $region_log_prob_sum; + + foreach my $hmm_bin ( @{ $arg_ref->{hmm_bins} } ) { + my ( $bin, $read_count, $log_prob ) = @{$hmm_bin}; + + # We're starting the first region + if ( !defined $region_bin_start ) { + $region_bin_start = $bin; + $region_bin_end = $bin; + $region_max_read_count = $read_count; + $region_log_prob_sum = $log_prob; + next; + } + + # Extend or finish current region? + if ( $bin == $region_bin_end + 1 ) { + + # Next bin, so extend current region + $region_bin_end = $bin; + if ( $read_count > $region_max_read_count ) { + $region_max_read_count = $read_count; + } + $region_log_prob_sum += $log_prob; + } + else { + # Finish current region and convert to genomic coordinates + push @regions, + [ + $region_bin_start * $arg_ref->{bin_size} + 1, + ( $region_bin_end + 1 ) * $arg_ref->{bin_size}, + $region_max_read_count, + $region_log_prob_sum, + ]; + + # Start new region + $region_bin_start = $bin; + $region_bin_end = $bin; + $region_max_read_count = $read_count; + $region_log_prob_sum = $log_prob; + } + } + + # Finish last region + if ( defined $region_bin_start ) { + push @regions, + [ + $region_bin_start * $arg_ref->{bin_size} + 1, + ( $region_bin_end + 1 ) * $arg_ref->{bin_size}, + $region_max_read_count, + $region_log_prob_sum, + ]; + } + + return { $arg_ref->{seq_name} => \@regions }; +} + +1; diff --git a/lib/DETCT/Misc/R.pm b/lib/DETCT/Misc/R.pm new file mode 100644 index 0000000..64b5cea --- /dev/null +++ b/lib/DETCT/Misc/R.pm @@ -0,0 +1,271 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Misc::R; +## use critic + +# ABSTRACT: Miscellaneous functions for running R + +## Author : is1 +## Maintainer : is1 +## Created : 2012-11-21 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use English qw( -no_match_vars ); +use POSIX qw( WIFEXITED); +use File::Slurp; +use File::Spec; +use File::Path qw( make_path ); +use Sort::Naturally; +use List::Util qw( sum ); +use List::MoreUtils qw( uniq ); + +use base qw( Exporter ); +our @EXPORT_OK = qw( + run_deseq +); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +=func run_deseq + + Usage : my $regions_ref = DETCT::Misc::R::run_deseq( { + dir => '.', + regions => $regions_hash_ref, + samples => $samples_ary_ref, + r_binary => 'R', + deseq_script => 'bin/run_deseq.R', + } ); + Purpose : Run DESeq + Returns : Arrayref [ + Arrayref [ + String (region sequence name), + Int (region start), + Int (region end), + Int (region maximum read count), + Float (region log probability sum), + String (3' end sequence name) or undef, + Int (3' end position) or undef, + Int (3' end strand) or undef, + Int (3' end read count) or undef, + Arrayref [ + Int (count) + ... + ], + Arrayref [ + Int (normalised count) + ... + ], + Int (p value) or undef, + Int (adjusted p value) or undef, + Arrayref [ + Int (condition fold change) or undef, + Int (log2 condition fold change) or undef, + ], + Arrayref [ + Arrayref [ + Int (group fold change) or undef, + Int (log2 group fold change) or undef, + ], + ... (groups) + ] + ], + ... (regions) + } + Parameters : Hashref { + dir => String (the working directory), + regions => Hashref (of arrayrefs of regions), + samples => Arrayref (of samples) + r_binary => String (the R binary), + deseq_script => String (the DESeq script), + } + Throws : If directory is missing + If regions are missing + If samples are missing + If R binary is missing + If DESeq script is missing + If command line can't be run + Comments : None + +=cut + +sub run_deseq { + my ($arg_ref) = @_; + + confess 'No directory specified' if !defined $arg_ref->{dir}; + confess 'No regions specified' if !defined $arg_ref->{regions}; + confess 'No samples specified' if !defined $arg_ref->{samples}; + confess 'No R binary specified' if !defined $arg_ref->{r_binary}; + confess 'No DESeq script specified' if !defined $arg_ref->{deseq_script}; + + # Get conditions and groups + my @samples = @{ $arg_ref->{samples} }; + my @conditions = uniq( nsort( map { $_->condition } @samples ) ); + my @groups = uniq( nsort( map { $_->group } @samples ) ); + @groups = grep { defined $_ } @groups; + + # Make sure working directory exists + if ( !-d $arg_ref->{dir} ) { + make_path( $arg_ref->{dir} ); + } + + # Write regions to input file + my $input_file = File::Spec->catfile( $arg_ref->{dir}, 'input.txt' ); + my @sample_names = map { $_->name } @samples; + open my $input_fh, '>', $input_file; + print {$input_fh} ( join "\t", q{}, @sample_names ), "\n"; + foreach my $seq_name ( nsort( keys %{ $arg_ref->{regions} } ) ) { + foreach my $region ( @{ $arg_ref->{regions}->{$seq_name} } ) { + my $counts = $region->[-1]; + my $region_text = join q{:}, $seq_name, $region->[0], $region->[1]; + print {$input_fh} ( join "\t", $region_text, @{$counts} ), "\n"; + } + } + close $input_fh; + + # Write samples to input file + my $samples_file = File::Spec->catfile( $arg_ref->{dir}, 'samples.txt' ); + my $last_col_to_print = @groups > 1 ? 2 : 1; + my @header = ( q{}, 'condition', 'group' )[ 0 .. $last_col_to_print ]; + open my $samples_fh, '>', $samples_file; + print {$samples_fh} ( join "\t", @header ), "\n"; + foreach my $sample (@samples) { + my @row = + ( $sample->name, $sample->condition, $sample->group ) + [ 0 .. $last_col_to_print ]; + print {$samples_fh} ( join "\t", @row ), "\n"; + } + close $samples_fh; + + my $output_file = File::Spec->catfile( $arg_ref->{dir}, 'output.txt' ); + my $size_factors_file = + File::Spec->catfile( $arg_ref->{dir}, 'size_factors.txt' ); + my $qc_pdf_file = File::Spec->catfile( $arg_ref->{dir}, 'qc.pdf' ); + my $stdout_file = File::Spec->catfile( $arg_ref->{dir}, 'deseq.o' ); + my $stderr_file = File::Spec->catfile( $arg_ref->{dir}, 'deseq.e' ); + + my $cmd = join q{ }, $arg_ref->{r_binary}, '--slave', '--args', + $input_file, $samples_file, $output_file, $size_factors_file, + $qc_pdf_file, '<', $arg_ref->{deseq_script}; + $cmd .= ' 1>' . $stdout_file; + $cmd .= ' 2>' . $stderr_file; + WIFEXITED( system $cmd) or confess "Couldn't run $cmd ($OS_ERROR)"; + + # Get size factors for each sample + my @size_factors = read_file($size_factors_file); + chomp @size_factors; + + # Get output + my %pval_for; + my %padj_for; + foreach my $line ( read_file($output_file) ) { + chomp $line; + my ( $region_text, $pval, $padj ) = split /\t/xms, $line; + $pval_for{$region_text} = $pval; + $padj_for{$region_text} = $padj; + } + + # Reformat output into array of arrayrefs + my @output; + foreach my $seq_name ( nsort( keys %{ $arg_ref->{regions} } ) ) { + foreach my $region ( @{ $arg_ref->{regions}->{$seq_name} } ) { + my $region_text = join q{:}, $seq_name, $region->[0], $region->[1]; + my $counts = $region->[-1]; + + # Add sequence name to region + unshift @{$region}, $seq_name; + + # Normalise counts and store for fold change calculation + my @normalised_counts; + my %counts_for_condition; + my %counts_for_group_condition; + my $sample_index = 0; + foreach my $sample (@samples) { + my $normalised_count = + $counts->[$sample_index] / $size_factors[$sample_index]; + push @normalised_counts, $normalised_count; + push @{ $counts_for_condition{ $sample->condition } }, + $normalised_count; + push @{ $counts_for_group_condition{ $sample->group } + { $sample->condition } }, $normalised_count; + $sample_index++; + } + push @{$region}, \@normalised_counts; + + # Add p value and adjusted p value + push @{$region}, $pval_for{$region_text}, $padj_for{$region_text}; + + # Calculate fold change if two conditions + my $fold_change; + my $log2_fold_change; + if ( scalar @conditions == 2 ) { + ( $fold_change, $log2_fold_change ) = calc_fold_change( + $counts_for_condition{ $conditions[0] }, + $counts_for_condition{ $conditions[1] } + ); + } + push @{$region}, [ $fold_change, $log2_fold_change ]; + + # Calculate fold change for each group if two conditions + my @group_fold_changes; + if ( scalar @conditions == 2 && scalar @groups > 1 ) { + foreach my $group (@groups) { + my ( $group_fold_change, $group_log2_fold_change ) = + calc_fold_change( + $counts_for_group_condition{$group}{ $conditions[0] }, + $counts_for_group_condition{$group}{ $conditions[1] } + ); + push @group_fold_changes, + [ $group_fold_change, $group_log2_fold_change ]; + } + } + push @{$region}, \@group_fold_changes; + + push @output, $region; + } + } + + return \@output; +} + +=func calc_fold_change + + Usage : ($fold_change, $log2_fold_change) + = calc_fold_change(\@array1, \@array2); + Purpose : Calculate the fold change in mean value of two arrays + Returns : Int (fold change) + Int (log2 fold change) + Parameters : Arrayref + Arrayref + Throws : No exceptions + Comments : None + +=cut + +sub calc_fold_change { + my ( $array1_ref, $array2_ref ) = @_; + + my $fold_change; + my $log2_fold_change; + my $mean1 = sum( @{$array1_ref} ) / scalar @{$array1_ref}; + my $mean2 = sum( @{$array2_ref} ) / scalar @{$array2_ref}; + if ( $mean1 && $mean2 ) { + $fold_change = $mean1 / $mean2; # e.g. mutant / sibling + $log2_fold_change = log($fold_change) / log 2; + } + + return $fold_change, $log2_fold_change; +} + +1; diff --git a/lib/DETCT/Misc/Tag.pm b/lib/DETCT/Misc/Tag.pm new file mode 100644 index 0000000..4c507e2 --- /dev/null +++ b/lib/DETCT/Misc/Tag.pm @@ -0,0 +1,269 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Misc::Tag; +## use critic + +# ABSTRACT: Miscellaneous functions for interacting with DETCT read tags + +## Author : is1 +## Maintainer : is1 +## Created : 2013-01-07 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use base qw( Exporter ); +our @EXPORT_OK = qw( + detag_trim_fastq + convert_tag_to_regexp +); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +=func detag_trim_fastq + + Usage : DETCT::Misc::Tag::detag_trim_fastq( { + fastq_read1_input => $fastq_read1_input, + fastq_read2_input => $fastq_read2_input, + fastq_output_prefix => $fastq_output_prefix, + pre_detag_trim_length => $pre_detag_trim_length, + polyt_trim_length => $polyt_trim_length, + polyt_min_length => $polyt_min_length, + read_tags => \@read_tags, + } ); + Purpose : Detag and trim FASTQ files + Returns : undef + Parameters : Hashref { + fastq_read1_input => String (read 1 FASTQ file), + fastq_read2_input => String (read 2 FASTQ file), + fastq_output_prefix => String (prefix for output FASTQs), + pre_detag_trim_length => Int (length to trim reads to), + polyt_trim_length => Int (polyT length to be trimmed), + polyt_min_length => Int (min Ts to define polyT), + read_tags => Arrayref (of read tags), + no_pair_suffix => Boolean or undef, + } + Throws : No exceptions + Comments : None + +=cut + +sub detag_trim_fastq { + my ($arg_ref) = @_; + + # Assume all tags are same length + my $tag_length = length $arg_ref->{read_tags}[0]; + + my $min_polyt = q{T} x $arg_ref->{polyt_min_length}; + my $polyt_re = qr/$min_polyt/xms; # Regexp for polyT matching + + my $pre_detag_trim_length = $arg_ref->{pre_detag_trim_length}; + my $polyt_trim_length = $arg_ref->{polyt_trim_length}; + + # Convert tags to regular expressions + my @read_tags = @{ $arg_ref->{read_tags} }; + my %re_tag_for = convert_tag_to_regexp(@read_tags); + + ## no critic (RequireBriefOpen) + open my $fh1_in, '<', $arg_ref->{fastq_read1_input}; + open my $fh2_in, '<', $arg_ref->{fastq_read2_input}; + ## use critic + my $fh_out_for = _open_output_fhs( $arg_ref->{fastq_output_prefix}, + $tag_length, @read_tags ); + + while ( my $read1_id = <$fh1_in> ) { + my $read2_id = <$fh2_in>; + my $read1_seq = <$fh1_in>; + my $read2_seq = <$fh2_in>; + my $read1_plus = <$fh1_in>; + my $read2_plus = <$fh2_in>; + my $read1_qual = <$fh1_in>; + my $read2_qual = <$fh2_in>; + + chomp $read1_id; + chomp $read2_id; + chomp $read1_seq; + chomp $read2_seq; + chomp $read1_plus; + chomp $read2_plus; + chomp $read1_qual; + chomp $read2_qual; + + # Do we need to add pair suffix to read IDs? + if ( $arg_ref->{no_pair_suffix} ) { + $read1_id .= '/1'; + $read2_id .= '/2'; + } + + # Remove /1 or /2 from read ids and then check they match + my $read1_id_no_suffix = $read1_id; + my $read2_id_no_suffix = $read2_id; + ## no critic (ProhibitMagicNumbers) + substr $read1_id_no_suffix, -2, 2, q{}; + substr $read2_id_no_suffix, -2, 2, q{}; + ## use critic + if ( $read1_id_no_suffix ne $read2_id_no_suffix ) { + confess 'Read order does not match in input ' + . "($read1_id_no_suffix does not match $read2_id_no_suffix)"; + } + + # Trim reads to specified length if necessary + if ( length $read1_seq > $pre_detag_trim_length ) { + $read1_seq = substr $read1_seq, 0, $pre_detag_trim_length; + $read2_seq = substr $read2_seq, 0, $pre_detag_trim_length; + $read1_qual = substr $read1_qual, 0, $pre_detag_trim_length; + $read2_qual = substr $read2_qual, 0, $pre_detag_trim_length; + } + + # Get tag and putative polyT from read 1 + my $tag_in_read = substr $read1_seq, 0, $tag_length; + my $polyt_seq = substr $read1_seq, $tag_length, $polyt_trim_length; + + # Default tag to add to id if no match + my $tag_for_id = q{X} x $tag_length; + my $tag_found = q{X} x $tag_length; + + # Make sure a tag matches and polyT is present + TAG: foreach my $tag ( sort keys %re_tag_for ) { + my $regexps = $re_tag_for{$tag}; + foreach my $re ( @{$regexps} ) { + if ( $tag_in_read =~ $re && $polyt_seq =~ $polyt_re ) { + $tag_for_id = $tag_in_read; + $tag_found = $tag; + substr $read1_seq, 0, $tag_length + $polyt_trim_length, q{}; + substr $read1_qual, 0, $tag_length + $polyt_trim_length, + q{}; + last TAG; # Skip rest if got a match + } + } + } + + # Add tag to id + $read1_id =~ s{ /1 \z}{#$tag_for_id/1}xms; + $read2_id =~ s{ /2 \z}{#$tag_for_id/2}xms; + + print { $fh_out_for->{$tag_found}->{1} } $read1_id, "\n"; + print { $fh_out_for->{$tag_found}->{1} } $read1_seq, "\n"; + print { $fh_out_for->{$tag_found}->{1} } $read1_plus, "\n"; + print { $fh_out_for->{$tag_found}->{1} } $read1_qual, "\n"; + print { $fh_out_for->{$tag_found}->{2} } $read2_id, "\n"; + print { $fh_out_for->{$tag_found}->{2} } $read2_seq, "\n"; + print { $fh_out_for->{$tag_found}->{2} } $read2_plus, "\n"; + print { $fh_out_for->{$tag_found}->{2} } $read2_qual, "\n"; + } + + close $fh1_in; + close $fh2_in; + _close_output_fhs($fh_out_for); + + return; +} + +# Usage : my $fh_out_for = _open_output_fhs( +# $fastq_output_prefix, $tag_length, @read_tags +# ); +# Purpose : Open filehandles for all output FASTQ files +# Returns : Hashref of hashref of filehandles +# Parameters : String (prefix for output FASTQs) +# Int (tag length) +# Array of strings (the tags) +# Throws : No exceptions +# Comments : None +sub _open_output_fhs { + my ( $fastq_output_prefix, $tag_length, @tags ) = @_; + + push @tags, q{X} x $tag_length; # Default tag if no match + + my %fh_for; + foreach my $tag (@tags) { + foreach my $read ( 1, 2 ) { + my $filename = join q{_}, $fastq_output_prefix, $tag, $read; + $filename .= '.fastq'; + open my $fh, '>', $filename; ## no critic (RequireBriefOpen) + $fh_for{$tag}->{$read} = $fh; + } + } + + return \%fh_for; +} + +# Usage : _close_output_fhs($fh_out_for); +# Purpose : Close filehandles for all output FASTQ files +# Returns : undef +# Parameters : Hashref of hashref of filehandles +# Throws : No exceptions +# Comments : None +sub _close_output_fhs { + my ($fh_for) = @_; + + foreach my $tag ( keys %{$fh_for} ) { + foreach my $read ( keys %{ $fh_for->{$tag} } ) { + close $fh_for->{$tag}->{$read}; + } + } + + return; +} + +=func convert_tag_to_regexp + + Usage : %re_for = convert_tag_to_regexp( 'NNNNBGAGGC', 'NNNNBAGAAG' ); + Purpose : Convert tags to regular expressions for matching + Returns : Hash ( + String (tag) => Arrayref (of Regexps) + ) + Parameters : Array of strings (the tags) + Throws : No exceptions + Comments : None + +=cut + +sub convert_tag_to_regexp { + my @tags = @_; + + my %re_for; + foreach my $tag (@tags) { + my @mismatch_tags = ($tag); # Start with tag without mismatches + + # Add tag with each possible mismatch + foreach my $i ( 0 .. length($tag) - 1 ) { + my $mismatch_tag = $tag; + my $base = substr $mismatch_tag, $i, 1, q{N}; # Replace with N + if ( $base ne q{N} ) { + + # Not completely random base already + push @mismatch_tags, $mismatch_tag; + } + } + + # Convert IUPAC codes to AGCT (or N) + foreach my $re (@mismatch_tags) { + $re =~ s/N/[NAGCT]/xmsg; # Random bases can be called as N + $re =~ s/B/[GCT]/xmsg; + $re =~ s/D/[AGT]/xmsg; + $re =~ s/H/[ACT]/xmsg; + $re =~ s/V/[AGC]/xmsg; + $re =~ s/R/[AG]/xmsg; + $re =~ s/Y/[CT]/xmsg; + $re =~ s/K/[GT]/xmsg; + $re =~ s/M/[AC]/xmsg; + $re =~ s/S/[GC]/xmsg; + $re =~ s/W/[AT]/xmsg; + push @{ $re_for{$tag} }, qr/\A $re \Z/xms; + } + } + + return %re_for; +} + +1; diff --git a/lib/DETCT/Pipeline.pm b/lib/DETCT/Pipeline.pm new file mode 100644 index 0000000..8f0c925 --- /dev/null +++ b/lib/DETCT/Pipeline.pm @@ -0,0 +1,1280 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Pipeline; +## use critic + +# ABSTRACT: Object representing a pipeline + +## Author : is1 +## Maintainer : is1 +## Created : 2013-01-09 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Readonly; +use Class::InsideOut qw( private register id ); +use Scalar::Util qw( refaddr ); +use English qw( -no_match_vars ); +use POSIX qw( WIFEXITED WIFSIGNALED WTERMSIG ); +use File::Slurp; +use File::Spec; +use File::Path qw( make_path ); +use Hash::Merge; +use YAML::Tiny qw( DumpFile ); +use Sys::Hostname; +use File::Basename; +use File::Find; +use DETCT; +use DETCT::Pipeline::Job; +use DETCT::Pipeline::Stage; + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Attributes: +private scheduler => my %scheduler; # e.g. lsf +private analysis_dir => my %analysis_dir; # e.g. . +private analysis => my %analysis; # DETCT::Analysis object +private cmd_line => my %cmd_line; # e.g. run_de_pipeline.pl +private max_retries => my %max_retries; # e.g. 10 +private sleep_time => my %sleep_time; # e.g. 600 +private stage_to_run => my %stage_to_run; # DETCT::Pipeline::Stage object +private component_to_run => my %component_to_run; # e.g. 5 +private verbose => my %verbose; # e.g. 1 +private hash_merge => my %hash_merge; # Hash::Merge object +private stage => my %stage; # arrayref of stages + +# Constants +Readonly our %EXTENSION_TO_KEEP => map { $_ => 1 } qw( + csv html pdf tsv txt +); + +=method new + + Usage : my $pipeline = DETCT::Pipeline->new( { + scheduler => 'lsf', + analysis_dir => '.', + analysis => $analysis, + cmd_line => 'run_de_pipeline.pl', + max_retries => 10, + sleep_time => 600, + verbose => 1, + } ); + Purpose : Constructor for pipeline objects + Returns : DETCT::Pipeline + Parameters : Hashref { + scheduler => String, + analysis_dir => String, + analysis => DETCT::Analysis, + cmd_line => String, + max_retries => Int, + sleep_time => Int, + verbose => Boolean or undef + } + Throws : No exceptions + Comments : None + +=cut + +sub new { + my ( $class, $arg_ref ) = @_; + my $self = register($class); + $self->set_scheduler( $arg_ref->{scheduler} ); + $self->set_analysis_dir( $arg_ref->{analysis_dir} ); + $self->set_analysis( $arg_ref->{analysis} ); + $self->set_cmd_line( $arg_ref->{cmd_line} ); + $self->set_max_retries( $arg_ref->{max_retries} ); + $self->set_sleep_time( $arg_ref->{sleep_time} ); + $self->set_verbose( $arg_ref->{verbose} ); + return $self; +} + +=method scheduler + + Usage : my $scheduler = $pipeline->scheduler; + Purpose : Getter for scheduler attribute + Returns : String (e.g. "lsf") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub scheduler { + my ($self) = @_; + return $scheduler{ id $self}; +} + +=method set_scheduler + + Usage : $pipeline->set_scheduler('lsf'); + Purpose : Setter for scheduler attribute + Returns : undef + Parameters : String (the scheduler) + Throws : No exceptions + Comments : None + +=cut + +sub set_scheduler { + my ( $self, $arg ) = @_; + $scheduler{ id $self} = _check_scheduler($arg); + return; +} + +# Usage : $scheduler = _check_scheduler($scheduler); +# Purpose : Check for valid scheduler +# Returns : String (the valid scheduler) +# Parameters : String (the scheduler) +# Throws : If scheduler is not lsf or local +# Comments : None +sub _check_scheduler { + my ($scheduler) = @_; + + confess 'Invalid scheduler specified' + if !defined $scheduler + || ( $scheduler ne 'lsf' && $scheduler ne 'local' ); + + return $scheduler; +} + +=method analysis_dir + + Usage : my $analysis_dir = $pipeline->analysis_dir; + Purpose : Getter for analysis directory attribute + Returns : String (e.g. ".") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub analysis_dir { + my ($self) = @_; + return $analysis_dir{ id $self}; +} + +=method set_analysis_dir + + Usage : $pipeline->set_analysis_dir('.'); + Purpose : Setter for analysis directory attribute + Returns : undef + Parameters : String (the analysis directory) + Throws : No exceptions + Comments : None + +=cut + +sub set_analysis_dir { + my ( $self, $arg ) = @_; + $analysis_dir{ id $self} = _check_analysis_dir($arg); + return; +} + +# Usage : $analysis_dir = _check_analysis_dir($analysis_dir); +# Purpose : Check for valid analysis directory +# Returns : String (the valid analysis directory) +# Parameters : String (the analysis directory) +# Throws : If analysis directory is missing or invalid +# Comments : None +sub _check_analysis_dir { + my ($analysis_dir) = @_; + + # Make sure analysis directory exists + if ( defined $analysis_dir && !-d $analysis_dir ) { + make_path($analysis_dir); + } + + return $analysis_dir if defined $analysis_dir && -d $analysis_dir; + confess 'No analysis_dir specified' if !defined $analysis_dir; + confess "Invalid analysis_dir ($analysis_dir) specified"; +} + +=method analysis + + Usage : my $analysis = $pipeline->analysis; + Purpose : Getter for analysis attribute + Returns : DETCT::Analysis + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub analysis { + my ($self) = @_; + return $analysis{ id $self}; +} + +=method set_analysis + + Usage : $pipeline->set_analysis($analysis); + Purpose : Setter for analysis attribute + Returns : undef + Parameters : DETCT::Analysis + Throws : No exceptions + Comments : None + +=cut + +sub set_analysis { + my ( $self, $arg ) = @_; + $analysis{ id $self} = _check_analysis($arg); + return; +} + +# Usage : $analysis = _check_analysis($analysis); +# Purpose : Check for valid analysis object +# Returns : DETCT::Analysis +# Parameters : DETCT::Analysis +# Throws : If analysis object is missing or invalid (i.e. not a +# DETCT::Analysis object) +# Comments : None +sub _check_analysis { + my ($analysis) = @_; + return $analysis if defined $analysis && $analysis->isa('DETCT::Analysis'); + confess 'No analysis specified' if !defined $analysis; + confess 'Class of analysis (', ref $analysis, ') not DETCT::Analysis'; +} + +=method cmd_line + + Usage : my $cmd_line = $pipeline->cmd_line; + Purpose : Getter for command line attribute + Returns : String (e.g. "run_de_pipeline.pl") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub cmd_line { + my ($self) = @_; + return $cmd_line{ id $self}; +} + +=method set_cmd_line + + Usage : $pipeline->set_cmd_line('run_de_pipeline.pl'); + Purpose : Setter for command line attribute + Returns : undef + Parameters : String (the command line) + Throws : No exceptions + Comments : None + +=cut + +sub set_cmd_line { + my ( $self, $arg ) = @_; + $cmd_line{ id $self} = _check_cmd_line($arg); + return; +} + +# Usage : $cmd_line = _check_cmd_line($cmd_line); +# Purpose : Check for valid command line +# Returns : String (the valid command line) +# Parameters : String (the command line) +# Throws : If command line is missing +# Comments : None +sub _check_cmd_line { + my ($cmd_line) = @_; + + confess 'No command line specified' if !defined $cmd_line || !$cmd_line; + + return $cmd_line; +} + +=method max_retries + + Usage : my $max_retries = $pipeline->max_retries; + Purpose : Getter for max retries attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub max_retries { + my ($self) = @_; + return $max_retries{ id $self}; +} + +=method set_max_retries + + Usage : $pipeline->set_max_retries(10); + Purpose : Setter for max retries attribute + Returns : undef + Parameters : +ve Int (the max retries) + Throws : No exceptions + Comments : None + +=cut + +sub set_max_retries { + my ( $self, $arg ) = @_; + $max_retries{ id $self} = _check_max_retries($arg); + return; +} + +# Usage : $max_retries = _check_max_retries($max_retries); +# Purpose : Check for valid max retries +# Returns : +ve Int (the valid max retries) +# Parameters : +ve Int (the max retries) +# Throws : If max retries is missing or not a positive integer +# Comments : None +sub _check_max_retries { + my ($max_retries) = @_; + return $max_retries + if defined $max_retries && $max_retries =~ m/\A \d+ \z/xms; + confess 'No max retries specified' if !defined $max_retries; + confess "Invalid max retries ($max_retries) specified"; +} + +=method sleep_time + + Usage : my $sleep_time = $pipeline->sleep_time; + Purpose : Getter for sleep time attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub sleep_time { + my ($self) = @_; + return $sleep_time{ id $self}; +} + +=method set_sleep_time + + Usage : $pipeline->set_sleep_time(600); + Purpose : Setter for sleep time attribute + Returns : undef + Parameters : +ve Int (the sleep time) + Throws : No exceptions + Comments : None + +=cut + +sub set_sleep_time { + my ( $self, $arg ) = @_; + $sleep_time{ id $self} = _check_sleep_time($arg); + return; +} + +# Usage : $sleep_time = _check_sleep_time($sleep_time); +# Purpose : Check for valid sleep time +# Returns : +ve Int (the valid sleep time) +# Parameters : +ve Int (the sleep time) +# Throws : If sleep time is missing or not a positive integer +# Comments : None +sub _check_sleep_time { + my ($sleep_time) = @_; + return $sleep_time + if defined $sleep_time && $sleep_time =~ m/\A \d+ \z/xms; + confess 'No sleep time specified' if !defined $sleep_time; + confess "Invalid sleep time ($sleep_time) specified"; +} + +=method stage_to_run + + Usage : my $stage = $pipeline->stage_to_run; + Purpose : Getter for stage to be run attribute + Returns : DETCT::Pipeline::Stage + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub stage_to_run { + my ($self) = @_; + return $stage_to_run{ id $self}; +} + +=method set_stage_to_run + + Usage : $pipeline->set_stage_to_run($stage); + Purpose : Setter for stage to be run attribute + Returns : undef + Parameters : DETCT::Pipeline::Stage + Throws : No exceptions + Comments : None + +=cut + +sub set_stage_to_run { + my ( $self, $arg ) = @_; + $stage_to_run{ id $self} = _check_stage_to_run($arg); + return; +} + +# Usage : $stage = _check_stage_to_run($stage); +# Purpose : Check for valid stage to be run object +# Returns : DETCT::Pipeline::Stage +# Parameters : DETCT::Pipeline::Stage +# Throws : If stage to be run object is missing or invalid (i.e. not a +# DETCT::Pipeline::Stage object) +# Comments : None +sub _check_stage_to_run { + my ($stage_to_run) = @_; + return $stage_to_run + if defined $stage_to_run && $stage_to_run->isa('DETCT::Pipeline::Stage'); + confess 'No stage to be run specified' if !defined $stage_to_run; + confess 'Class of stage to be run (', ref $stage_to_run, + ') not DETCT::Pipeline::Stage'; +} + +=method component_to_run + + Usage : my $component = $pipeline->component_to_run; + Purpose : Getter for component to be run attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub component_to_run { + my ($self) = @_; + return $component_to_run{ id $self}; +} + +=method set_component_to_run + + Usage : $pipeline->set_component_to_run(5); + Purpose : Setter for component to be run attribute + Returns : undef + Parameters : +ve Int (the component to be run) + Throws : No exceptions + Comments : None + +=cut + +sub set_component_to_run { + my ( $self, $arg ) = @_; + $component_to_run{ id $self} = _check_component_to_run($arg); + return; +} + +# Usage : $component = _check_component_to_run($component); +# Purpose : Check for valid component to be run +# Returns : +ve Int (the valid component to be run) +# Parameters : +ve Int (the component to be run) +# Throws : If component to be run is missing or not a positive integer +# Comments : None +sub _check_component_to_run { + my ($component_to_run) = @_; + return $component_to_run + if defined $component_to_run && $component_to_run =~ m/\A \d+ \z/xms; + confess 'No component to be run specified' if !defined $component_to_run; + confess "Invalid component to be run ($component_to_run) specified"; +} + +=method verbose + + Usage : my $verbose = $pipeline->verbose; + Purpose : Getter for verbose flag + Returns : Boolean + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub verbose { + my ($self) = @_; + return $verbose{ id $self} || 0; +} + +=method set_verbose + + Usage : $pipeline->set_verbose(1); + Purpose : Setter for verbose flag + Returns : undef + Parameters : Boolean + Throws : No exceptions + Comments : None + +=cut + +sub set_verbose { + my ( $self, $arg ) = @_; + $verbose{ id $self} = $arg ? 1 : 0; + return; +} + +=method hash_merge + + Usage : %chunk_hmm + = %{ $pipeline->hash_merge->merge(\%chunk_hmm, $seq_hmm) }; + Purpose : Return a Hash::Merge object for merging job output + Returns : Hash::Merge + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub hash_merge { + my ($self) = @_; + + if ( !exists $hash_merge{ id $self} ) { + ## no critic (ProtectPrivateSubs) + Hash::Merge::specify_behavior( + { + SCALAR => { + SCALAR => sub { $_[0] + $_[1] }, # Add scalars + ARRAY => sub { undef }, + HASH => sub { undef }, + }, + ARRAY => { + SCALAR => sub { undef }, + ARRAY => sub { [ @{ $_[0] }, @{ $_[1] } ] }, # Join arrays + HASH => sub { undef }, + }, + HASH => { + SCALAR => sub { undef }, + ARRAY => sub { undef }, + HASH => sub { Hash::Merge::_merge_hashes( $_[0], $_[1] ) }, + }, + }, + 'detct', + ); + ## use critic + $hash_merge{ id $self} = Hash::Merge->new('detct'); + } + + return $hash_merge{ id $self}; +} + +=method add_stages_from_yaml + + Usage : $pipeline->add_stages_from_yaml( 'detct.yaml' ); + Purpose : Add stages from a YAML file + Returns : undef + Parameters : String (the YAML file) + Throws : If YAML file is missing or not readable or invalid + Comments : None + +=cut + +sub add_stages_from_yaml { + my ( $self, $yaml_file ) = @_; + + confess "YAML file ($yaml_file) does not exist or cannot be read" + if !-r $yaml_file; + + my $yaml = YAML::Tiny->read($yaml_file); + + if ( !$yaml ) { + confess sprintf 'YAML file (%s) is invalid: %s', $yaml_file, + YAML::Tiny->errstr; + } + + my %tmp_cache; # Temporarily store stages by name + + foreach my $stage_hash ( @{ $yaml->[0] } ) { + my $stage = DETCT::Pipeline::Stage->new( + { + name => $stage_hash->{name}, + default_memory => $stage_hash->{default_memory}, + } + ); + foreach my $prerequisite_name ( @{ $stage_hash->{prerequisites} } ) { + $stage->add_prerequisite( $tmp_cache{$prerequisite_name} ); + } + $self->add_stage($stage); + + $tmp_cache{ $stage_hash->{name} } = $stage; + } + + return; +} + +=method add_stage + + Usage : $pipeline->add_stage($stage); + Purpose : Add a stage to a pipeline + Returns : undef + Parameters : DETCT::Pipeline::Stage + Throws : If stage is missing or invalid (i.e. not a + DETCT::Pipeline::Stage object) + Comments : None + +=cut + +sub add_stage { + my ( $self, $stage ) = @_; + + confess 'No stage specified' if !defined $stage; + confess 'Class of stage (', ref $stage, ') not DETCT::Pipeline::Stage' + if !$stage->isa('DETCT::Pipeline::Stage'); + + if ( !exists $stage{ id $self} ) { + $stage{ id $self} = [$stage]; + } + else { + push @{ $stage{ id $self} }, $stage; + } + + return; +} + +=method get_all_stages + + Usage : $stages = $pipeline->get_all_stages(); + Purpose : Get all stages of a pipeline + Returns : Arrayref of DETCT::Pipeline::Stage objects + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub get_all_stages { + my ($self) = @_; + + return $stage{ id $self} || []; +} + +=method get_stage_by_name + + Usage : $stage = $pipeline->get_stage_by_name('run_deseq'); + Purpose : Get a named stage of a pipeline + Returns : DETCT::Pipeline::Stage + Parameters : String (the stage name) + Throws : If stage with specified name does not exist + Comments : None + +=cut + +sub get_stage_by_name { + my ( $self, $name ) = @_; + + foreach my $stage ( @{ $stage{ id $self} } ) { + return $stage if $stage->name eq $name; + } + + confess "Invalid stage name ($name)"; +} + +=method run + + Usage : $pipeline->run(); + Purpose : Run pipeline + Returns : undef + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub run { + my ($self) = @_; + + $self->init_run(); + + my $all_stages_run = 0; + + while ( !$all_stages_run ) { + $all_stages_run = 1; + + my $jobs_running_or_run = 0; + + # Iterate over all stages of pipeline + STAGE: foreach my $stage ( @{ $self->get_all_stages() } ) { + + # Check prerequisites have already run and skip this stage if not + foreach my $prereq_stage ( @{ $stage->get_all_prerequisites() } ) { + if ( !$prereq_stage->all_jobs_run ) { + $self->say_if_verbose( + sprintf 'Skipping %s because %s not run', + $stage->name, $prereq_stage->name ); + next STAGE; + } + } + + # Create directory for current stage of analysis + my $dir = $self->get_and_create_stage_dir($stage); + + # Assume all jobs have run OK until we know otherwise + $stage->set_all_jobs_run(1); + + # All jobs marked as having run OK? + my $done_marker_file = $dir . '.done'; + if ( -e $done_marker_file ) { + $self->say_if_verbose( sprintf 'Stage %s has finished', + $stage->name ); + next STAGE; + } + + # Running a specific stage, but not this one + if ( $self->stage_to_run + && refaddr( $self->stage_to_run ) != refaddr($stage) ) + { + next STAGE; + } + + # Get all parameters for all components of current stage + my @all_parameters = $self->all_parameters($stage); + + $self->say_if_verbose( sprintf 'Stage %s has %d components', + $stage->name, scalar @all_parameters ); + + my $component = 0; # Index for current component of current stage + foreach my $parameters (@all_parameters) { + $component++; + + # Running a specific component, but not this one + if ( + $self->stage_to_run + && $self->component_to_run + && ( refaddr( $self->stage_to_run ) != refaddr($stage) + || $self->component_to_run != $component ) + ) + { + next; + } + + my $job = DETCT::Pipeline::Job->new( + { + stage => $stage, + component => $component, + scheduler => $self->scheduler, + base_filename => + File::Spec->catfile( $dir, $component ), + parameters => $parameters, + } + ); + + # Run job if running a specific component of a specific stage + if ( $self->stage_to_run && $self->component_to_run ) { + $self->run_job($job); + return; + } + + $jobs_running_or_run += $self->process_job($job); + } + + if ( $stage->all_jobs_run ) { + write_file( $done_marker_file, '1' ); + } + else { + $all_stages_run = 0; + } + } + + if ( !$all_stages_run && !$jobs_running_or_run ) { + $self->_delete_lock(); + die 'Stopping pipeline - no jobs to run' . "\n"; + } + + if ( !$all_stages_run ) { + $self->say_if_verbose( sprintf 'Sleeping for %d seconds', + $self->sleep_time ); + sleep $self->sleep_time; + } + } + + print 'Pipeline finished - all jobs run' . "\n"; + + $self->clean_up(); + + $self->_delete_lock(); + + return; +} + +=method init_run + + Usage : $self->init_run(); + Purpose : Initialise a pipeline run + Returns : undef + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub init_run { + my ($self) = @_; + + if ( !$self->stage_to_run && !$self->component_to_run ) { + ## no critic (RequireLocalizedPunctuationVars) + $SIG{INT} = sub { + $self->_delete_lock(); + die "\n" . 'Interrupted' . "\n"; + }; + ## use critic + $self->_create_lock(); + } + + return; +} + +=method get_and_create_stage_dir + + Usage : my $dir = $pipeline->get_and_create_stage_dir( $stage ); + Purpose : Get (and create if necessary) a directory for the current stage + Returns : String (the directory) + Parameters : DETCT::Pipeline::Stage + Throws : No exceptions + Comments : None + +=cut + +sub get_and_create_stage_dir { + my ( $self, $stage ) = @_; + + my $stage_dir = File::Spec->catdir( $self->analysis_dir, $stage->name ); + if ( !-d $stage_dir ) { + make_path($stage_dir); + } + + return $stage_dir; +} + +=method get_and_check_output_file + + Usage : my $file = $pipeline->get_and_check_output_file('run_deseq', 1); + Purpose : Get an output file for a particular component of a stage + Returns : String (the file) + Parameters : String (the stage) + Int (the component) + Throws : If output file doesn't exist + Comments : None + +=cut + +sub get_and_check_output_file { + my ( $self, $stage_name, $component ) = @_; + + my $output_file = File::Spec->catfile( $self->analysis_dir, $stage_name, + $component . '.out' ); + if ( !-e $output_file ) { + confess "$output_file doesn't exist, but should"; + } + + return $output_file; +} + +=method process_job + + Usage : $jobs_running_or_run += $pipeline->process_job($job); + Purpose : Process a job to see if needs to be submitted + Returns : Boolean + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : Returns whether or not job has been run or was already running + +=cut + +sub process_job { + my ( $self, $job ) = @_; + + my $job_running_or_run = 0; + + if ( $job->status_code eq 'NOT_RUN' ) { + + # Job not yet run so submit it + $job->stage->set_all_jobs_run(0); + $self->say_if_verbose( sprintf ' Running component %d of %s', + $job->component, $job->stage->name ); + $job_running_or_run = 1; + $self->submit_job($job); + } + elsif ( $job->status_code eq 'RUNNING' ) { + + # Job is running + $job->stage->set_all_jobs_run(0); + $self->say_if_verbose( sprintf ' Component %d of %s is still running', + $job->component, $job->stage->name ); + $job_running_or_run = 1; + } + elsif ( $job->status_code eq 'FAILED' ) { + + # Job has failed, so submit again + $job->stage->set_all_jobs_run(0); + $self->say_if_verbose( sprintf ' Component %d of %s has FAILED: %s', + $job->component, $job->stage->name, $job->status_text ); + if ( $job->retries < $self->max_retries ) { + $self->say_if_verbose( sprintf ' Running component %d of %s', + $job->component, $job->stage->name ); + $job_running_or_run = 1; + $self->submit_job($job); + } + else { + $self->say_if_verbose( + sprintf + ' Not running component %d of %s because retried %d times', + $job->component, $job->stage->name, $job->retries ); + } + } + + return $job_running_or_run; +} + +=method submit_job + + Usage : $pipeline->submit_job($job); + Purpose : Submit a job + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : If job or bsub can't be run + If job id can't be extracted from bsub output + Comments : None + +=cut + +sub submit_job { + my ( $self, $job ) = @_; + + my $stdout_file = $job->base_filename . '.o'; + my $stderr_file = $job->base_filename . '.e'; + + my $cmd = + $self->cmd_line + . ' --stage ' + . $job->stage->name + . ' --component ' + . $job->component; + + if ( $job->scheduler eq 'local' ) { + + # Just run job + $cmd .= ' 1>' . $stdout_file; + $cmd .= ' 2>' . $stderr_file; + my $cmd_status = system $cmd; + + # Die if the command was interrupted + if ( WIFSIGNALED($cmd_status) && WTERMSIG($cmd_status) == 2 ) { + $self->_delete_lock(); + die "\n" . 'Interrupted' . "\n"; + } + + # Die if the command couldn't be run + confess "Couldn't run $cmd ($OS_ERROR)" if !WIFEXITED($cmd_status); + + if ( defined $job->retries ) { + $job->set_retries( $job->retries + 1 ); + } + else { + $job->set_retries(0); + } + my $dump = { retries => $job->retries, }; + my $job_file = $job->base_filename . '.job'; + DumpFile( $job_file, $dump ); + } + elsif ( $job->scheduler eq 'lsf' ) { + + # Either use default memory or increase by 50% (if retrying failed job) + if ( !$job->memory ) { + $job->set_memory( $job->stage->default_memory ); + } + elsif ( $job->status_text =~ m/\A MEMLIMIT /xms ) { + ## no critic (ProhibitMagicNumbers) + $job->set_memory( int( $job->memory * 1.5 ) ); + ## use critic + } + + # bsub job + my $bsub_stdout_file = $job->base_filename . '.bsub.o'; + my $bsub_stderr_file = $job->base_filename . '.bsub.e'; + ## no critic (ProhibitMagicNumbers) + my $memory_clause = sprintf q{ -R'select[mem>%d] rusage[mem=%d]' -M%d }, + $job->memory, $job->memory, $job->memory * 1000; + ## use critic + $cmd = + 'bsub' . ' -oo ' + . $stdout_file . ' -eo ' + . $stderr_file + . $memory_clause + . $cmd . ' 1>' + . $bsub_stdout_file . ' 2>' + . $bsub_stderr_file; + WIFEXITED( system $cmd) or confess "Couldn't run $cmd ($OS_ERROR)"; + + # Extract job id from bsub output and store along with other parameters + my $bsub_stdout = read_file($bsub_stdout_file); + if ( $bsub_stdout =~ m/Job \s <(\d+)> \s is \s submitted/xms ) { + my $id = $1; + if ( defined $job->retries ) { + $job->set_retries( $job->retries + 1 ); + } + else { + $job->set_retries(0); + } + my $dump = { + id => $id, + retries => $job->retries, + memory => $job->memory, + }; + my $job_file = $job->base_filename . '.job'; + DumpFile( $job_file, $dump ); + } + else { + confess "Couldn't get job id from $bsub_stdout_file"; + } + } + + return; +} + +=method all_parameters + + Usage : @all_parameters = $pipeline->all_parameters( $stage ); + Purpose : Get all the parameters for a stage + Returns : Array + Parameters : DETCT::Pipeline::Stage + Throws : No exceptions + Comments : This function calls the all_parameters_for_ method associated + with the current stage and gets all the parameters for that + stage as an array of arbitrary data (e.g. arrayref or scalar) + +=cut + +sub all_parameters { + my ( $self, $stage ) = @_; + + my $sub_name = 'all_parameters_for_' . $stage->name; + + return $self->$sub_name(); +} + +=method run_job + + Usage : $pipeline->run_job($job); + Purpose : Run a job + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : This function calls the run_ method associated with the current + stage and passes along the parameters for the current component, + which are arbitrary (e.g. arrayref or scalar) + +=cut + +sub run_job { + my ( $self, $job ) = @_; + + my $sub_name = 'run_' . $job->stage->name; + + $self->$sub_name($job); + + return; +} + +=method input_overview + + Usage : $pipeline->say_if_verbose($pipeline->input_overview); + Purpose : Return textual overview of pipeline's input + Returns : Array of Strings + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub input_overview { + my ($self) = @_; + + my @output; + + push @output, 'Command line:', $self->cmd_line; + if ( defined $DETCT::VERSION ) { + push @output, 'DETCT version:' . $DETCT::VERSION; + } + push @output, 'Working directory: ' . $self->analysis_dir; + + push @output, 'BAM files: ' . join q{ }, + $self->analysis->list_all_bam_files(); + + push @output, sprintf 'Number of samples: %d', + scalar @{ $self->analysis->get_all_samples }; + push @output, sprintf 'Number of sequences: %d', + scalar @{ $self->analysis->get_all_sequences }; + push @output, sprintf 'Number of chunks: %d', $self->analysis->chunk_total; + + push @output, 'Number of sequences per chunk:'; + my $chunk_component = 0; + foreach my $chunk ( @{ $self->analysis->get_all_chunks } ) { + $chunk_component++; + push @output, sprintf " Chunk $chunk_component: %d sequences", + scalar @{$chunk}; + } + + return @output; +} + +=method say_if_verbose + + Usage : $pipeline->say_if_verbose( 'Command line:', $cmd_line ); + Purpose : Print output if pipeline is set to verbose + Returns : undef + Parameters : Array of Strings + Throws : No exceptions + Comments : Each string is a line without carriage returns or newlines + +=cut + +sub say_if_verbose { + my ( $self, @output ) = @_; + if ( $self->verbose ) { + print join "\n", @output; + print "\n"; + } + return; +} + +=method write_log_file + + Usage : $pipeline->write_log_file( @output ); + Purpose : Write data to a specified log file + Returns : undef + Parameters : String (the filename) + Array of Strings + Throws : No exceptions + Comments : None + +=cut + +sub write_log_file { + my ( $self, $filename, @output ) = @_; + + my $log_file = File::Spec->catfile( $self->analysis_dir, $filename ); + write_file( $log_file, @output ); + + return; +} + +# Usage : $self->_create_lock(); +# Purpose : Create lock file +# Returns : undef +# Parameters : None +# Throws : If lock file already exists +# Comments : None +sub _create_lock { + my ($self) = @_; + + my $lock_file = File::Spec->catfile( $self->analysis_dir, 'pipeline.lock' ); + + if ( -e $lock_file ) { + my $message = + "\nERROR: Is another pipeline running?\n" + . "Make sure before deleting $lock_file and restarting.\n" + . "Lock file contains:\n\n"; + $message .= read_file($lock_file); + die $message . "\n"; + } + + my $hostname = hostname(); + my $timestamp = localtime; + write_file( $lock_file, $hostname . "\n" . $timestamp . "\n" ); + + return; +} + +# Usage : $self->_delete_lock(); +# Purpose : Delete lock file +# Returns : undef +# Parameters : None +# Throws : No exceptions +# Comments : None +sub _delete_lock { + my ($self) = @_; + + my $lock_file = File::Spec->catfile( $self->analysis_dir, 'pipeline.lock' ); + + unlink $lock_file; + + return; +} + +=method clean_up + + Usage : $self->clean_up(); + Purpose : Move results, archive stages and delete data + Returns : undef + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub clean_up { + my ($self) = @_; + + # Already cleaned up? + my $done_marker_file = + File::Spec->catfile( $self->analysis_dir, 'cleanup.done' ); + return if -e $done_marker_file; + + print 'Cleaning up...' . "\n"; + + # Tar all stages + my @stage_dirs = + map { $self->get_and_create_stage_dir($_) } @{ $self->get_all_stages() }; + my $tarball_file = + File::Spec->catfile( $self->analysis_dir, 'archive.tar.gz' ); + my $cmd = join q{ }, 'tar', 'cf', q{-}, @stage_dirs, q{|}, 'gzip', '-9', + '-c', q{>}, $tarball_file; + WIFEXITED( system $cmd) or confess "Couldn't run $cmd ($OS_ERROR)"; + + # Delete or move files + my $wanted = \&_move_or_delete; + find( + { + wanted => sub { $wanted->( $self->analysis_dir ) }, + postprocess => sub { rmdir $File::Find::dir }, + no_chdir => 1, + }, + @stage_dirs + ); + + write_file( $done_marker_file, '1' ); + + print 'Done' . "\n"; + + return; +} + +# Usage : find(\&_move_or_delete, $dir); +# Purpose : Move results files and delete other files +# Returns : undef +# Parameters : None +# Throws : No exceptions +# Comments : None +sub _move_or_delete { + my ($archive_dir) = @_; + + return if -d; # Ignore directories + + my ( $filename, undef, $extension ) = fileparse($File::Find::name); + + # Move or delete? + if ( $EXTENSION_TO_KEEP{$extension} ) { + rename $File::Find::name, + File::Spec->catfile( $archive_dir, $filename ); + } + else { + unlink $File::Find::name; + } + + return; +} + +1; diff --git a/lib/DETCT/Pipeline/Job.pm b/lib/DETCT/Pipeline/Job.pm new file mode 100644 index 0000000..ce8f348 --- /dev/null +++ b/lib/DETCT/Pipeline/Job.pm @@ -0,0 +1,697 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Pipeline::Job; +## use critic + +# ABSTRACT: Object representing a pipeline job + +## Author : is1 +## Maintainer : is1 +## Created : 2013-01-17 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Readonly; +use Class::InsideOut qw( private register id ); +use English qw( -no_match_vars ); +use File::ReadBackwards; +use YAML::Tiny qw( LoadFile ); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Attributes: +private stage => my %stage; # DETCT::Pipeline::Stage object +private component => my %component; # e.g. 2 +private scheduler => my %scheduler; # e.g. lsf +private base_filename => my %base_filename; # e.g. ./run_deseq/1 +private parameters => my %parameters; # e.g. arrayref or scalar +private retries => my %retries; # e.g. 5 +private memory => my %memory; # e.g. 3000 +private status_code => my %status_code; # e.g. DONE +private status_text => my %status_text; # e.g. Job killed by owner + +# Constants +Readonly our %STATUS_FOR => ( + PEND => 'RUNNING', + PSUSP => 'RUNNING', + RUN => 'RUNNING', + USUSP => 'RUNNING', + SSUSP => 'RUNNING', + WAIT => 'RUNNING', + EXIT => 'FAILED', + UNKWN => 'FAILED', + ZOMBI => 'FAILED', + DONE => 'DONE', +); + +=method new + + Usage : my $job = DETCT::Pipeline::Job->new( { + stage => $stage, + component => 2, + scheduler => 'lsf', + base_filename => './run_deseq/1', + parameters => $parameters, + } ); + Purpose : Constructor for job objects + Returns : DETCT::Pipeline::Job + Parameters : Hashref { + stage => DETCT::Pipeline::Stage, + component => Int, + scheduler => String, + base_filename => String, + parameters => Any (probably arrayref or scalar) + } + Throws : No exceptions + Comments : None + +=cut + +sub new { + my ( $class, $arg_ref ) = @_; + my $self = register($class); + $self->set_stage( $arg_ref->{stage} ); + $self->set_component( $arg_ref->{component} ); + $self->set_scheduler( $arg_ref->{scheduler} ); + $self->set_base_filename( $arg_ref->{base_filename} ); + $self->set_parameters( $arg_ref->{parameters} ); + $self->set_state_from_filesystem(); + return $self; +} + +=method stage + + Usage : my $stage = $job->stage; + Purpose : Getter for stage attribute + Returns : DETCT::Pipeline::Stage + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub stage { + my ($self) = @_; + return $stage{ id $self}; +} + +=method set_stage + + Usage : $job->set_stage($stage); + Purpose : Setter for stage attribute + Returns : undef + Parameters : DETCT::Pipeline::Stage + Throws : No exceptions + Comments : None + +=cut + +sub set_stage { + my ( $self, $arg ) = @_; + $stage{ id $self} = _check_stage($arg); + return; +} + +# Usage : $stage = _check_stage($stage); +# Purpose : Check for valid stage object +# Returns : DETCT::Pipeline::Stage +# Parameters : DETCT::Pipeline::Stage +# Throws : If stage object is missing or invalid (i.e. not a +# DETCT::Pipeline::Stage object) +# Comments : None +sub _check_stage { + my ($stage) = @_; + return $stage if defined $stage && $stage->isa('DETCT::Pipeline::Stage'); + confess 'No stage specified' if !defined $stage; + confess 'Class of stage (', ref $stage, ') not DETCT::Pipeline::Stage'; +} + +=method component + + Usage : my $component = $job->component; + Purpose : Getter for component attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub component { + my ($self) = @_; + return $component{ id $self}; +} + +=method set_component + + Usage : $job->set_component(2); + Purpose : Setter for component attribute + Returns : undef + Parameters : +ve Int (the component) + Throws : No exceptions + Comments : None + +=cut + +sub set_component { + my ( $self, $arg ) = @_; + $component{ id $self} = _check_component($arg); + return; +} + +# Usage : $component = _check_component($component); +# Purpose : Check for valid component +# Returns : +ve Int (the valid component) +# Parameters : +ve Int (the component) +# Throws : If component is missing or not a positive integer +# Comments : None +sub _check_component { + my ($component) = @_; + return $component if defined $component && $component =~ m/\A \d+ \z/xms; + confess 'No component specified' if !defined $component; + confess "Invalid component ($component) specified"; +} + +=method scheduler + + Usage : my $scheduler = $job->scheduler; + Purpose : Getter for scheduler attribute + Returns : String (e.g. "lsf") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub scheduler { + my ($self) = @_; + return $scheduler{ id $self}; +} + +=method set_scheduler + + Usage : $job->set_scheduler('lsf'); + Purpose : Setter for scheduler attribute + Returns : undef + Parameters : String (the scheduler) + Throws : No exceptions + Comments : None + +=cut + +sub set_scheduler { + my ( $self, $arg ) = @_; + $scheduler{ id $self} = _check_scheduler($arg); + return; +} + +# Usage : $scheduler = _check_scheduler($scheduler); +# Purpose : Check for valid scheduler +# Returns : String (the valid scheduler) +# Parameters : String (the scheduler) +# Throws : If scheduler is not lsf or local +# Comments : None +sub _check_scheduler { + my ($scheduler) = @_; + + confess 'Invalid scheduler specified' + if !defined $scheduler + || ( $scheduler ne 'lsf' && $scheduler ne 'local' ); + + return $scheduler; +} + +=method base_filename + + Usage : my $base_filename = $job->base_filename; + Purpose : Getter for the base filename attribute + Returns : String (e.g. "./run_deseq/1") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub base_filename { + my ($self) = @_; + return $base_filename{ id $self}; +} + +=method set_base_filename + + Usage : $job->set_base_filename('./run_deseq/1'); + Purpose : Setter for the base filename attribute + Returns : undef + Parameters : String (the base filename) + Throws : No exceptions + Comments : None + +=cut + +sub set_base_filename { + my ( $self, $arg ) = @_; + $base_filename{ id $self} = _check_base_filename($arg); + return; +} + +# Usage : $base_filename = _check_base_filename($base_filename); +# Purpose : Check for valid base filename +# Returns : String (the valid base filename) or undef +# Parameters : String (the base filename) +# Throws : If base filename is missing +# Comments : None +sub _check_base_filename { + my ($base_filename) = @_; + + confess 'No base filename specified' + if !defined $base_filename || !$base_filename; + + return $base_filename; +} + +=method parameters + + Usage : my $parameters = $job->parameters; + Purpose : Getter for parameters attribute + Returns : Any (usually arrayref or scalar) + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub parameters { + my ($self) = @_; + return $parameters{ id $self}; +} + +=method set_parameters + + Usage : $job->set_parameters($parameters); + Purpose : Setter for parameters attribute + Returns : undef + Parameters : Any (the parameters; usually arrayref or scalar) + Throws : No exceptions + Comments : None + +=cut + +sub set_parameters { + my ( $self, $arg ) = @_; + $parameters{ id $self} = $arg; + return; +} + +=method retries + + Usage : my $retries = $job->retries; + Purpose : Getter for retries attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub retries { + my ($self) = @_; + return $retries{ id $self}; +} + +=method set_retries + + Usage : $job->set_retries(5); + Purpose : Setter for retries attribute + Returns : undef + Parameters : +ve Int (the retries) + Throws : No exceptions + Comments : None + +=cut + +sub set_retries { + my ( $self, $arg ) = @_; + $retries{ id $self} = _check_retries($arg); + return; +} + +# Usage : $retries = _check_retries($retries); +# Purpose : Check for valid retries +# Returns : +ve Int (the valid retries) +# Parameters : +ve Int (the retries) +# Throws : If retries is not a positive integer +# Comments : None +sub _check_retries { + my ($retries) = @_; + + confess "Invalid retries ($retries) specified" + if defined $retries && $retries !~ m/\A \d+ \z/xms; + + return $retries; +} + +=method memory + + Usage : my $memory = $job->memory; + Purpose : Getter for memory attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub memory { + my ($self) = @_; + return $memory{ id $self}; +} + +=method set_memory + + Usage : $job->set_memory(1000); + Purpose : Setter for memory attribute + Returns : undef + Parameters : +ve Int (the memory) + Throws : No exceptions + Comments : None + +=cut + +sub set_memory { + my ( $self, $arg ) = @_; + $memory{ id $self} = _check_memory($arg); + return; +} + +# Usage : $memory = _check_memory($memory); +# Purpose : Check for valid memory +# Returns : +ve Int (the valid memory) +# Parameters : +ve Int (the memory) +# Throws : If memory is not a positive integer +# Comments : None +sub _check_memory { + my ($memory) = @_; + + confess "Invalid memory ($memory) specified" + if defined $memory && $memory !~ m/\A \d+ \z/xms; + + return $memory; +} + +=method status_code + + Usage : my $status_code = $job->status_code; + Purpose : Getter for the status code attribute + Returns : String (e.g. "DONE") + Parameters : None + Throws : No exceptions + Comments : Status code can be RUNNING, FAILED, DONE or NOT_RUN + +=cut + +sub status_code { + my ($self) = @_; + return $status_code{ id $self}; +} + +=method set_status_code + + Usage : $job->set_status_code('DONE'); + Purpose : Setter for the status code attribute + Returns : undef + Parameters : String (the status code) + Throws : No exceptions + Comments : None + +=cut + +sub set_status_code { + my ( $self, $arg ) = @_; + $status_code{ id $self} = _check_status_code($arg); + return; +} + +# Usage : $status_code = _check_status_code($status_code); +# Purpose : Check for valid status code +# Returns : String (the valid status code) +# Parameters : String (the status code) +# Throws : If status code is not valid +# Comments : None +sub _check_status_code { + my ($status_code) = @_; + + return $status_code + if defined $status_code + && ( $status_code eq 'RUNNING' + || $status_code eq 'FAILED' + || $status_code eq 'DONE' + || $status_code eq 'NOT_RUN' ); + confess 'No status code specified' if !defined $status_code; + confess "Invalid status code ($status_code) specified"; +} + +=method status_text + + Usage : my $status_text = $job->status_text; + Purpose : Getter for status text attribute + Returns : String (e.g. "Job killed by owner") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub status_text { + my ($self) = @_; + return $status_text{ id $self}; +} + +=method set_status_text + + Usage : $job->set_status_text('Job killed by owner'); + Purpose : Setter for status text attribute + Returns : undef + Parameters : String (the status text) + Throws : No exceptions + Comments : None + +=cut + +sub set_status_text { + my ( $self, $arg ) = @_; + $status_text{ id $self} = $arg; + return; +} + +=method set_state_from_filesystem + + Usage : $job->set_state_from_filesystem(); + Purpose : Set state-related attributes of a job from filesystem + Returns : undef + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub set_state_from_filesystem { + my ($self) = @_; + + if ( $self->scheduler eq 'local' ) { + $self->_set_state_from_filesystem_for_local(); + } + elsif ( $self->scheduler eq 'lsf' ) { + $self->_set_state_from_filesystem_for_lsf(); + } + + return; +} + +# Usage : $self->_set_state_from_filesystem_for_local(); +# Purpose : Set state-related attributes of a job run locally (probably for +# testing) +# Returns : None +# Parameters : None +# Throws : No exceptions +# Comments : None +sub _set_state_from_filesystem_for_local { + my ($self) = @_; + + my $output_file = $self->base_filename . '.out'; + + my $job_file = $self->base_filename . '.job'; + + # Check if job has even been run yet + if ( !-e $job_file ) { + $self->set_status_code('NOT_RUN'); + return; + } + + # Get number of retries + my $yaml = LoadFile($job_file); + my $retries = $yaml->{retries}; + $self->set_retries($retries); + + if ( -e $output_file && !-z $output_file ) { + $self->set_status_code('DONE'); + } + elsif ( !-e $output_file || -z $output_file ) { + $self->set_status_code('FAILED'); + $self->set_status_text( 'Enpty output file: ' . $output_file ); + } + + return; +} + +# Usage : $self->_set_state_from_filesystem_for_lsf(); +# Purpose : Set state-related attributes of a job submitted to LSF +# Returns : None +# Parameters : None +# Throws : If job id in job file is not an integer +# If the status returned by bjobs is not recognised +# Comments : None +sub _set_state_from_filesystem_for_lsf { + my ($self) = @_; + + my $output_file = $self->base_filename . '.out'; + + my $job_file = $self->base_filename . '.job'; + + # Check if job has even been run yet + if ( !-e $job_file ) { + $self->set_status_code('NOT_RUN'); + return; + } + + # Get job id + my $yaml = LoadFile($job_file); + my $job_id = $yaml->{id}; + if ( $job_id !~ /\A \d+ \z/xms ) { + confess "Job ID ($job_id) not valid"; + } + + # Get number of retries + my $retries = $yaml->{retries}; + $self->set_retries($retries); + + # Get memory requested + my $memory = $yaml->{memory}; + $self->set_memory($memory); + + my ( $status_code, $status_text ); + + # Get job status for job id from bjobs command + my $lsf_status; + open my $pipe, q{-|}, 'bjobs ' . $job_id . ' 2>/dev/null'; # Hide STDERR + while ( my $job_line = <$pipe> ) { + if ( $job_line =~ m/\A $job_id \s+ \S+ \s+ (\S+)/xms ) { + $lsf_status = $1; + } + } + close $pipe; + if ($lsf_status) { + + # Got job status from bjobs + if ( !exists $STATUS_FOR{$lsf_status} ) { + confess "Unknown LSF status ($lsf_status)"; + } + $status_code = $STATUS_FOR{$lsf_status}; + $status_text = 'LSF status: ' . $lsf_status; + } + if ( !$status_code || $status_code eq 'FAILED' ) { + + # If bjobs doesn't return status or failed then check job's STDOUT + ( $status_code, $status_text ) = $self->_parse_lsf_stdout($job_id); + } + + $self->set_status_code($status_code); + $self->set_status_text($status_text); + + return; +} + +# Usage : ($status_code, $status_text) +# = $pipeline->_parse_lsf_stdout($job_id); +# Purpose : Parses LSF's STDOUT to get a job's status +# Returns : String (status code: DONE or FAILED) +# String (status info) or undef +# Parameters : Int (the job id) +# Throws : If STDOUT file can't be read +# Comments : Based on +# https://github.com/VertebrateResequencing/vr-pipe/blob/master/modules/VRPipe/Parser/lsf.pm +sub _parse_lsf_stdout { + my ( $self, $job_id ) = @_; + + # Check STDOUT file exists at all (in case job was killed whilst pending) + my $stdout_file = $self->base_filename . '.o'; + if ( !-e $stdout_file ) { + return 'FAILED', 'Job did not run'; + } + + # STDOUT file is overwritten so no need to read backwards to get last job + my ( $status_code, $status_text, $stdout_job_id ); + my $found_start = 0; + my $found_end = 0; + my $bw = File::ReadBackwards->new($stdout_file) + or confess "Can't read $stdout_file: $OS_ERROR"; + while ( defined( my $line = $bw->readline ) ) { + if ( $line =~ m/\A Resource \s usage \s summary: /xms ) { + $found_end = 1; + next; + } + elsif ( $line =~ m/\A Sender: \s LSF \s System/xms ) { + $found_start = 1; + last; # Will find start after end + } + elsif ($found_end) { + + # Get job id + if ( $line =~ m/\A Subject: \s Job \s (\d+):/xms ) { + $stdout_job_id = $1; + } + + # Get job's status code + if ( $line =~ m/\A Successfully \s completed[.] /xms ) { + $status_code = 'DONE'; + } + elsif ( !$status_code + && $line =~ + m/\A Exited \s with \s exit \s code \s (\d+) [.] /xms ) + { + $status_code = 'FAILED'; + $status_text = "Exit code: $1"; + } + elsif ( $line =~ m/\A TERM_ (\w+: .*) [.] /xms ) { + $status_code = 'FAILED'; + $status_text = $1; + } + } + } + + # Ensure correct job + if ( defined $stdout_job_id && $job_id != $stdout_job_id ) { + $status_code = 'FAILED'; + $status_text = "Wrong job id (expecting $job_id, got $stdout_job_id)"; + } + + # If no status then STDOUT could not be parsed + if ( !defined $status_code ) { + $status_code = 'FAILED'; + $status_text = "Could not parse job's STDOUT: $stdout_file"; + } + + return $status_code, $status_text; +} + +1; diff --git a/lib/DETCT/Pipeline/Stage.pm b/lib/DETCT/Pipeline/Stage.pm new file mode 100644 index 0000000..f626279 --- /dev/null +++ b/lib/DETCT/Pipeline/Stage.pm @@ -0,0 +1,236 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Pipeline::Stage; +## use critic + +# ABSTRACT: Object representing a pipeline stage + +## Author : is1 +## Maintainer : is1 +## Created : 2013-01-09 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Class::InsideOut qw( private register id ); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Attributes: +private name => my %name; # e.g. count_tags +private default_memory => my %default_memory; # e.g. 3000 +private all_jobs_run => my %all_jobs_run; # e.g. 1 +private prerequisite => my %prerequisite; # arrayref of stages + +=method new + + Usage : my $stage = DETCT::Pipeline::Stage->new( { + name => 'count_tags', + default_memory => 3000, + } ); + Purpose : Constructor for stage objects + Returns : DETCT::Pipeline::Stage + Parameters : Hashref { + name => String, + default_memory => Int, + all_jobs_run => Boolean or undef, + } + Throws : No exceptions + Comments : None + +=cut + +sub new { + my ( $class, $arg_ref ) = @_; + my $self = register($class); + $self->set_name( $arg_ref->{name} ); + $self->set_default_memory( $arg_ref->{default_memory} ); + $self->set_all_jobs_run( $arg_ref->{all_jobs_run} ); + return $self; +} + +=method name + + Usage : my $name = $sample->name; + Purpose : Getter for name attribute + Returns : String (e.g. "count_tags") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub name { + my ($self) = @_; + return $name{ id $self}; +} + +=method set_name + + Usage : $sample->set_name('count_tags'); + Purpose : Setter for name attribute + Returns : undef + Parameters : String (the name) + Throws : No exceptions + Comments : None + +=cut + +sub set_name { + my ( $self, $arg ) = @_; + $name{ id $self} = _check_name($arg); + return; +} + +# Usage : $name = _check_name($name); +# Purpose : Check for valid name +# Returns : String (the valid name) +# Parameters : String (the name) +# Throws : If name is missing or invalid (i.e. not alphanumeric) +# Comments : None +sub _check_name { + my ($name) = @_; + + return $name if defined $name && $name =~ m/\A \w+ \z/xms; + confess 'No name specified' if !defined $name; + confess "Invalid name ($name) specified"; +} + +=method default_memory + + Usage : my $default_memory = $stage->default_memory; + Purpose : Getter for default memory attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub default_memory { + my ($self) = @_; + return $default_memory{ id $self}; +} + +=method set_default_memory + + Usage : $stage->set_default_memory(3000); + Purpose : Setter for default memory attribute + Returns : undef + Parameters : +ve Int (the default memory) + Throws : No exceptions + Comments : None + +=cut + +sub set_default_memory { + my ( $self, $arg ) = @_; + $default_memory{ id $self} = _check_default_memory($arg); + return; +} + +# Usage : $default_memory = _check_default_memory($default_memory); +# Purpose : Check for valid default memory +# Returns : +ve Int (the valid default memory) +# Parameters : +ve Int (the default memory) +# Throws : If default memory is missing or not a positive integer +# Comments : None +sub _check_default_memory { + my ($default_memory) = @_; + return $default_memory + if defined $default_memory && $default_memory =~ m/\A \d+ \z/xms; + confess 'No default memory specified' if !defined $default_memory; + confess "Invalid default memory ($default_memory) specified"; +} + +=method all_jobs_run + + Usage : my $all_jobs_run = $stage->all_jobs_run; + Purpose : Getter for all jobs run flag + Returns : Boolean + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_jobs_run { + my ($self) = @_; + return $all_jobs_run{ id $self} || 0; +} + +=method set_all_jobs_run + + Usage : $stage->set_all_jobs_run(1); + Purpose : Setter for all jobs run flag + Returns : undef + Parameters : Boolean + Throws : No exceptions + Comments : None + +=cut + +sub set_all_jobs_run { + my ( $self, $arg ) = @_; + $all_jobs_run{ id $self} = $arg ? 1 : 0; + return; +} + +=method add_prerequisite + + Usage : $stage->add_prerequisite($prerequisite); + Purpose : Add a prerequisite to a stage + Returns : undef + Parameters : DETCT::Pipeline::Stage + Throws : If prerequisite is missing or invalid (i.e. not a + DETCT::Pipeline::Stage object) + Comments : None + +=cut + +sub add_prerequisite { + my ( $self, $prerequisite ) = @_; + + confess 'No prerequisite specified' if !defined $prerequisite; + confess 'Class of prerequisite (', ref $prerequisite, + ') not DETCT::Pipeline::Stage' + if !$prerequisite->isa('DETCT::Pipeline::Stage'); + + if ( !exists $prerequisite{ id $self} ) { + $prerequisite{ id $self} = [$prerequisite]; + } + else { + push @{ $prerequisite{ id $self} }, $prerequisite; + } + + return; +} + +=method get_all_prerequisites + + Usage : $prerequisites = $stage->get_all_prerequisites(); + Purpose : Get all prerequisites of a stage + Returns : Arrayref of DETCT::Pipeline::Stage objects + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub get_all_prerequisites { + my ($self) = @_; + + return $prerequisite{ id $self} || []; +} + +1; diff --git a/lib/DETCT/Pipeline/WithDiffExprStages.pm b/lib/DETCT/Pipeline/WithDiffExprStages.pm new file mode 100644 index 0000000..5f2161d --- /dev/null +++ b/lib/DETCT/Pipeline/WithDiffExprStages.pm @@ -0,0 +1,1236 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Pipeline::WithDiffExprStages; +## use critic + +# ABSTRACT: Object representing a differential expression pipeline + +## Author : is1 +## Maintainer : is1 +## Created : 2013-01-16 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use parent qw(DETCT::Pipeline); + +use Class::InsideOut qw( private register id ); +use Scalar::Util qw( refaddr ); +use YAML::Tiny qw( DumpFile LoadFile ); +use DETCT::GeneFinder; +use DETCT::Misc::BAM qw( + count_tags + bin_reads + get_read_peaks + get_three_prime_ends + merge_three_prime_ends + filter_three_prime_ends + choose_three_prime_end + count_reads + merge_read_counts +); +use DETCT::Misc::PeakHMM qw( + merge_read_peaks + summarise_read_peaks + run_peak_hmm + join_hmm_bins +); +use DETCT::Misc::R qw( + run_deseq +); +use DETCT::Misc::Output qw( + dump_as_table +); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +=method all_parameters_by_bam_file_then_chunk + + Usage : all_parameters_by_bam_file_then_chunk(); + Purpose : Get all parameters for stage that requires jobs split up by BAM + file then by chunk + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_by_bam_file_then_chunk { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + foreach my $bam_file ( $self->analysis->list_all_bam_files() ) { + my @tags = $self->analysis->list_all_tags_by_bam_file($bam_file); + foreach my $chunk ( @{$chunks} ) { + push @all_parameters, [ $bam_file, $chunk, @tags ]; + } + } + + return @all_parameters; +} + +=method all_parameters_for_count_tags + + Usage : all_parameters_for_count_tags(); + Purpose : Get all parameters for count_tags stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_count_tags { + my ($self) = @_; + + return $self->all_parameters_by_bam_file_then_chunk(); +} + +=method run_count_tags + + Usage : run_count_tags(); + Purpose : Run function for count_tags stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_count_tags { + my ( $self, $job ) = @_; + + my ( $bam_file, $chunk, @tags ) = @{ $job->parameters }; + + my %chunk_count; + + # Get count for each sequence of a chunk separately and then merge + foreach my $seq ( @{$chunk} ) { + my $seq_count = count_tags( + { + bam_file => $bam_file, + mismatch_threshold => $self->analysis->mismatch_threshold, + seq_name => $seq->name, + tags => \@tags, + } + ); + %chunk_count = + %{ $self->hash_merge->merge( \%chunk_count, $seq_count ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_count ); + + return; +} + +=method all_parameters_for_bin_reads + + Usage : all_parameters_for_bin_reads(); + Purpose : Get all parameters for bin_reads stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_bin_reads { + my ($self) = @_; + + return $self->all_parameters_by_bam_file_then_chunk(); +} + +=method run_bin_reads + + Usage : run_bin_reads(); + Purpose : Run function for bin_reads stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_bin_reads { + my ( $self, $job ) = @_; + + my ( $bam_file, $chunk, @tags ) = @{ $job->parameters }; + + my %chunk_bins; + + # Get bins for each sequence of a chunk separately and then merge + foreach my $seq ( @{$chunk} ) { + my $seq_bins = bin_reads( + { + bam_file => $bam_file, + mismatch_threshold => $self->analysis->mismatch_threshold, + bin_size => $self->analysis->bin_size, + seq_name => $seq->name, + tags => \@tags, + } + ); + %chunk_bins = %{ $self->hash_merge->merge( \%chunk_bins, $seq_bins ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_bins ); + + return; +} + +=method all_parameters_for_get_read_peaks + + Usage : all_parameters_for_get_read_peaks(); + Purpose : Get all parameters for get_read_peaks stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_get_read_peaks { + my ($self) = @_; + + return $self->all_parameters_by_bam_file_then_chunk(); +} + +=method run_get_read_peaks + + Usage : run_get_read_peaks(); + Purpose : Run function for get_read_peaks stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_get_read_peaks { + my ( $self, $job ) = @_; + + my ( $bam_file, $chunk, @tags ) = @{ $job->parameters }; + + my %chunk_peaks; + + # Get read peaks for each sequence of a chunk separately and then merge + foreach my $seq ( @{$chunk} ) { + my $seq_peaks = get_read_peaks( + { + bam_file => $bam_file, + mismatch_threshold => $self->analysis->mismatch_threshold, + peak_buffer_width => $self->analysis->peak_buffer_width, + seq_name => $seq->name, + tags => \@tags, + } + ); + %chunk_peaks = + %{ $self->hash_merge->merge( \%chunk_peaks, $seq_peaks ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_peaks ); + + return; +} + +=method all_parameters_for_merge_read_peaks + + Usage : all_parameters_for_merge_read_peaks(); + Purpose : Get all parameters for merge_read_peaks stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_merge_read_peaks { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + # Work out which get_read_peaks stage files need to be combined + foreach my $merge_chunk ( @{$chunks} ) { + my @get_read_peaks_output_files; + my $component = 0; + foreach my $bam_file ( $self->analysis->list_all_bam_files() ) { + foreach my $get_chunk ( @{$chunks} ) { + $component++; + if ( refaddr($merge_chunk) == refaddr($get_chunk) ) { + my $output_file = + $self->get_and_check_output_file( 'get_read_peaks', + $component ); + push @get_read_peaks_output_files, $output_file; + } + } + } + push @all_parameters, [ $merge_chunk, @get_read_peaks_output_files ]; + } + + return @all_parameters; +} + +=method run_merge_read_peaks + + Usage : run_merge_read_peaks(); + Purpose : Run function for merge_read_peaks stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_merge_read_peaks { + my ( $self, $job ) = @_; + + my ( $chunk, @get_read_peaks_output_files ) = @{ $job->parameters }; + + # Join lists of peaks + my %unmerged_peaks; + foreach my $output_file (@get_read_peaks_output_files) { + %unmerged_peaks = %{ + $self->hash_merge->merge( + \%unmerged_peaks, LoadFile($output_file) + ) + }; + } + + my %chunk_peaks; + + # Merge read peaks for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + my $seq_peaks = merge_read_peaks( + { + peak_buffer_width => $self->analysis->peak_buffer_width, + seq_name => $seq->name, + peaks => $unmerged_peaks{ $seq->name }, + } + ); + %chunk_peaks = + %{ $self->hash_merge->merge( \%chunk_peaks, $seq_peaks ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_peaks ); + + return; +} + +=method all_parameters_for_summarise_read_peaks + + Usage : all_parameters_for_summarise_read_peaks(); + Purpose : Get all parameters for summarise_read_peaks stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_summarise_read_peaks { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + my $component = 0; + foreach my $chunk ( @{$chunks} ) { + $component++; + my $merge_read_peaks_output_file = + $self->get_and_check_output_file( 'merge_read_peaks', $component ); + push @all_parameters, [ $chunk, $merge_read_peaks_output_file ]; + } + + return @all_parameters; +} + +=method run_summarise_read_peaks + + Usage : run_summarise_read_peaks(); + Purpose : Run function for summarise_read_peaks stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_summarise_read_peaks { + my ( $self, $job ) = @_; + + my ( $chunk, $merge_read_peaks_output_file ) = @{ $job->parameters }; + + # Get merged peaks + my %merged_peaks = %{ LoadFile($merge_read_peaks_output_file) }; + + my %chunk_summary; + + # Summarise read peaks for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + my $seq_summary = summarise_read_peaks( + { + bin_size => $self->analysis->bin_size, + peak_buffer_width => $self->analysis->peak_buffer_width, + hmm_sig_level => $self->analysis->hmm_sig_level, + seq_name => $seq->name, + seq_bp => $seq->bp, + read_length => $self->analysis->read2_length, + peaks => $merged_peaks{ $seq->name }, + } + ); + %chunk_summary = + %{ $self->hash_merge->merge( \%chunk_summary, $seq_summary ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_summary ); + + return; +} + +=method all_parameters_for_run_peak_hmm + + Usage : all_parameters_for_run_peak_hmm(); + Purpose : Get all parameters for run_peak_hmm stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_run_peak_hmm { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + # Work out which bin_reads stage files need to be combined + my $component = 0; + foreach my $hmm_chunk ( @{$chunks} ) { + $component++; + my @bin_reads_output_files; + my $bin_component = 0; + foreach my $bam_file ( $self->analysis->list_all_bam_files() ) { + foreach my $bin_chunk ( @{$chunks} ) { + $bin_component++; + if ( refaddr($hmm_chunk) == refaddr($bin_chunk) ) { + my $bin_output_file = + $self->get_and_check_output_file( 'bin_reads', + $bin_component ); + push @bin_reads_output_files, $bin_output_file; + } + } + } + my $summary_output_file = + $self->get_and_check_output_file( 'summarise_read_peaks', + $component ); + push @all_parameters, + [ $hmm_chunk, $summary_output_file, @bin_reads_output_files ]; + } + + return @all_parameters; +} + +=method run_run_peak_hmm + + Usage : run_run_peak_hmm(); + Purpose : Run function for run_peak_hmm stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_run_peak_hmm { + my ( $self, $job ) = @_; + + my ( $chunk, $summary_output_file, @bin_reads_output_files ) = + @{ $job->parameters }; + + # Join read bins + my %read_bins; + foreach my $output_file (@bin_reads_output_files) { + %read_bins = + %{ $self->hash_merge->merge( \%read_bins, LoadFile($output_file) ) }; + } + + # Load summary + my $summary = LoadFile($summary_output_file); + + my %chunk_hmm; + + # Run peak HMM for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + my $seq_hmm = run_peak_hmm( + { + dir => $job->base_filename, + hmm_sig_level => $self->analysis->hmm_sig_level, + seq_name => $seq->name, + read_bins => $read_bins{ $seq->name }, + summary => $summary->{ $seq->name }, + hmm_binary => $self->analysis->hmm_binary, + } + ); + %chunk_hmm = %{ $self->hash_merge->merge( \%chunk_hmm, $seq_hmm ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_hmm ); + + return; +} + +=method all_parameters_for_join_hmm_bins + + Usage : all_parameters_for_join_hmm_bins(); + Purpose : Get all parameters for join_hmm_bins stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_join_hmm_bins { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + my $component = 0; + foreach my $chunk ( @{$chunks} ) { + $component++; + my $run_peak_hmm_output_file = + $self->get_and_check_output_file( 'run_peak_hmm', $component ); + push @all_parameters, [ $chunk, $run_peak_hmm_output_file ]; + } + + return @all_parameters; +} + +=method run_join_hmm_bins + + Usage : run_join_hmm_bins(); + Purpose : Run function for join_hmm_bins stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_join_hmm_bins { + my ( $self, $job ) = @_; + + my ( $chunk, $run_peak_hmm_output_file ) = @{ $job->parameters }; + + # Get HMM bins + my $hmm_bins = LoadFile($run_peak_hmm_output_file); + + my %chunk_regions; + + # Join HMM bins for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + my $seq_regions = join_hmm_bins( + { + bin_size => $self->analysis->bin_size, + seq_name => $seq->name, + hmm_bins => $hmm_bins->{ $seq->name }, + } + ); + %chunk_regions = + %{ $self->hash_merge->merge( \%chunk_regions, $seq_regions ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_regions ); + + return; +} + +=method all_parameters_for_get_three_prime_ends + + Usage : all_parameters_for_get_three_prime_ends(); + Purpose : Get all parameters for get_three_prime_ends stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_get_three_prime_ends { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + foreach my $bam_file ( $self->analysis->list_all_bam_files() ) { + my @tags = $self->analysis->list_all_tags_by_bam_file($bam_file); + my $component = 0; + foreach my $chunk ( @{$chunks} ) { + $component++; + my $join_hmm_bins_output_file = + $self->get_and_check_output_file( 'join_hmm_bins', $component ); + push @all_parameters, + [ $chunk, $bam_file, $join_hmm_bins_output_file, @tags ]; + } + } + + return @all_parameters; +} + +=method run_get_three_prime_ends + + Usage : run_get_three_prime_ends(); + Purpose : Run function for get_three_prime_ends stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_get_three_prime_ends { + my ( $self, $job ) = @_; + + my ( $chunk, $bam_file, $join_hmm_bins_output_file, @tags ) = + @{ $job->parameters }; + + # Get regions + my $regions = LoadFile($join_hmm_bins_output_file); + + my %chunk_regions; + + # Get 3' ends for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + my $seq_regions = get_three_prime_ends( + { + bam_file => $bam_file, + mismatch_threshold => $self->analysis->mismatch_threshold, + seq_name => $seq->name, + tags => \@tags, + regions => $regions->{ $seq->name }, + } + ); + %chunk_regions = + %{ $self->hash_merge->merge( \%chunk_regions, $seq_regions ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_regions ); + + return; +} + +=method all_parameters_for_merge_three_prime_ends + + Usage : all_parameters_for_merge_three_prime_ends(); + Purpose : Get all parameters for merge_three_prime_ends stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_merge_three_prime_ends { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + # Work out which get_three_prime_ends stage files need to be merged + foreach my $merge_chunk ( @{$chunks} ) { + my @get_three_prime_ends_output_files; + my $component = 0; + foreach my $bam_file ( $self->analysis->list_all_bam_files() ) { + foreach my $run_chunk ( @{$chunks} ) { + $component++; + if ( refaddr($merge_chunk) == refaddr($run_chunk) ) { + my $output_file = + $self->get_and_check_output_file( 'get_three_prime_ends', + $component ); + push @get_three_prime_ends_output_files, $output_file; + } + } + } + push @all_parameters, + [ $merge_chunk, @get_three_prime_ends_output_files ]; + } + + return @all_parameters; +} + +=method run_merge_three_prime_ends + + Usage : run_merge_three_prime_ends(); + Purpose : Run function for merge_three_prime_ends stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_merge_three_prime_ends { + my ( $self, $job ) = @_; + + my ( $chunk, @get_three_prime_ends_output_files ) = @{ $job->parameters }; + + # Load all regions + my @list_of_lists_of_regions; + foreach my $output_file (@get_three_prime_ends_output_files) { + my $regions = LoadFile($output_file); + push @list_of_lists_of_regions, $regions; + } + + my %chunk_regions; + + # Merge 3' ends for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + my @regions = map { $_->{ $seq->name } } @list_of_lists_of_regions; + my $seq_regions = merge_three_prime_ends( + { + seq_name => $seq->name, + regions => \@regions, + } + ); + %chunk_regions = + %{ $self->hash_merge->merge( \%chunk_regions, $seq_regions ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_regions ); + + return; +} + +=method all_parameters_for_filter_three_prime_ends + + Usage : all_parameters_for_filter_three_prime_ends(); + Purpose : Get all parameters for filter_three_prime_ends stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_filter_three_prime_ends { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + my $component = 0; + foreach my $chunk ( @{$chunks} ) { + $component++; + my $merge_three_prime_ends_output_file = + $self->get_and_check_output_file( 'merge_three_prime_ends', + $component ); + push @all_parameters, [ $chunk, $merge_three_prime_ends_output_file ]; + } + + return @all_parameters; +} + +=method run_filter_three_prime_ends + + Usage : run_filter_three_prime_ends(); + Purpose : Run function for filter_three_prime_ends stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_filter_three_prime_ends { + my ( $self, $job ) = @_; + + my ( $chunk, $merge_three_prime_ends_output_file ) = @{ $job->parameters }; + + # Get regions + my $regions = LoadFile($merge_three_prime_ends_output_file); + + my %chunk_regions; + + # Filter 3' ends for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + my $seq_regions = filter_three_prime_ends( + { + analysis => $self->analysis, + seq_name => $seq->name, + regions => $regions->{ $seq->name }, + } + ); + %chunk_regions = + %{ $self->hash_merge->merge( \%chunk_regions, $seq_regions ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_regions ); + + return; +} + +=method all_parameters_for_choose_three_prime_end + + Usage : all_parameters_for_choose_three_prime_end(); + Purpose : Get all parameters for choose_three_prime_end stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_choose_three_prime_end { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + my $component = 0; + foreach my $chunk ( @{$chunks} ) { + $component++; + my $filter_three_prime_ends_output_file = + $self->get_and_check_output_file( 'filter_three_prime_ends', + $component ); + push @all_parameters, [ $chunk, $filter_three_prime_ends_output_file ]; + } + + return @all_parameters; +} + +=method run_choose_three_prime_end + + Usage : run_choose_three_prime_end(); + Purpose : Run function for choose_three_prime_end stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_choose_three_prime_end { + my ( $self, $job ) = @_; + + my ( $chunk, $filter_three_prime_ends_output_file ) = @{ $job->parameters }; + + # Get regions + my $regions = LoadFile($filter_three_prime_ends_output_file); + + my %chunk_regions; + + # Choose 3' ends for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + my $seq_regions = choose_three_prime_end( + { + seq_name => $seq->name, + regions => $regions->{ $seq->name }, + } + ); + %chunk_regions = + %{ $self->hash_merge->merge( \%chunk_regions, $seq_regions ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_regions ); + + return; +} + +=method all_parameters_for_count_reads + + Usage : all_parameters_for_count_reads(); + Purpose : Get all parameters for count_reads stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_count_reads { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + foreach my $bam_file ( $self->analysis->list_all_bam_files() ) { + my @tags = $self->analysis->list_all_tags_by_bam_file($bam_file); + my $component = 0; + foreach my $chunk ( @{$chunks} ) { + $component++; + my $choose_three_prime_end_output_file = + $self->get_and_check_output_file( 'choose_three_prime_end', + $component ); + push @all_parameters, + [ $chunk, $bam_file, $choose_three_prime_end_output_file, @tags ]; + } + } + + return @all_parameters; +} + +=method run_count_reads + + Usage : run_count_reads(); + Purpose : Run function for count_reads stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_count_reads { + my ( $self, $job ) = @_; + + my ( $chunk, $bam_file, $choose_three_prime_end_output_file, @tags ) = + @{ $job->parameters }; + + # Get regions + my $regions = LoadFile($choose_three_prime_end_output_file); + + my %chunk_regions; + + # Count reads for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + my $seq_regions = count_reads( + { + bam_file => $bam_file, + mismatch_threshold => $self->analysis->mismatch_threshold, + seq_name => $seq->name, + regions => $regions->{ $seq->name }, + tags => \@tags, + } + ); + %chunk_regions = + %{ $self->hash_merge->merge( \%chunk_regions, $seq_regions ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_regions ); + + return; +} + +=method all_parameters_for_merge_read_counts + + Usage : all_parameters_for_merge_read_counts(); + Purpose : Get all parameters for merge_read_counts stage + Returns : Array of arrayrefs + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_merge_read_counts { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + # Work out which count_reads stage files need to be merged + foreach my $merge_chunk ( @{$chunks} ) { + my %output_file_for; + my $component = 0; + foreach my $bam_file ( $self->analysis->list_all_bam_files() ) { + foreach my $run_chunk ( @{$chunks} ) { + $component++; + if ( refaddr($merge_chunk) == refaddr($run_chunk) ) { + my $output_file = + $self->get_and_check_output_file( 'count_reads', + $component ); + $output_file_for{$bam_file} = $output_file; + } + } + } + push @all_parameters, [ $merge_chunk, %output_file_for ]; + } + + return @all_parameters; +} + +=method run_merge_read_counts + + Usage : run_merge_read_counts(); + Purpose : Run function for merge_read_counts stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_merge_read_counts { + my ( $self, $job ) = @_; + + my ( $chunk, %output_file_for ) = @{ $job->parameters }; + + # Load all regions + my %hash_of_lists_of_regions; + foreach my $bam_file ( keys %output_file_for ) { + my $regions = LoadFile( $output_file_for{$bam_file} ); + $hash_of_lists_of_regions{$bam_file} = $regions; + } + + my %chunk_regions; + + # Merge read counts for each sequence of a chunk separately + foreach my $seq ( @{$chunk} ) { + + # Hash keyed by BAM file + my %regions = + map { $_ => $hash_of_lists_of_regions{$_}->{ $seq->name } } + keys %hash_of_lists_of_regions; + my $seq_regions = merge_read_counts( + { + seq_name => $seq->name, + regions => \%regions, + samples => $self->analysis->get_all_samples(), + } + ); + %chunk_regions = + %{ $self->hash_merge->merge( \%chunk_regions, $seq_regions ) }; + } + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, \%chunk_regions ); + + return; +} + +=method all_parameters_for_run_deseq + + Usage : all_parameters_for_run_deseq(); + Purpose : Get all parameters for run_deseq stage + Returns : Arrayref + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_run_deseq { + my ($self) = @_; + + my @all_parameters; + + my $chunks = $self->analysis->get_all_chunks(); + + my @merge_read_counts_output_files; + my $component = 0; + foreach my $chunk ( @{$chunks} ) { + $component++; + push @merge_read_counts_output_files, + $self->get_and_check_output_file( 'merge_read_counts', $component ); + } + push @all_parameters, \@merge_read_counts_output_files; + + return @all_parameters; +} + +=method run_run_deseq + + Usage : run_run_deseq(); + Purpose : Run function for run_deseq stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_run_deseq { + my ( $self, $job ) = @_; + + my (@merge_read_counts_output_files) = @{ $job->parameters }; + + # Join regions + my %regions; + foreach my $output_file (@merge_read_counts_output_files) { + %regions = + %{ $self->hash_merge->merge( \%regions, LoadFile($output_file) ) }; + } + + my $regions_ref = run_deseq( + { + dir => $job->base_filename, + regions => \%regions, + samples => $self->analysis->get_all_samples(), + r_binary => $self->analysis->r_binary, + deseq_script => $self->analysis->deseq_script, + } + ); + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, $regions_ref ); + + return; +} + +=method all_parameters_for_add_gene_annotation + + Usage : all_parameters_for_add_gene_annotation(); + Purpose : Get all parameters for add_gene_annotation stage + Returns : Arrayref + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_add_gene_annotation { + my ($self) = @_; + + my @all_parameters; + + my $run_deseq_output_file = + $self->get_and_check_output_file( 'run_deseq', 1 ); + + push @all_parameters, [$run_deseq_output_file]; + + return @all_parameters; +} + +=method run_add_gene_annotation + + Usage : run_add_gene_annotation(); + Purpose : Run function for add_gene_annotation stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_add_gene_annotation { + my ( $self, $job ) = @_; + + my ($run_deseq_output_file) = @{ $job->parameters }; + + # Get regions + my $regions = LoadFile($run_deseq_output_file); + + # Annotate 3' ends with genes + # Could split regions by chunk if slow + my $gene_finder = DETCT::GeneFinder->new( + { slice_adaptor => $self->analysis->slice_adaptor, } ); + my $annotated_regions_ref = $gene_finder->add_gene_annotation($regions); + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, $annotated_regions_ref ); + + return; +} + +=method all_parameters_for_dump_as_table + + Usage : all_parameters_for_dump_as_table(); + Purpose : Get all parameters for dump_as_table stage + Returns : Arrayref + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub all_parameters_for_dump_as_table { + my ($self) = @_; + + my @all_parameters; + + my $add_gene_annotation_output_file = + $self->get_and_check_output_file( 'add_gene_annotation', 1 ); + + push @all_parameters, [$add_gene_annotation_output_file]; + + return @all_parameters; +} + +=method run_dump_as_table + + Usage : run_dump_as_table(); + Purpose : Run function for dump_as_table stage + Returns : undef + Parameters : DETCT::Pipeline::Job + Throws : No exceptions + Comments : None + +=cut + +sub run_dump_as_table { + my ( $self, $job ) = @_; + + my ($add_gene_annotation_output_file) = @{ $job->parameters }; + + # Get regions + my $regions = LoadFile($add_gene_annotation_output_file); + + DETCT::Misc::Output::dump_as_table( + { + analysis => $self->analysis, + dir => $job->base_filename, + regions => $regions, + } + ); + + my $output_file = $job->base_filename . '.out'; + + DumpFile( $output_file, 1 ); + + return; +} + +1; diff --git a/lib/DETCT/Sample.pm b/lib/DETCT/Sample.pm new file mode 100644 index 0000000..042a235 --- /dev/null +++ b/lib/DETCT/Sample.pm @@ -0,0 +1,367 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Sample; +## use critic + +# ABSTRACT: Object representing a sample + +## Author : is1 +## Maintainer : is1 +## Created : 2012-09-19 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Readonly; +use Class::InsideOut qw( private register id ); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Attributes: +private name => my %name; # e.g. zmp_ph1_1m +private description => my %description; # e.g. ZMP phenotype 1.1 mutant +private condition => my %condition; # e.g. mutant +private group => my %group; # e.g. 1 +private tag => my %tag; # e.g. NNNNBGAGGC +private bam_file => my %bam_file; # e.g. 8295_6#1.bam + +# Constants +Readonly our $MAX_NAME_LENGTH => 128; +Readonly our $MAX_CONDITION_LENGTH => 128; +Readonly our $MAX_GROUP_LENGTH => 128; + +=method new + + Usage : my $sample = DETCT::Sample->new( { + name => 'zmp_ph1_1m', + condition => 'mutant', + group => '1', + tag => 'NNNNBGAGGC', + bam_file => '8295_6#1.bam', + } ); + Purpose : Constructor for sample objects + Returns : DETCT::Sample + Parameters : Hashref { + name => String, + description => String or undef, + condition => String, + group => String or undef, + tag => String, + bam_file => String, + } + Throws : No exceptions + Comments : None + +=cut + +sub new { + my ( $class, $arg_ref ) = @_; + my $self = register($class); + $self->set_name( $arg_ref->{name} ); + $self->set_description( $arg_ref->{description} ); + $self->set_condition( $arg_ref->{condition} ); + $self->set_group( $arg_ref->{group} ); + $self->set_tag( $arg_ref->{tag} ); + $self->set_bam_file( $arg_ref->{bam_file} ); + return $self; +} + +=method name + + Usage : my $name = $sample->name; + Purpose : Getter for name attribute + Returns : String (e.g. "zmp_ph1_1m") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub name { + my ($self) = @_; + return $name{ id $self}; +} + +=method set_name + + Usage : $sample->set_name('zmp_ph1_1m'); + Purpose : Setter for name attribute + Returns : undef + Parameters : String (the name) + Throws : No exceptions + Comments : None + +=cut + +sub set_name { + my ( $self, $arg ) = @_; + $name{ id $self} = _check_name($arg); + return; +} + +# Usage : $name = _check_name($name); +# Purpose : Check for valid name +# Returns : String (the valid name) +# Parameters : String (the name) +# Throws : If name is missing +# If name is invalid (i.e. not alphanumeric) +# If name is empty +# If name > $MAX_NAME_LENGTH characters +# Comments : None +sub _check_name { + my ($name) = @_; + + confess 'No name specified' if !defined $name; + confess 'Empty name specified' if !length $name; + confess 'Invalid name specified' if $name !~ m/\A [\w.-]+ \z/xms; + confess "Name ($name) longer than $MAX_NAME_LENGTH characters" + if length $name > $MAX_NAME_LENGTH; + + return $name; +} + +=method description + + Usage : my $description = $sample->description; + Purpose : Getter for description attribute + Returns : String (e.g. "ZMP phenotype 1.1 mutant") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub description { + my ($self) = @_; + return $description{ id $self}; +} + +=method set_description + + Usage : $sample->set_description('ZMP phenotype 1.1 mutant'); + Purpose : Setter for description attribute + Returns : undef + Parameters : String (the description) + Throws : No exceptions + Comments : None + +=cut + +sub set_description { + my ( $self, $arg ) = @_; + $description{ id $self} = $arg; + return; +} + +=method condition + + Usage : my $condition = $sample->condition; + Purpose : Getter for condition attribute + Returns : String (e.g. "mutant") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub condition { + my ($self) = @_; + return $condition{ id $self}; +} + +=method set_condition + + Usage : $sample->set_condition('mutant'); + Purpose : Setter for condition attribute + Returns : undef + Parameters : String (the condition) + Throws : No exceptions + Comments : None + +=cut + +sub set_condition { + my ( $self, $arg ) = @_; + $condition{ id $self} = _check_condition($arg); + return; +} + +# Usage : $condition = _check_condition($condition); +# Purpose : Check for valid condition +# Returns : String (the valid condition) +# Parameters : String (the condition) +# Throws : If condition is missing +# If condition is empty +# If condition > $MAX_GROUP_LENGTH characters +# Comments : None +sub _check_condition { + my ($condition) = @_; + + confess 'No condition specified' if !defined $condition; + confess 'Empty condition specified' if !length $condition; + confess + "Condition ($condition) longer than $MAX_CONDITION_LENGTH characters" + if length $condition > $MAX_CONDITION_LENGTH; + + return $condition; +} + +=method group + + Usage : my $group = $sample->group; + Purpose : Getter for group attribute + Returns : String (e.g. "1") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub group { + my ($self) = @_; + return $group{ id $self}; +} + +=method set_group + + Usage : $sample->set_group('1'); + Purpose : Setter for group attribute + Returns : undef + Parameters : String (the group) + Throws : No exceptions + Comments : None + +=cut + +sub set_group { + my ( $self, $arg ) = @_; + $group{ id $self} = _check_group($arg); + return; +} + +# Usage : $group = _check_group($group); +# Purpose : Check for valid group +# Returns : String (the valid group) +# Parameters : String (the group) +# Throws : If group is empty +# If group > $MAX_GROUP_LENGTH characters +# Comments : None +sub _check_group { + my ($group) = @_; + + confess 'Empty group specified' if defined $group && !length $group; + confess "Group ($group) longer than $MAX_GROUP_LENGTH characters" + if defined $group && length $group > $MAX_GROUP_LENGTH; + + return $group; +} + +=method tag + + Usage : my $tag = $sample->tag; + Purpose : Getter for tag attribute + Returns : String (e.g. "NNNNBGAGGC") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub tag { + my ($self) = @_; + return $tag{ id $self}; +} + +=method set_tag + + Usage : $sample->set_tag('NNNNBGAGGC'); + Purpose : Setter for tag attribute + Returns : undef + Parameters : String (the tag) + Throws : No exceptions + Comments : None + +=cut + +sub set_tag { + my ( $self, $arg ) = @_; + $tag{ id $self} = _check_tag($arg); + return; +} + +# Usage : $tag = _check_tag($tag); +# Purpose : Check for valid tag +# Returns : String (the valid tag) +# Parameters : String (the tag) +# Throws : If tag is missing or invalid +# Comments : None +sub _check_tag { + my ($tag) = @_; + return $tag + if defined $tag && $tag =~ m/\A [NRYKMSWBDHV]+ [AGCT]+ \z/xms; + confess 'No tag specified' if !defined $tag; + confess "Invalid tag ($tag) specified"; +} + +=method bam_file + + Usage : my $bam_file = $sample->bam_file; + Purpose : Getter for BAM file attribute + Returns : String (e.g. "8295_6#1.bam") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub bam_file { + my ($self) = @_; + return $bam_file{ id $self}; +} + +=method set_bam_file + + Usage : $sample->set_bam('8295_6#1.bam'); + Purpose : Setter for BAM file attribute + Returns : undef + Parameters : String (the BAM file) + Throws : No exceptions + Comments : None + +=cut + +sub set_bam_file { + my ( $self, $arg ) = @_; + $bam_file{ id $self} = check_bam_file($arg); + return; +} + +=method check_bam_file + + Usage : $bam_file = check_bam_file($bam_file); + Purpose : Check for valid BAM file + Returns : String (the valid BAM file) + Parameters : String (the BAM file) + Throws : If BAM file is missing or not readable + Comments : None + +=cut + +sub check_bam_file { + my ($bam_file) = @_; + return $bam_file if defined $bam_file && -r $bam_file; + confess 'No BAM file specified' if !defined $bam_file; + confess "BAM file ($bam_file) does not exist or cannot be read"; +} + +1; diff --git a/lib/DETCT/Sequence.pm b/lib/DETCT/Sequence.pm new file mode 100644 index 0000000..ff80064 --- /dev/null +++ b/lib/DETCT/Sequence.pm @@ -0,0 +1,161 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Sequence; +## use critic + +# ABSTRACT: Object representing a sequence (a component of a reference sequence) + +## Author : is1 +## Maintainer : is1 +## Created : 2012-09-21 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Readonly; +use Class::InsideOut qw( private register id ); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Attributes: +private name => my %name; # e.g. 1 +private bp => my %bp; # e.g. 60348388 + +# Constants +Readonly our $MAX_NAME_LENGTH => 128; + +=method new + + Usage : my $sequence = DETCT::Sequence->new( { + name => '1', + bp => 60_348_388, + } ); + Purpose : Constructor for sequence objects + Returns : DETCT::Sequence + Parameters : Hashref { + name => String, + bp => Int, + } + Throws : No exceptions + Comments : None + +=cut + +sub new { + my ( $class, $arg_ref ) = @_; + my $self = register($class); + $self->set_name( $arg_ref->{name} ); + $self->set_bp( $arg_ref->{bp} ); + return $self; +} + +=method name + + Usage : my $name = $sequence->name; + Purpose : Getter for name attribute + Returns : String (e.g. "1") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub name { + my ($self) = @_; + return $name{ id $self}; +} + +=method set_name + + Usage : $sequence->set_name('1'); + Purpose : Setter for name attribute + Returns : undef + Parameters : String (the name) + Throws : No exceptions + Comments : None + +=cut + +sub set_name { + my ( $self, $arg ) = @_; + $name{ id $self} = _check_name($arg); + return; +} + +# Usage : $name = _check_name($name); +# Purpose : Check for valid name +# Returns : String (the valid name) +# Parameters : String (the name) +# Throws : If name is missing +# If name is empty +# If name > $MAX_NAME_LENGTH characters +# Comments : None +sub _check_name { + my ($name) = @_; + + confess 'No name specified' if !defined $name; + confess 'Empty name specified' if !length $name; + confess "Name ($name) longer than $MAX_NAME_LENGTH characters" + if length $name > $MAX_NAME_LENGTH; + + return $name; +} + +=method bp + + Usage : my $bp = $sequence->bp; + Purpose : Getter for bp attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub bp { + my ($self) = @_; + return $bp{ id $self}; +} + +=method set_bp + + Usage : $sequence->set_bp(40352744); + Purpose : Setter for bp attribute + Returns : undef + Parameters : +ve Int (bp) + Throws : No exceptions + Comments : None + +=cut + +sub set_bp { + my ( $self, $arg ) = @_; + $bp{ id $self} = _check_bp($arg); + return; +} + +# Usage : $bp = _check_bp($bp); +# Purpose : Check for valid bp +# Returns : +ve Int (valid bp) +# Parameters : +ve Int (bp) +# Throws : If bp is missing or not a positive integer +# Comments : None +sub _check_bp { + my ($bp) = @_; + return $bp + if defined $bp && $bp =~ m/\A \d+ \z/xms; + confess 'No bp specified' if !defined $bp; + confess "Invalid bp ($bp) specified"; +} + +1; diff --git a/lib/DETCT/Transcript.pm b/lib/DETCT/Transcript.pm new file mode 100644 index 0000000..b7a406e --- /dev/null +++ b/lib/DETCT/Transcript.pm @@ -0,0 +1,516 @@ +## no critic (RequireUseStrict, RequireUseWarnings, RequireTidyCode) +package DETCT::Transcript; +## use critic + +# ABSTRACT: Object representing a transcript + +## Author : is1 +## Maintainer : is1 +## Created : 2013-01-28 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Readonly; +use Class::InsideOut qw( private register id ); + +=head1 SYNOPSIS + + # Brief code examples + +=cut + +# Attributes: +private stable_id => my %stable_id; # e.g. ENSDART00000133571 +private name => my %name; # e.g. cxc64-001 +private description => my %description; # e.g. CXC chemokine 64... +private biotype => my %biotype; # e.g. protein_coding +private seq_name => my %seq_name; # e.g. 5 +private start => my %start; # e.g. 40352744 +private end => my %end; # e.g. 40354399 +private strand => my %strand; # e.g. 1 +private gene => my %gene; # DETCT::Gene + +# Constants +Readonly our $MAX_NAME_LENGTH => 128; + +=method new + + Usage : my $transcript = DETCT::Transcript->new( { + stable_id => 'ENSDART00000133571', + biotype => 'protein_coding', + seq_name => '5', + start => 40352744, + end => 40354399, + strand => 1, + } ); + Purpose : Constructor for transcript objects + Returns : DETCT::Transcript + Parameters : Hashref { + stable_id => String, + name => String or undef, + description => String or undef, + biotype => String, + seq_name => String, + start => +ve Int, + end => +ve Int, + strand => Int (1 or -1), + gene => DETCT::Gene, + } + Throws : No exceptions + Comments : None + +=cut + +sub new { + my ( $class, $arg_ref ) = @_; + my $self = register($class); + $self->set_stable_id( $arg_ref->{stable_id} ); + $self->set_name( $arg_ref->{name} ); + $self->set_description( $arg_ref->{description} ); + $self->set_biotype( $arg_ref->{biotype} ); + $self->set_seq_name( $arg_ref->{seq_name} ); + $self->set_start( $arg_ref->{start} ); + $self->set_end( $arg_ref->{end} ); + $self->set_strand( $arg_ref->{strand} ); + $self->set_gene( $arg_ref->{gene} ); + return $self; +} + +=method stable_id + + Usage : my $stable_id = $transcript->stable_id; + Purpose : Getter for stable id attribute + Returns : String (e.g. "ENSDART00000133571") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub stable_id { + my ($self) = @_; + return $stable_id{ id $self}; +} + +=method set_stable_id + + Usage : $transcript->set_stable_id('ENSDART00000133571'); + Purpose : Setter for stable id attribute + Returns : undef + Parameters : String (the stable id) + Throws : No exceptions + Comments : None + +=cut + +sub set_stable_id { + my ( $self, $arg ) = @_; + $stable_id{ id $self} = check_stable_id($arg); + return; +} + +=method check_stable_id + + Usage : $stable_id = check_stable_id($stable_id); + Purpose : Check for valid stable id + Returns : String (the valid stable id) + Parameters : String (the stable id) + Throws : If stable id is missing or invalid + Comments : None + +=cut + +sub check_stable_id { + my ($stable_id) = @_; + return $stable_id + if defined $stable_id && $stable_id =~ m/\A [[:upper:]]+ \d{11} \z/xms; + confess 'No stable id specified' if !defined $stable_id; + confess "Invalid stable id ($stable_id) specified"; +} + +=method name + + Usage : my $name = $transcript->name; + Purpose : Getter for name attribute + Returns : String (e.g. "cxc64-001") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub name { + my ($self) = @_; + return $name{ id $self}; +} + +=method set_name + + Usage : $transcript->set_name('cxc64-001'); + Purpose : Setter for name attribute + Returns : undef + Parameters : String (the name) + Throws : No exceptions + Comments : None + +=cut + +sub set_name { + my ( $self, $arg ) = @_; + $name{ id $self} = _check_name($arg); + return; +} + +# Usage : $name = _check_name($name); +# Purpose : Check for valid name +# Returns : String (the valid name) +# Parameters : String (the name) +# Throws : If name > $MAX_NAME_LENGTH characters +# Comments : None +sub _check_name { + my ($name) = @_; + return $name + if !defined $name + || ( length $name > 0 && length $name <= $MAX_NAME_LENGTH ); + confess 'Name is empty' if !length $name; + confess "Name ($name) longer than $MAX_NAME_LENGTH characters"; +} + +=method description + + Usage : my $description = $transcript->description; + Purpose : Getter for description attribute + Returns : String (e.g. "CXC chemokine 64") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub description { + my ($self) = @_; + return $description{ id $self}; +} + +=method set_description + + Usage : $transcript->set_description('CXC chemokine 64'); + Purpose : Setter for description attribute + Returns : undef + Parameters : String (the description) + Throws : No exceptions + Comments : None + +=cut + +sub set_description { + my ( $self, $arg ) = @_; + $description{ id $self} = $arg; + return; +} + +=method biotype + + Usage : my $biotype = $transcript->biotype; + Purpose : Getter for biotype attribute + Returns : String (e.g. "protein_coding") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub biotype { + my ($self) = @_; + return $biotype{ id $self}; +} + +=method set_biotype + + Usage : $transcript->set_biotype('protein_coding'); + Purpose : Setter for biotype attribute + Returns : undef + Parameters : String (the biotype) + Throws : No exceptions + Comments : None + +=cut + +sub set_biotype { + my ( $self, $arg ) = @_; + $biotype{ id $self} = check_biotype($arg); + return; +} + +=method check_biotype + + Usage : $biotype = check_biotype($biotype); + Purpose : Check for valid biotype + Returns : String (the valid biotype) + Parameters : String (the biotype) + Throws : If biotype is missing or invalid (i.e. not alphanumeric) + Comments : None + +=cut + +sub check_biotype { + my ($biotype) = @_; + return $biotype if defined $biotype && $biotype =~ m/\A \w+ \z/xms; + confess 'No biotype specified' if !defined $biotype; + confess "Invalid biotype ($biotype) specified"; +} + +=method seq_name + + Usage : my $seq_name = $transcript->seq_name; + Purpose : Getter for sequence name attribute + Returns : String (e.g. "5") + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub seq_name { + my ($self) = @_; + return $seq_name{ id $self}; +} + +=method set_seq_name + + Usage : $transcript->set_seq_name('5'); + Purpose : Setter for sequence name attribute + Returns : undef + Parameters : String (the sequence name) + Throws : No exceptions + Comments : None + +=cut + +sub set_seq_name { + my ( $self, $arg ) = @_; + $seq_name{ id $self} = check_seq_name($arg); + return; +} + +=method check_seq_name + + Usage : $seq_name = check_seq_name($seq_name); + Purpose : Check for valid sequence name + Returns : String (the valid sequence name) + Parameters : String (the sequence name) + Throws : If sequence name is missing or invalid (i.e. not alphanumeric) + Comments : None + +=cut + +sub check_seq_name { + my ($seq_name) = @_; + return $seq_name if defined $seq_name && $seq_name =~ m/\A \w+ \z/xms; + confess 'No sequence name specified' if !defined $seq_name; + confess "Invalid sequence name ($seq_name) specified"; +} + +=method start + + Usage : my $start = $transcript->start; + Purpose : Getter for start attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub start { + my ($self) = @_; + return $start{ id $self}; +} + +=method set_start + + Usage : $transcript->set_start(40352744); + Purpose : Setter for start attribute + Returns : undef + Parameters : +ve Int (the start) + Throws : No exceptions + Comments : None + +=cut + +sub set_start { + my ( $self, $arg ) = @_; + $start{ id $self} = check_start($arg); + return; +} + +=method check_start + + Usage : $start = check_start($start); + Purpose : Check for valid start + Returns : +ve Int (the valid start) + Parameters : +ve Int (the start) + Throws : If start is missing or not a positive integer + Comments : None + +=cut + +sub check_start { + my ($start) = @_; + return $start if defined $start && $start =~ m/\A \d+ \z/xms; + confess 'No start specified' if !defined $start; + confess "Invalid start ($start) specified"; +} + +=method end + + Usage : my $end = $transcript->end; + Purpose : Getter for end attribute + Returns : +ve Int + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub end { + my ($self) = @_; + return $end{ id $self}; +} + +=method set_end + + Usage : $transcript->set_end(40352744); + Purpose : Setter for end attribute + Returns : undef + Parameters : +ve Int (the end) + Throws : No exceptions + Comments : None + +=cut + +sub set_end { + my ( $self, $arg ) = @_; + $end{ id $self} = check_end($arg); + return; +} + +=method check_end + + Usage : $end = check_end($end); + Purpose : Check for valid end + Returns : +ve Int (the valid end) + Parameters : +ve Int (the end) + Throws : If end is missing or not a positive integer + Comments : None + +=cut + +sub check_end { + my ($end) = @_; + return $end if defined $end && $end =~ m/\A \d+ \z/xms; + confess 'No end specified' if !defined $end; + confess "Invalid end ($end) specified"; +} + +=method strand + + Usage : my $strand = $transcript->strand; + Purpose : Getter for strand attribute + Returns : Int (1 or -1) + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub strand { + my ($self) = @_; + return $strand{ id $self}; +} + +=method set_strand + + Usage : $transcript->set_strand(1); + Purpose : Setter for strand attribute + Returns : undef + Parameters : Int (the strand) + Throws : No exceptions + Comments : None + +=cut + +sub set_strand { + my ( $self, $arg ) = @_; + $strand{ id $self} = _check_strand($arg); + return; +} + +# Usage : $strand = _check_strand($strand); +# Purpose : Check for valid strand +# Returns : Int (1 or -1) (the valid strand) +# Parameters : Int (1 or -1) (the strand) +# Throws : If strand is missing or not 1 or -1 +# Comments : None +sub _check_strand { + my ($strand) = @_; + return $strand if defined $strand && $strand =~ m/\A \-? 1 \z/xms; + confess 'No strand specified' if !defined $strand; + confess "Invalid strand ($strand) specified"; +} + +=method gene + + Usage : my $gene = $transcript->gene; + Purpose : Getter for gene attribute + Returns : DETCT::Gene + Parameters : None + Throws : No exceptions + Comments : None + +=cut + +sub gene { + my ($self) = @_; + return $gene{ id $self}; +} + +=method set_gene + + Usage : $transcript->set_gene($gene); + Purpose : Setter for gene attribute + Returns : undef + Parameters : DETCT::Gene + Throws : No exceptions + Comments : None + +=cut + +sub set_gene { + my ( $self, $arg ) = @_; + $gene{ id $self} = _check_gene($arg); + return; +} + +# Usage : $gene = _check_gene($gene); +# Purpose : Check for valid gene +# Returns : DETCT::Gene +# Parameters : DETCT::Gene +# Throws : If gene is invalid (i.e. not a DETCT::Gene object) +# Comments : None +sub _check_gene { + my ($gene) = @_; + confess 'Class of gene (', ref $gene, ') not DETCT::Gene' + if defined $gene && !$gene->isa('DETCT::Gene'); + return $gene; +} + +1; diff --git a/perlcritic.rc b/perlcritic.rc new file mode 100644 index 0000000..30963fa --- /dev/null +++ b/perlcritic.rc @@ -0,0 +1,12 @@ +severity = 1 +exclude = RequirePodSections RequireVersionVar + +[Documentation::PodSpelling] +stop_words_file = pod-stop-words.txt + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitStringyEval] +allow_includes = 1 + +[InputOutput::RequireCheckedSyscalls] +functions = :builtins +exclude_functions = print sleep diff --git a/pod-stop-words.txt b/pod-stop-words.txt new file mode 100644 index 0000000..e69de29 diff --git a/script/detag_fastq.pl b/script/detag_fastq.pl new file mode 100644 index 0000000..cd53089 --- /dev/null +++ b/script/detag_fastq.pl @@ -0,0 +1,163 @@ +#!/usr/bin/env perl + +# PODNAME: detag_fastq.pl +# ABSTRACT: Extract tags from transcript counting FASTQ files and process files + +## Author : is1 +## Maintainer : is1 +## Created : 2012-12-15 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Getopt::Long; +use Pod::Usage; +use DETCT::Misc::Tag; + +=head1 DESCRIPTION + + +=head1 EXAMPLES + + +=cut + +# Default options +## no critic (ProhibitMagicNumbers) +my $fastq_read1_input; +my $fastq_read2_input; +my $fastq_output_prefix; +my $pre_detag_trim_length = 54; +my $polyt_trim_length = 14; +my $polyt_min_length = 10; +my @read_tags; +my $no_pair_suffix = 0; +my ( $help, $man ); +## use critic + +# Get and check command line options +get_and_check_options(); + +DETCT::Misc::Tag::detag_trim_fastq( + { + fastq_read1_input => $fastq_read1_input, + fastq_read2_input => $fastq_read2_input, + fastq_output_prefix => $fastq_output_prefix, + pre_detag_trim_length => $pre_detag_trim_length, + polyt_trim_length => $polyt_trim_length, + polyt_min_length => $polyt_min_length, + read_tags => \@read_tags, + no_pair_suffix => $no_pair_suffix, + } +); + +# Get and check command line options +sub get_and_check_options { + + # Get options + GetOptions( + 'fastq_read1_input=s' => \$fastq_read1_input, + 'fastq_read2_input=s' => \$fastq_read2_input, + 'fastq_output_prefix=s' => \$fastq_output_prefix, + 'pre_detag_trim_length=i' => \$pre_detag_trim_length, + 'polyt_trim_length=i' => \$polyt_trim_length, + 'polyt_min_length=i' => \$polyt_min_length, + 'read_tags=s@{1,}' => \@read_tags, + 'no_pair_suffix' => \$no_pair_suffix, + 'help' => \$help, + 'man' => \$man, + ) or pod2usage(2); + + # Documentation + if ($help) { + pod2usage(1); + } + elsif ($man) { + pod2usage( -verbose => 2 ); + } + + # Check options + if ( !$fastq_read1_input ) { + pod2usage("--fastq_read1_input must be specified\n"); + } + if ( !$fastq_read2_input ) { + pod2usage("--fastq_read2_input must be specified\n"); + } + if ( !$fastq_output_prefix ) { + pod2usage("--fastq_output_prefix must be specified\n"); + } + if ( !@read_tags ) { + pod2usage("--read_tags must be specified\n"); + } + + return; +} + +=head1 USAGE + + detag_fastq.pl + [--fastq_read1_input file] + [--fastq_read2_input file] + [--fastq_output_prefix prefix] + [--pre_detag_trim_length int] + [--polyt_trim_length int] + [--polyt_min_length int] + [--read_tags tags...] + [--no_pair_suffix] + [--help] + [--man] + +=head1 OPTIONS + +=over 8 + +=item B<--fastq_read1_input FILE> + +Input FASTQ file for read 1. + +=item B<--fastq_read2_input FILE> + +Input FASTQ file for read 2. + +=item B<--fastq_output_prefix FILE> + +Prefix for output FASTQ files. + +=item B<--pre_detag_trim_length INT> + +Length to trim reads to before detagging. + +=item B<--polyt_trim_length INT> + +Length of (largely) polyT to be trimmed. + +=item B<--polyt_min_length INT> + +Minimum number of consecutive Ts in length of polyT. + +=item B<--read_tags TAGS> + +Read tags. + +=item B<--no_pair_suffix> + +Input FASTQ file don't have pair suffixes. + +=item B<--help> + +Print a brief help message and exit. + +=item B<--man> + +Print this script's manual page and exit. + +=back + +=cut diff --git a/script/make_test_fasta.pl b/script/make_test_fasta.pl new file mode 100644 index 0000000..51f589b --- /dev/null +++ b/script/make_test_fasta.pl @@ -0,0 +1,164 @@ +#!/usr/bin/env perl + +# PODNAME: make_test_fasta.pl +# ABSTRACT: Make transcript counting test file in FASTA format + +## Author : is1 +## Maintainer : is1 +## Created : 2012-11-12 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Getopt::Long; +use Pod::Usage; + +=head1 DESCRIPTION + +This script generates test transcript counting FASTA files. The number and +maximum length of chromosomes can be varied. + +=head1 EXAMPLES + + # Generate random FASTA file using default values + perl script/make_test_fasta.pl > test.fa + + # Generate FASTA file with reproducible chromosomes using default values + perl script/make_test_fasta.pl --seed 1 > test.fa + + # Generate FASTA file with 25 chromosomes (each up to 50 Mbp long) + perl script/make_test_fasta.pl \ + --seq_region_count 25 \ + --seq_region_max_length 50_000_000 \ + > test.fa + +=cut + +# Default options +## no critic (ProhibitMagicNumbers) +my $seed; +my $seq_region_count = 1; +my $seq_region_max_length = 1_000_000; +my ( $help, $man ); +## use critic + +# Get and check command line options +get_and_check_options(); + +# Ensure reproducible chromosome lengths if seed set +if ( defined $seed ) { + srand $seed; +} + +# Make each chromosome of random length +my %length_of; +foreach my $seq_region ( 1 .. $seq_region_count ) { + my $length = int rand( $seq_region_max_length + 1 ); + $length_of{$seq_region} = $length; +} + +# Ensure sequences are always random +srand; + +# Generate sequence for each chromosome one by one +foreach my $seq_region ( 1 .. $seq_region_count ) { + printf ">%s\n", $seq_region; + my $length_required = $length_of{$seq_region}; + my $length_printed = 0; + while ($length_required) { + ## no critic (ProhibitMagicNumbers) + print qw( A G C T a g c t ) [ int rand 8 ]; + ## use critic + $length_required--; + $length_printed++; + + # Wrap every 80 bases + ## no critic (ProhibitMagicNumbers) + if ( !( $length_printed % 80 ) ) { + ## use critic + print "\n"; + } + } + + # Final new line if haven't just printed one + ## no critic (ProhibitMagicNumbers) + if ( $length_printed % 80 ) { + ## use critic + print "\n"; + } +} + +# Get and check command line options +sub get_and_check_options { + + # Get options + GetOptions( + 'seed=i' => \$seed, + 'seq_region_count=i' => \$seq_region_count, + 'seq_region_max_length=i' => \$seq_region_max_length, + 'help' => \$help, + 'man' => \$man, + ) or pod2usage(2); + + # Documentation + if ($help) { + pod2usage(1); + } + elsif ($man) { + pod2usage( -verbose => 2 ); + } + + # Check options + if ( !$seq_region_count ) { + pod2usage("--seq_region_count must be a positive integer\n"); + } + if ( !$seq_region_max_length ) { + pod2usage("--seq_region_max_length must be a positive integer\n"); + } + + return; +} + +=head1 USAGE + + make_test_fasta.pl + [--seed seed] + [--seq_region_count int] + [--seq_region_max_length int] + [--help] + [--man] + +=head1 OPTIONS + +=over 8 + +=item B<--seed INT> + +Random seed (to get reproducible chromosome lengths). + +=item B<--seq_region_count INT> + +Number of seq regions (default to 1). + +=item B<--seq_region_max_length INT> + +Maximum length of each seq region (defaults to 1,000,000 bp). + +=item B<--help> + +Print a brief help message and exit. + +=item B<--man> + +Print this script's manual page and exit. + +=back + +=cut diff --git a/script/make_test_fastq.pl b/script/make_test_fastq.pl new file mode 100644 index 0000000..a4ce378 --- /dev/null +++ b/script/make_test_fastq.pl @@ -0,0 +1,276 @@ +#!/usr/bin/env perl + +# PODNAME: make_test_fastq.pl +# ABSTRACT: Make transcript counting test files in FASTQ format + +## Author : is1 +## Maintainer : is1 +## Created : 2013-01-08 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Getopt::Long; +use Pod::Usage; + +=head1 DESCRIPTION + +This script generates test transcript counting FASTQ files. + +=head1 EXAMPLES + + # Generate random FASTQ files using default values + perl script/make_test_fastq.pl --read_tags NNNNCTACCA + + # Generate FASTQ files with reproducible reads using default values + perl script/make_test_fastq.pl --seed 1 + + # Generate random FASTQ files with 1000 read pairs and 54 bp reads + perl script/make_test_fastq.pl \ + --read_tags NNNNCTACCA \ + --read_pair_count 1000 \ + --read_length 54 + +=cut + +# Default options +## no critic (ProhibitMagicNumbers) +my $seed; +my $output_prefix = 'test'; +my $read_pair_count = 100; +my @read_tags; +my $read_length = 75; +my $polyt_length = 14; +my ( $help, $man ); +## use critic + +# Get and check command line options +get_and_check_options(); + +# Assume all tags are same length +my $tag_length = length $read_tags[0]; + +# Add dummy tag for reads that don't match a real tag +push @read_tags, q{X} x $tag_length; + +# Ensure reproducible FASTQ files if seed set +if ( defined $seed ) { + srand $seed; +} + +# Generate start of each read name +## no critic (ProhibitMagicNumbers) +my $read_name_base = 'HS'; +$read_name_base .= ( int rand 50 ) + 1; # Instrument name +$read_name_base .= q{_}; +$read_name_base .= ( int rand 20_000 ) + 1; # Run +$read_name_base .= q{:}; +$read_name_base .= ( int rand 8 ) + 1; # Flowcell lane +$read_name_base .= q{:}; +## use critic + +my %tag_count; + +## no critic (RequireBriefOpen) +open my $fh1, '>', $output_prefix . '_1.fastq'; +open my $fh2, '>', $output_prefix . '_2.fastq'; +## use critic +foreach ( 1 .. $read_pair_count ) { + my $read_name = get_read_name($read_name_base); + + my $tag = @read_tags[ int rand scalar @read_tags ]; # Random tag + + # 20% of read 1s have no polyT + my $has_polyt = int rand 5 ? 1 : 0; ## no critic (ProhibitMagicNumbers) + + print {$fh1} q{@}, $read_name, '/1', "\n"; + print {$fh1} get_read1_seq( $read_length, $tag, $has_polyt ), "\n"; + print {$fh1} "+\n"; + print {$fh1} q{~} x $read_length, "\n"; + print {$fh2} q{@}, $read_name, '/2', "\n"; + print {$fh2} get_read2_seq($read_length), "\n"; + print {$fh2} "+\n"; + print {$fh2} q{~} x $read_length, "\n"; + + if ( !$has_polyt ) { + $tag = $read_tags[-1]; # Dummy tag + } + $tag_count{$tag}++; +} +close $fh1; +close $fh2; + +# Display tag counts +foreach my $read_tag (@read_tags) { + print $output_prefix, "\t", $read_tag, ":\t", + ( $tag_count{$read_tag} || 0 ), "\n"; +} + +# Construct read name +sub get_read_name { + my ( $read_name, ) = @_; + + ## no critic (ProhibitMagicNumbers) + $read_name .= ( int rand 3_000 ) + 1; # Tile number + $read_name .= q{:}; + $read_name .= ( int rand 20_000 ) + 1; # Cluster x coordinate + $read_name .= q{:}; + $read_name .= ( int rand 200_000 ) + 1; # Cluster y coordinate + ## use critic + + return $read_name; +} + +# Get read 1 sequence (just random but with tag) +sub get_read1_seq { + my ( $read_len, $tag, $has_polyt ) = @_; + + my $is_dummy_tag = $tag =~ m/X/xms ? 1 : 0; # If tag is X then no tag + + # Replace IUPAC codes in tag with random bases + $tag =~ s/ N / qw( A G C T )[ int rand 4 ] /xmsge; + $tag =~ s/ B / qw( G C T )[ int rand 3 ] /xmsge; + $tag =~ s/ D / qw( A G T )[ int rand 3 ] /xmsge; + $tag =~ s/ H / qw( A C T )[ int rand 3 ] /xmsge; + $tag =~ s/ V / qw( A G C )[ int rand 3 ] /xmsge; + $tag =~ s/ R / qw( A G )[ int rand 2 ] /xmsge; + $tag =~ s/ Y / qw( C T )[ int rand 2 ] /xmsge; + $tag =~ s/ K / qw( G T )[ int rand 2 ] /xmsge; + $tag =~ s/ M / qw( A C )[ int rand 2 ] /xmsge; + $tag =~ s/ S / qw( G C )[ int rand 2 ] /xmsge; + $tag =~ s/ W / qw( A T )[ int rand 2 ] /xmsge; + $tag =~ s/ X / qw( A G C T )[ int rand 4 ] /xmsge; # Not actually IUPAC + + # Make the last two bases be Ns so should never match a real tag + if ( $is_dummy_tag && $has_polyt ) { # No need if not polyT + substr $tag, -2, 2, 'NN'; ## no critic (ProhibitMagicNumbers) + } + + # 20% of reads have a single mismatch somewhere in the tag + if ( int rand 5 ) { ## no critic (ProhibitMagicNumbers) + my $mismatch_base = int rand length $tag; + my $base = substr $tag, $mismatch_base, 1; + $base =~ tr/AGCT/TCGA/; + substr $tag, $mismatch_base, 1, $base; + } + + # Read begins with tag then polyT (or add polyA if doesn't have polyT) + my $seq = $tag; + $seq .= $has_polyt ? q{T} x $polyt_length : q{A} x $polyt_length; + $read_len -= length $seq; + + # Rest of read is random + ## no critic (ProhibitMagicNumbers) + $seq .= join q{}, map { qw( A G C T ) [ int rand 4 ] } 1 .. $read_len; + ## use critic + + return $seq; +} + +# Get read 2 sequence (just random) +sub get_read2_seq { + my ($read_len) = @_; + + ## no critic (ProhibitMagicNumbers) + return join q{}, map { qw( A G C T ) [ int rand 4 ] } 1 .. $read_len; + ## use critic +} + +# Get and check command line options +sub get_and_check_options { + + # Get options + GetOptions( + 'seed=i' => \$seed, + 'output_prefix=s' => \$output_prefix, + 'read_pair_count=i' => \$read_pair_count, + 'read_tags=s@{1,}' => \@read_tags, + 'read_length=i' => \$read_length, + 'polyt_length=i' => \$polyt_length, + 'help' => \$help, + 'man' => \$man, + ) or pod2usage(2); + + # Documentation + if ($help) { + pod2usage(1); + } + elsif ($man) { + pod2usage( -verbose => 2 ); + } + + # Check options + if ( !$output_prefix ) { + pod2usage("--output_prefix must be specified\n"); + } + if ( !$read_pair_count ) { + pod2usage("--read_pair_count must be a positive integer\n"); + } + if ( !$read_length ) { + pod2usage("--read_length must be a positive integer\n"); + } + if ( !@read_tags ) { + pod2usage("--read_tags must be specified\n"); + } + + return; +} + +=head1 USAGE + + make_test_fastq.pl + [--seed seed] + [--output_prefix prefix] + [--read_pair_count int] + [--read_tags tags...] + [--read_length int] + [--polyt_length int] + [--help] + [--man] + +=head1 OPTIONS + +=over 8 + +=item B<--seed INT> + +Random seed (to get reproducible chromosome lengths). + +=item B<--output_prefix FILE> + +Prefix for output FASTQ files. + +=item B<--read_pair_count INT> + +Number of read pairs aligned to each seq region (defaults to 100). + +=item B<--read_tags TAGS> + +Read tags. + +=item B<--read_length INT> + +Length of reads (defaults to 75 bp). + +=item B<--polyt_length INT> + +Length of polyT in read 1. + +=item B<--help> + +Print a brief help message and exit. + +=item B<--man> + +Print this script's manual page and exit. + +=back + +=cut diff --git a/script/make_test_sam.pl b/script/make_test_sam.pl new file mode 100644 index 0000000..e88d2a1 --- /dev/null +++ b/script/make_test_sam.pl @@ -0,0 +1,630 @@ +#!/usr/bin/env perl + +# PODNAME: make_test_sam.pl +# ABSTRACT: Make transcript counting test file in SAM format + +## Author : is1 +## Maintainer : is1 +## Created : 2012-09-14 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Getopt::Long; +use Pod::Usage; +use Readonly; + +=head1 DESCRIPTION + +This script generates test transcript counting SAM files. The number and maximum +length of chromosomes can be varied along with the number and length of reads. +Read tags must be specified. + +=head1 EXAMPLES + + # Generate random BAM file using default values + perl script/make_test_sam.pl --read_tags NNNNCTACCA \ + | samtools view -bS - | samtools sort - test + + # Generate BAM file with reproducible chromosomes using default values + perl script/make_test_sam.pl --seed 1 --read_tags NNNNCTACCA \ + | samtools view -bS - | samtools sort - test + + # Generate BAM file with 25 chromosomes (each up to 50 Mbp long), 1000 + # alignments per chromosome and four 10mer tags + perl script/make_test_sam.pl \ + --seq_region_count 25 \ + --seq_region_max_length 50_000_000 \ + --read_pair_count 1000 \ + --read_tags NNNNCTACCA NNNNAAGTTA NNNNTTAATC NNNNTAGACA \ + | samtools view -bS - | samtools sort - test + +=cut + +# Constants from http://samtools.sourceforge.net/SAM1.pdf + +# Regexps for checking alignment line mandatory fields +Readonly our %ALIGNMENT_REGEXP_MANDATORY => ( + qname => qr/\A [!-?A-~]{1,255} \z/xms, + rname => qr/\A [*] | [!-()+-<>-~][!-~]* \z/xms, + cigar => qr/\A [*] | (\d+[MIDNSHPX=])+ \z/xms, + rnext => qr/\A [*] | = | [!-()+-<>-~][!-~]* \z/xms, + seq => qr/\A [*] | [[:alpha:]=.]+ \z/xms, + qual => qr/\A [!-~]+ \z/xms, +); + +# Ranges for checking alignment line mandatory fields +Readonly our %ALIGNMENT_RANGE_MANDATORY => ( + flag => [ 0, 2**16 - 1 ], + pos => [ 0, 2**29 - 1 ], + mapq => [ 0, 2**8 - 1 ], + pnext => [ 0, 2**29 - 1 ], + tlen => [ -2**29 + 1, 2**29 - 1 ], +); + +# Regexps for checking alignment line optional fields +Readonly our %ALIGNMENT_REGEXP_OPTIONAL => ( + A => qr/\A [!-~] \z/xms, + i => qr/\A [-+]?\d+ \z/xms, + f => qr/\A [-+]?\d*[.]?\d+([eE][-+]?\d+)? \z/xms, + Z => qr/\A [ !-~]+ \z/xms, + H => qr/\A [\dA-F]+ \z/xms, + B => qr/\A [cCsSiIf](,[-+]?\d*[.]?\d+([eE][-+]?\d+)?)+ \z/xms, +); + +# Bits of flag field +Readonly our $FLAG_READ_PAIRED => 1; +Readonly our $FLAG_PROPER_PAIR => 2; +Readonly our $FLAG_READ_UNMAPPED => 4; +Readonly our $FLAG_MATE_UNMAPPED => 8; +Readonly our $FLAG_READ_REVERSE_STRAND => 16; +Readonly our $FLAG_MATE_REVERSE_STRAND => 32; +Readonly our $FLAG_FIRST_IN_PAIR => 64; +Readonly our $FLAG_SECOND_IN_PAIR => 128; +Readonly our $FLAG_DUPLICATE => 1024; + +# Chance one read of a pair is unmapped +Readonly our $CHANCE_UNMAPPED => 0.1; + +# Default options +## no critic (ProhibitMagicNumbers) +my $seed; +my $seq_region_count = 1; +my $seq_region_max_length = 1_000_000; +my $read_pair_count = 100; +my @read_tags; +my $read1_length = 30; +my $read2_length = 54; +my ( $help, $man ); +## use critic + +# Get and check command line options +get_and_check_options(); + +# Ensure reproducible chromosome lengths if seed set +if ( defined $seed ) { + srand $seed; +} + +# Construct command line +my @cl = ('make_test_sam.pl'); +if ($seed) { + push @cl, '--seed', $seed; +} +push @cl, '--seq_region_count', $seq_region_count; +push @cl, '--seq_region_max_length', $seq_region_max_length; +push @cl, '--read_pair_count', $read_pair_count; +push @cl, '--read_tags', @read_tags; +push @cl, '--read1_length', $read1_length; +push @cl, '--read2_length', $read2_length; +my $cl = join q{ }, @cl; + +# Print HD and RG SAM header +print header_line( 'HD', [ 'VN', '1.4' ], [ 'SO', 'unsorted' ] ); +print header_line( 'RG', [ 'ID', q{1} ], [ 'SM', 'TC' ] ); +print header_line( + 'PG', + [ 'ID', q{1} ], + [ 'PN', 'make_test_sam.pl' ], + [ 'CL', $cl ] +); + +# Make each chromosome of random length and print SQ SAM headers +my %length_of; +foreach my $seq_region ( 1 .. $seq_region_count ) { + my $length = int rand( $seq_region_max_length + 1 ); + $length_of{$seq_region} = $length; + print header_line( 'SQ', [ 'SN', $seq_region ], [ 'LN', $length ] ); +} + +# Ensure alignments are always random +srand; + +# Generate start of each read name +## no critic (ProhibitMagicNumbers) +my $qname_base = 'HS'; +$qname_base .= ( int rand 50 ) + 1; # Instrument name +$qname_base .= q{_}; +$qname_base .= ( int rand 20_000 ) + 1; # Run +$qname_base .= q{:}; +$qname_base .= ( int rand 8 ) + 1; # Flowcell lane +$qname_base .= q{:}; +## use critic + +# Generate alignments for each chromosome one by one +foreach my $seq_region ( 1 .. $seq_region_count ) { + foreach ( 1 .. $read_pair_count ) { + my $read1_qname = get_qname( $qname_base, get_read_tag() ); + my $read2_qname = $read1_qname; # Always the same + my ( $read1_pos, $read2_pos ) = + get_pos( $length_of{$seq_region}, $read1_length, $read2_length ); + my ( $read1_flag, $read2_flag ) = get_flag( $read1_pos, $read2_pos ); + my ( $read1_tlen, $read2_tlen ) = + get_tlen( $read1_pos, $read2_pos, $read1_length, $read2_length ); + ( $read1_flag, $read2_flag, $read1_pos, $read2_pos ) = + get_unmapped( $read1_flag, $read2_flag, $read1_pos, $read2_pos ); + my ($read1_cigar) = get_cigar($read1_length); + my ($read2_cigar) = get_cigar($read2_length); + my ($read1_nm) = get_nm(); + my ($read2_nm) = get_nm(); + + # Rarely generate 50 to 99 real duplicates to simulate peaks + ## no critic (ProhibitMagicNumbers) + my $num_real_duplicates = 0; + if ( rand $read_pair_count < 2 ) { + $num_real_duplicates = int( rand 50 ) + 50; + } + ## use critic + + # Generate PCR duplicates (i.e. marked as duplicates) + ## no critic (ProhibitMagicNumbers) + my $num_pcr_duplicates = poisson_number(0.6); + ## use critic + + my $num_duplicates = $num_real_duplicates + $num_pcr_duplicates; + foreach my $read_pair_count ( 1 .. $num_duplicates + 1 ) { + + # First read + print alignment_line( + qname => $read1_qname, + flag => $read1_flag, + rname => $seq_region, + pos => $read1_pos, + mapq => 255, + cigar => $read1_cigar, + rnext => q{=}, + pnext => $read2_pos, + tlen => $read1_tlen, + seq => get_seq($read1_length), + qual => get_qual($read1_length), + opt => { + 'NM:i' => $read1_nm, + 'RG:Z' => q{1}, + }, + ); + + # Second read + print alignment_line( + qname => $read2_qname, + flag => $read2_flag, + rname => $seq_region, + pos => $read2_pos, + mapq => 255, + cigar => $read2_cigar, + rnext => q{=}, + pnext => $read1_pos, + tlen => $read2_tlen, + seq => get_seq($read2_length), + qual => get_qual($read2_length), + opt => { + 'NM:i' => $read2_nm, + 'RG:Z' => q{1}, + }, + ); + + if ( $read_pair_count == $num_real_duplicates + 1 ) { + + # Mark rest of reads as duplicates + $read1_flag = $read1_flag | $FLAG_DUPLICATE; + $read2_flag = $read2_flag | $FLAG_DUPLICATE; + } + } + } +} + +# Generate SAM header line +sub header_line { + my ( $record_type, @data ) = @_; + + my $header_line = q{}; + + if ( $record_type !~ m/\A [[:alpha:]][[:alpha:]] \z/xms ) { + confess 'Invalid record type (', $record_type, q{)}; + } + + $header_line .= q{@} . $record_type; + + foreach my $datum (@data) { + if ( ref $datum ne 'ARRAY' ) { + confess 'Arrayref of tag / value pairs is required (not ', + ref $datum, q{)}; + } + my ( $tag, $value ) = @{$datum}; + if ( $tag !~ m/\A [[:alpha:]][[:alpha:]\d] \z/xms ) { + confess 'Invalid tag (', $tag, q{)}; + } + if ( $value !~ m/\A [ -~]+ \z/xms ) { + confess 'Invalid value (', $value, q{)}; + } + + $header_line .= "\t" . $tag . q{:} . $value; + } + + $header_line .= "\n"; + + return $header_line; +} + +# Generate SAM alignment line +sub alignment_line { + my (%data) = @_; + + # Check string fields + foreach my $field ( sort keys %ALIGNMENT_REGEXP_MANDATORY ) { + if ( $data{$field} !~ $ALIGNMENT_REGEXP_MANDATORY{$field} ) { + confess 'Invalid ', uc $field, ' (', $data{$field}, q{)}; + } + } + + # Check int fields + foreach my $field ( sort keys %ALIGNMENT_RANGE_MANDATORY ) { + if ( $data{$field} < $ALIGNMENT_RANGE_MANDATORY{$field}->[0] + || $data{$field} > $ALIGNMENT_RANGE_MANDATORY{$field}->[1] ) + { + confess 'Invalid ', uc $field, ' (', $data{$field}, q{)}; + } + } + + # Mandatory fields + my $alignment_line = join "\t", $data{qname}, $data{flag}, $data{rname}, + $data{pos}, $data{mapq}, $data{cigar}, $data{rnext}, $data{pnext}, + $data{tlen}, $data{seq}, $data{qual}; + + # Optional fields + if ( exists $data{opt} ) { + foreach my $tag_type ( keys %{ $data{opt} } ) { + my $value = $data{opt}->{$tag_type}; + my ( $tag, $type ) = split /:/xms, $tag_type; + + # Validate tag + if ( $tag !~ /\A [[:alpha:]][[:alpha:]\d] \z/xms ) { + confess 'Invalid tag (', $tag, q{)}; + } + + # Validate type + if ( !exists $ALIGNMENT_REGEXP_OPTIONAL{$type} ) { + confess 'Invalid type (', $type, q{)}; + } + + # Validate value + if ( $value !~ $ALIGNMENT_REGEXP_OPTIONAL{$type} ) { + confess 'Invalid ', $tag, ' (', $value, q{)}; + } + + $alignment_line .= "\t"; + $alignment_line .= join q{:}, $tag, $type, $value; + } + } + + $alignment_line .= "\n"; + + return $alignment_line; +} + +# Get a random read tag and substitute random bases +sub get_read_tag { + my $tag = $read_tags[ int rand $#read_tags + 1 ]; + + # Replace IUPAC code with random bases + $tag =~ s/ N / qw( A G C T )[ int rand 4 ] /xmsge; + $tag =~ s/ B / qw( G C T )[ int rand 3 ] /xmsge; + $tag =~ s/ D / qw( A G T )[ int rand 3 ] /xmsge; + $tag =~ s/ H / qw( A C T )[ int rand 3 ] /xmsge; + $tag =~ s/ V / qw( A G C )[ int rand 3 ] /xmsge; + $tag =~ s/ R / qw( A G )[ int rand 2 ] /xmsge; + $tag =~ s/ Y / qw( C T )[ int rand 2 ] /xmsge; + $tag =~ s/ K / qw( G T )[ int rand 2 ] /xmsge; + $tag =~ s/ M / qw( A C )[ int rand 2 ] /xmsge; + $tag =~ s/ S / qw( G C )[ int rand 2 ] /xmsge; + $tag =~ s/ W / qw( A T )[ int rand 2 ] /xmsge; + + return $tag; +} + +# Construct read name +sub get_qname { + my ( $qname, $read_tag ) = @_; + + ## no critic (ProhibitMagicNumbers) + $qname .= ( int rand 3_000 ) + 1; # Tile number + $qname .= q{:}; + $qname .= ( int rand 20_000 ) + 1; # Cluster x coordinate + $qname .= q{:}; + $qname .= ( int rand 200_000 ) + 1; # Cluster y coordinate + $qname .= q{#}; + $qname .= $read_tag; + ## use critic + + return $qname; +} + +# Get position for both reads +sub get_pos { + my ( $seq_region_len, $read1_len, $read2_len ) = @_; + + my ( $read1_pos, $read2_pos ); + + my $pair_ok = 0; + + while ( !$pair_ok ) { + $read1_pos = ( int rand $seq_region_len ) + 1; + $read2_pos = ( int rand $seq_region_len ) + 1; + $pair_ok = 1; + + my $read1_end = $read1_pos + $read1_len - 1; + my $read2_end = $read2_pos + $read2_len - 1; + + # Check reads are within seq region + if ( $read1_end > $seq_region_len ) { + $pair_ok = 0; + } + if ( $read2_end > $seq_region_len ) { + $pair_ok = 0; + } + + # Check reads don't overlap + if ( $read1_pos <= $read2_end && $read1_end >= $read2_pos ) { + $pair_ok = 0; + } + } + + return $read1_pos, $read2_pos; +} + +# Get flags for both reads (http://picard.sourceforge.net/explain-flags.html) +sub get_flag { + my ( $read1_pos, $read2_pos ) = @_; + + my $read1_flag = $FLAG_READ_PAIRED | $FLAG_PROPER_PAIR; + my $read2_flag = $FLAG_READ_PAIRED | $FLAG_PROPER_PAIR; + + if ( $read1_pos < $read2_pos ) { + $read1_flag = $read1_flag | $FLAG_MATE_REVERSE_STRAND; + $read2_flag = $read2_flag | $FLAG_READ_REVERSE_STRAND; + } + else { + $read1_flag = $read1_flag | $FLAG_READ_REVERSE_STRAND; + $read2_flag = $read2_flag | $FLAG_MATE_REVERSE_STRAND; + } + + $read1_flag = $read1_flag | $FLAG_FIRST_IN_PAIR; + $read2_flag = $read2_flag | $FLAG_SECOND_IN_PAIR; + + return $read1_flag, $read2_flag; +} + +# Get template length for both reads +sub get_tlen { + my ( $read1_pos, $read2_pos, $read1_len, $read2_len ) = @_; + + my ( $read1_tlen, $read2_tlen ); + + if ( $read1_pos < $read2_pos ) { + $read1_tlen = $read2_pos - $read1_pos + $read2_len; + $read2_tlen = -$read1_tlen; + } + else { + $read2_tlen = $read1_pos - $read2_pos + $read1_len; + $read1_tlen = -$read2_tlen; + } + + return $read1_tlen, $read2_tlen; +} + +# Adjust flags and positions if a read is unmapped +sub get_unmapped { + my ( $read1_flag, $read2_flag, $read1_pos, $read2_pos ) = @_; + + if ( rand() < $CHANCE_UNMAPPED ) { + if ( rand() < 0.5 ) { ## no critic (ProhibitMagicNumbers) + # Read 1 unmapped + $read1_flag = $read1_flag | $FLAG_READ_UNMAPPED; + $read2_flag = $read2_flag | $FLAG_MATE_UNMAPPED; + $read1_pos = $read2_pos; + } + else { + # Read 2 unmapped + $read2_flag = $read2_flag | $FLAG_READ_UNMAPPED; + $read1_flag = $read1_flag | $FLAG_MATE_UNMAPPED; + $read2_pos = $read1_pos; + } + } + + return $read1_flag, $read2_flag, $read1_pos, $read2_pos; +} + +# Get sequence (just random) +sub get_seq { + my ($read_len) = @_; + + ## no critic (ProhibitMagicNumbers) + return join q{}, map { qw( A G C T ) [ int rand 4 ] } 1 .. $read_len; + ## use critic +} + +# Get CIGAR string containing random soft clipping +sub get_cigar { + my ($read_len) = @_; + + my $m = $read_len; # Length of alignment match + + ## no critic (ProhibitMagicNumbers) + my $s1 = poisson_number(0.7); # Soft clipping at start of alignment + my $s2 = poisson_number(0.7); # Soft clipping at end of alignment + ## use critic + + $m = $m - $s1 - $s2; + + # Construct CIGAR + + my $cigar = $m . q{M}; + + if ($s1) { + $cigar = $s1 . q{S} . $cigar; + } + if ($s2) { + $cigar = $cigar . $s2 . q{S}; + } + + return $cigar; +} + +# Get quality +sub get_qual { + my ($read_len) = @_; + + return q{~} x $read_len; +} + +# Get random number of mismatches for a read +sub get_nm { + ## no critic (ProhibitMagicNumbers) + return poisson_number(0.6); # ~ e^-0.5, so skewed towards 0 and 1 + ## use critic +} + +# Generate random Poisson-distributed number using Knuth's algorithm +sub poisson_number { + my ($l) = @_; # e^-lambda + + my $k = 0; + my $p = 1; + + while ( $p > $l ) { + $k++; + $p = $p * rand; + } + + return $k - 1; +} + +# Get and check command line options +sub get_and_check_options { + + # Get options + GetOptions( + 'seed=i' => \$seed, + 'seq_region_count=i' => \$seq_region_count, + 'seq_region_max_length=i' => \$seq_region_max_length, + 'read_pair_count=i' => \$read_pair_count, + 'read_tags=s@{1,}' => \@read_tags, + 'read1_length=i' => \$read1_length, + 'read2_length=i' => \$read2_length, + 'help' => \$help, + 'man' => \$man, + ) or pod2usage(2); + + # Documentation + if ($help) { + pod2usage(1); + } + elsif ($man) { + pod2usage( -verbose => 2 ); + } + + # Check options + if ( !$seq_region_count ) { + pod2usage("--seq_region_count must be a positive integer\n"); + } + if ( !$seq_region_max_length ) { + pod2usage("--seq_region_max_length must be a positive integer\n"); + } + if ( !$read_pair_count ) { + pod2usage("--read_pair_count must be a positive integer\n"); + } + if ( !$read1_length ) { + pod2usage("--read1_length must be a positive integer\n"); + } + if ( !$read2_length ) { + pod2usage("--read2_length must be a positive integer\n"); + } + if ( !@read_tags ) { + pod2usage("--read_tags must be specified\n"); + } + + return; +} + +=head1 USAGE + + make_test_sam.pl + [--seed seed] + [--seq_region_count int] + [--seq_region_max_length int] + [--read_pair_count int] + [--read_tags tags...] + [--read1_length int] + [--read2_length int] + [--help] + [--man] + +=head1 OPTIONS + +=over 8 + +=item B<--seed INT> + +Random seed (to get reproducible chromosome lengths). + +=item B<--seq_region_count INT> + +Number of seq regions (default to 1). + +=item B<--seq_region_max_length INT> + +Maximum length of each seq region (defaults to 1,000,000 bp). + +=item B<--read_pair_count INT> + +Number of read pairs aligned to each seq region (defaults to 100). + +=item B<--read_tags TAGS> + +Read tags. + +=item B<--read1_length INT> + +Length of read 1 after trimming (defaults to 30 bp). + +=item B<--read2_length INT> + +Length of read 2 (defaults to 54 bp). + +=item B<--help> + +Print a brief help message and exit. + +=item B<--man> + +Print this script's manual page and exit. + +=back + +=cut diff --git a/script/run_de_pipeline.pl b/script/run_de_pipeline.pl new file mode 100644 index 0000000..2f61ed6 --- /dev/null +++ b/script/run_de_pipeline.pl @@ -0,0 +1,236 @@ +#!/usr/bin/env perl + +# PODNAME: run_de_pipeline.pl +# ABSTRACT: Run DETCT differential expression pipeline + +## Author : is1 +## Maintainer : is1 +## Created : 2012-09-26 +## Last commit by : $Author$ +## Last modified : $Date$ +## Revision : $Revision$ +## Repository URL : $HeadURL$ + +use warnings; +use strict; +use autodie; +use Carp; +use Try::Tiny; + +use Probe::Perl; +use Getopt::Long; +use Pod::Usage; +use English qw( -no_match_vars ); +use File::Spec; +use File::Slurp; +use DETCT::Pipeline::WithDiffExprStages; +use DETCT::Analysis; + +=head1 DESCRIPTION + + + +=head1 EXAMPLES + + + +=cut + +# Default options +my $scheduler = 'lsf'; +my $analysis_dir = q{.}; +my $analysis_yaml = File::Spec->catfile( $analysis_dir, 'analysis.yaml' ); +my $stages_yaml = File::Spec->catfile( $analysis_dir, 'stages.yaml' ); +## no critic (ProhibitMagicNumbers) +my $max_retries = 10; +my $sleep_time = 600; # 10 minutes +## use critic +my $stage_to_run; +my $component_to_run; +my $verbose; +my ( $help, $man ); + +# Get command line (including interpreter and options) +my $cmd_line = get_cmd_line(); + +# Get and check command line options +get_and_check_options(); + +# Create analysis +my $analysis = DETCT::Analysis->new_from_yaml($analysis_yaml); + +# Create pipeline +my $pipeline = DETCT::Pipeline::WithDiffExprStages->new( + { + scheduler => $scheduler, + analysis_dir => $analysis_dir, + analysis => $analysis, + cmd_line => $cmd_line, + max_retries => $max_retries, + sleep_time => $sleep_time, + verbose => $verbose, + } +); + +# Add stages to pipeline +$pipeline->add_stages_from_yaml($stages_yaml); + +# Are we running the main pipeline or running a specific component of a specific +# stage (i.e. a job to be run under LSF or locally)? +if ($stage_to_run) { + $pipeline->set_stage_to_run( $pipeline->get_stage_by_name($stage_to_run) ); +} +if ($component_to_run) { + $pipeline->set_component_to_run($component_to_run); +} + +# Turn off verbose output when running specific components +if ( $pipeline->stage_to_run && $pipeline->component_to_run ) { + $pipeline->set_verbose(0); +} + +# Write overview of pipeline input and config files to log file +if ( !$pipeline->stage_to_run && !$pipeline->component_to_run ) { + my @log = map { "$_\n" } $pipeline->input_overview; + push @log, "\nYAML analysis config file:\n\n", read_file($analysis_yaml); + push @log, "\nYAML stages config file:\n\n", read_file($stages_yaml); + $pipeline->write_log_file( 'de.log', @log ); +} + +# Print overview of pipeline input +$pipeline->say_if_verbose( $pipeline->input_overview ); + +# Run pipeline +$pipeline->run(); + +# Get entire command line +sub get_cmd_line { + + # Get all lib directories + my %lib = map { $_ => 1 } @INC; + + # Remove default lib directories + foreach my $lib ( Probe::Perl->perl_inc() ) { + delete $lib{$lib}; + } + + # Remove PERL5LIB lib directories + foreach my $lib ( split /:/xms, $ENV{PERL5LIB} ) { + delete $lib{$lib}; + } + + # Reconstruct -I lib directories + my @libs; + foreach my $lib ( keys %lib ) { + push @libs, '-I' . $lib; + } + + return join q{ }, Probe::Perl->find_perl_interpreter(), @libs, + $PROGRAM_NAME, @ARGV; +} + +# Get and check command line options +sub get_and_check_options { + + # Get options + GetOptions( + 'scheduler=s' => \$scheduler, + 'dir=s' => \$analysis_dir, + 'analysis_yaml=s' => \$analysis_yaml, + 'stages_yaml=s' => \$stages_yaml, + 'max_retries=i' => \$max_retries, + 'sleep_time=i' => \$sleep_time, + 'stage=s' => \$stage_to_run, + 'component=i' => \$component_to_run, + 'verbose' => \$verbose, + 'help' => \$help, + 'man' => \$man, + ) or pod2usage(2); + + # Documentation + if ($help) { + pod2usage(1); + } + elsif ($man) { + pod2usage( -verbose => 2 ); + } + + # Check options + if ( $scheduler ne 'lsf' && $scheduler ne 'local' ) { + pod2usage("--scheduler must be 'lsf' or 'local'\n"); + } + if ( $stage_to_run && !$component_to_run + || !$stage_to_run && $component_to_run ) + { + pod2usage("--stage and --component must be specified together\n"); + } + + return; +} + +=head1 USAGE + + run_de_pipeline.pl + [--scheduler lsf|local] + [--dir directory] + [--analysis_yaml file] + [--stages_yaml file] + [--max_retries int] + [--sleep_time int] + [--stage stage] + [--component int] + [--verbose] + [--help] + [--man] + +=head1 OPTIONS + +=over 8 + +=item B<--scheduler lsf|local> + +Job scheduler - lsf (default) or local (for testing). + +=item B<--dir DIRECTORY> + +Working directory for analysis. + +=item B<--analysis_yaml FILE> + +YAML analysis configuration file. + +=item B<--stages_yaml FILE> + +YAML stages configuration file. + +=item B<--max_retries INT> + +Maximum number of times to retry a failing job. + +=item B<--sleep_time INT> + +Time to sleep, in seconds, between each iteration of the pipeline. + +=item B<--stage STAGE> + +The specific stage of the pipeline to be run. + +=item B<--component INT> + +The index of the component of the specified stage of the pipeline to be run. + +=item B<--verbose> + +Print information about the pipeline as it runs. + +=item B<--help> + +Print a brief help message and exit. + +=item B<--man> + +Print this script's manual page and exit. + +=back + +=cut diff --git a/script/run_deseq.R b/script/run_deseq.R new file mode 100644 index 0000000..920f79b --- /dev/null +++ b/script/run_deseq.R @@ -0,0 +1,124 @@ +library(DESeq) +library(RColorBrewer) +library(gplots) + +Args <- commandArgs(); +countFile <- Args[4] +designFile <- Args[5] +outputFile <- Args[6] +sizeFactorsFile <- Args[7] +qcPdfFile <- Args[8] + +# Get data and design +countTable <- read.table( countFile, header=TRUE, row.names=1 ) +design <- read.table( designFile, header=TRUE, row.names=1 ) +numFactors <- ncol(design) +numConditions <- nlevels(design$condition) + +# Check design +if (numFactors > 2) { + stop("Too many factors") +} +if (numConditions != 2) { + stop("Must be two conditions") +} + +# Write QC graphs to PDF +pdf(qcPdfFile) + +# Create CountDataSets +cdsOneFactFull <- newCountDataSet( countTable, design$condition ) +if (numFactors == 2) { + cdsTwoFactFull <- newCountDataSet( countTable, design ) +} + +# Remove regions with sum of counts below the 40th quantile +# See "5 Independent filtering and multiple testing" of +# http://bioconductor.org/packages/devel/bioc/vignettes/DESeq/inst/doc/DESeq.pdf +rs <- rowSums ( counts ( cdsOneFactFull )) +use <- (rs > quantile(rs, probs=0.4)) +cdsOneFactFilt <- cdsOneFactFull[ use, ] +if (numFactors == 2) { + cdsTwoFactFilt <- cdsTwoFactFull[ use, ] +} + +# Normalise +cdsOneFactFull <- estimateSizeFactors( cdsOneFactFull ) +cdsOneFactFilt <- estimateSizeFactors( cdsOneFactFilt ) +if (numFactors == 2) { + cdsTwoFactFull <- estimateSizeFactors( cdsTwoFactFull ) + cdsTwoFactFilt <- estimateSizeFactors( cdsTwoFactFilt ) +} +write.table( sizeFactors( cdsOneFactFull ), file=sizeFactorsFile, + col.names=FALSE, row.names=FALSE, quote=FALSE, sep="\t" ) + +# Estimate variance +cdsOneFactFiltPooled <- tryCatch({ + estimateDispersions( cdsOneFactFilt ) +}, error = function(e) { + estimateDispersions( cdsOneFactFilt, fitType="local" ) +}) +cdsOneFactFullBlind <- tryCatch({ + estimateDispersions( cdsOneFactFull, method="blind" ) +}, error = function(e) { + estimateDispersions( cdsOneFactFull, method="blind", fitType="local" ) +}) +if (numFactors == 1) { + plotDispEsts( cdsOneFactFiltPooled ) +} else if (numFactors == 2) { + cdsTwoFactFiltPooledCR <- tryCatch({ + estimateDispersions( cdsTwoFactFilt, method="pooled-CR" ) + }, error = function(e) { + estimateDispersions( cdsTwoFactFilt, method="pooled-CR", fitType="local" ) + }) + cdsTwoFactFullBlind <- tryCatch({ + estimateDispersions( cdsTwoFactFull, method="blind" ) + }, error = function(e) { + estimateDispersions( cdsTwoFactFull, method="blind", fitType="local" ) + }) + plotDispEsts( cdsTwoFactFiltPooledCR ) +} + +# Compare conditions +conditions <- levels(design$condition) +res <- nbinomTest( cdsOneFactFiltPooled, conditions[1], conditions[2] ) +if (numFactors == 2) { + fit1 <- fitNbinomGLMs( cdsTwoFactFiltPooledCR, count ~ group + condition ) + fit0 <- fitNbinomGLMs( cdsTwoFactFiltPooledCR, count ~ group ) + res$pval <- nbinomGLMTest( fit1, fit0 ) + res$padj <- p.adjust( res$pval, method="BH" ) +} +plotMA(res) +hist(res$pval, breaks=100, col="skyblue", border="slateblue", + main="Histogram of p values") + +# Write output +res = data.frame(id=res$id, pval=res$pval, padj=res$padj) +write.table( res, file=outputFile, col.names=FALSE, row.names=FALSE, + quote=FALSE, sep="\t" ) + +# Variance stabilising transformation +vsdOneFactFull <- varianceStabilizingTransformation( cdsOneFactFullBlind ) +if (numFactors == 2) { + vsdTwoFactFull <- varianceStabilizingTransformation( cdsTwoFactFullBlind ) +} + +# Plot heatmap of counts +select <- order(rowMeans(counts(cdsOneFactFull)), decreasing=TRUE)[1:30] +hmcol <- colorRampPalette(brewer.pal(9, "GnBu"))(100) +heatmap.2(exprs(vsdOneFactFull)[select,], col=hmcol, trace="none", + margin=c(10, 6)) + +# Plot heatmap of sample to sample distances +dists <- dist( t( exprs(vsdOneFactFull) ) ) +mat <- as.matrix( dists ) +heatmap.2(mat, trace="none", col = rev(hmcol), margin=c(13, 13)) + +# Plot PCA of samples +print(plotPCA(vsdOneFactFull, intgroup=c("condition"))) +if (numFactors == 2) { + print(plotPCA(vsdTwoFactFull, intgroup=c("group"))) + print(plotPCA(vsdTwoFactFull, intgroup=c("condition", "group"))) +} + +dev.off() diff --git a/src/quince_chiphmmnew.cpp b/src/quince_chiphmmnew.cpp new file mode 100644 index 0000000..d8a86ff --- /dev/null +++ b/src/quince_chiphmmnew.cpp @@ -0,0 +1,596 @@ +// steve qin. +// 07/01/08 +//#include "stdafx.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace std; + +#define MAX_LENGTH 500 +#define PI 3.14159265 +#define LIMIT 100 +#define MIN(a,b) ((a) < (b) ? (a):(b)) +#define MAX(a,b) ((a) > (b) ? (a):(b)) + +void readData(const char dataFileName[], vector &order, + vector &data, int *nRow, double *totalcount); + +double logIntPoisson(const int k, const double lambda); + +double logPoisson(const double x, const double lambda); + +double genPoisson(const double y, const double mu, const double alpha); + +double logGenPoisson(const double y, const double mu, const double alpha); + +double logIntGenPoisson(const int y, const double mu, const double alpha); + +double logIntTrunc0GenPoisson(const int y, const double mu, const double alpha); + +double logTrunc0GenPoisson(const double y, const double mu, const double alpha); + +void pathFinder(const int count, const vector &order, + const vector &data, const int chromosomeLengthInBins, + int *&path, double *&proba, double *&logproba, double *&hits, + const double totalMapReads, const double totalPeakReads, + const double totalPeakArea, const double medianPeakBinCount, + const int numPeaks, const int readcoverage, const int threshold); + +void parameterEstimate(const double *peaksdata, const int peakscount, + const int threshold, const double mu, const double alpha, + double *mufore, double *alphafore); + +double likelihood(const double *y, const int chromosomeLengthInBins, + const int threshold, const double mu, const double alpha); + +double unran(int *na, int *nb, int *nc); + +double gammaln(double xx); + +int main(int argc, char **argv) { + int j, numCycles = 1; + int count, chromosomeLengthInBins, binSize; + int readcoverage = 0;//added 04/13/08 + vector order; + vector data; + int *path; + double *hits; + double *proba, *logproba; + char inputname[MAX_LENGTH] = "10.select.chr21.txt";//"jy9.10.select.chr22.txt";//"sle.txt"; + char parasname[MAX_LENGTH] = "jy10.paras.txt"; + char outputname[MAX_LENGTH] = "out.txt"; + ofstream outPutFile; + double totalMapReads = 0; + double totalPeakReads = 0; + double totalPeakArea = 0; + double medianPeakWidth = 0; + double medianPeakBinCount = 0; + int numPeaks = 0; + istringstream iss; + string lineString; + double temval; + vector datas; + double totalcount; + int threshold; + + if (argc != 4) { + printf( + "3 options need to be specified:\n\tinput file name,\n\tinformation file name,\n\toutputfile name.\n"); + exit(0); + } + for (j = 0; j < MAX_LENGTH; j++) { + inputname[j] = argv[1][j]; + parasname[j] = argv[2][j]; + outputname[j] = argv[3][j]; + } + + ifstream inFile(parasname); + if (!inFile) { + cout << "Error opening input parameter file" << parasname << endl; + exit(0); + } + for (j = 0; j < 9; j++) { + getline(inFile, lineString); + iss.clear(); + iss.str(lineString + " "); + iss >> temval; + datas.push_back(temval); + } + totalMapReads = datas[0]; + totalPeakReads = datas[1]; + totalPeakArea = datas[2]; + medianPeakWidth = datas[3]; + // if(medianPeakWidth >800) + // medianPeakWidth = 800; + numPeaks = (int) datas[4]; + readcoverage = (int) datas[5]; + threshold = (int) datas[6]; + binSize = (int) datas[7]; + chromosomeLengthInBins = (int) datas[8]; + + medianPeakBinCount = medianPeakWidth / (double) binSize; + path = new int[chromosomeLengthInBins]; + proba = new double[chromosomeLengthInBins]; + logproba = new double[chromosomeLengthInBins]; + srand((unsigned) time(NULL)); + readData(inputname, order, data, &count, &totalcount); + for (j = 0; j < numCycles; j++) { + pathFinder(count, order, data, chromosomeLengthInBins, path, proba, + logproba, hits, totalMapReads, totalPeakReads, totalPeakArea, + medianPeakBinCount, numPeaks, readcoverage, threshold); + } + outPutFile.open(outputname); + if (!outPutFile) { + cout << "ERROR: Unable to open file: " << outputname << endl; + exit(30); + }//end of if + for (j = 0; j < chromosomeLengthInBins; j++) { + if (proba[j] > 0.5)//0.01 + { + outPutFile << j << " " << path[j] << " " << proba[j] << " " + << hits[j] << " " << logproba[j] << endl; + } + } + delete[] path; + delete[] proba; + delete[] hits; + outPutFile.close(); + return 0; +}//end of main + +void pathFinder(const int count, const vector &order, + const vector &data, const int chromosomeLengthInBins, + int *&path, double *&proba, double *&logproba, double *&hits, + const double totalMapReads, const double totalPeakReads, + const double totalPeakArea, const double medianPeakBinCount, + const int numPeaks, const int readcoverage, const int threshold) { + int j; + double (*logfnh)[2], trp[2][2]; + double p[2], p0, p1; + double ratio, compa; + double dif = 0, inside1, inside2, inside3, inside4; + int na, nb, nc; + //double lambdaback, lambdafore; + double sum = 0, sum2 = 0, mean, var, nsize; + double muback, alphaback; + double mufore, alphafore; + //double mufore,mualpha; + double mu, alpha; + int bgtotal, number; + double *peaksdata; + //- int threshold; + //ofstream outParameterFile; + + //outParameterFile.open("poisson.out"); + //if (!outParameterFile) { + // cout << "ERROR: Unable to open file poisson.out." << endl; + // exit(30); + //}//end of if + na = rand() + 1; + nb = rand() - 1; + nc = rand(); + // hits are for all 25bp window on the genome. + // only some of them are non-zero. + hits = new double[chromosomeLengthInBins]; + sum = 0; + sum2 = 0; + bgtotal = 0; + //- threshold = 6; + for (j = 0; j < chromosomeLengthInBins; j++) { + hits[j] = 0; + } + for (j = 0; j < count; j++) { + if (order[j] > chromosomeLengthInBins) { + cout << "read bins extend further than the chromosome size in bins " << order[j] << " "<< chromosomeLengthInBins << endl; + exit(1); + } + hits[order[j]] = data[j]; + if (data[j] < threshold) { + // sum = sum + (double) floor(data[j]); + // sum2 = sum2 + (double) floor(data[j])*floor(data[j]); + sum = sum + data[j]; + sum2 = sum2 + data[j] * data[j]; + bgtotal++; + } + } + nsize = (double) chromosomeLengthInBins - count + bgtotal; + mean = sum / nsize; + var = (sum2 - (double) nsize * mean * mean) / (nsize - 1); + muback = mean; + alphaback = (sqrt(var / mean) - 1) / mean; + cout << "background: mu = " << muback << " alpha = " << alphaback << endl; + //outParameterFile << muback << " " << alphaback << endl; + // double aa = logGenPoisson(5,5,2); + peaksdata = new double[count - bgtotal]; + number = 0; + sum = 0; + sum2 = 0; + for (j = 0; j < count; j++) { + if (data[j] >= threshold) { + peaksdata[number] = data[j]; + // sum = sum + (double) floor(data[j]); + // sum2 = sum2 + (double) floor(data[j])*floor(data[j]); + sum = sum + data[j]; + sum2 = sum2 + data[j] * data[j]; + number++; + } + } + mean = sum / number; + var = (sum2 - (double) number * mean * mean) / (number - 1); + mu = mean; + alpha = (sqrt(var / mean) - 1) / mean; + cout << "foreground (raw): mu = " << mu << " alpha = " << alpha << endl; + //outParameterFile << mu << " " << alpha << endl; + //double ff = likelihood(peaksdata,number,threshold,6,0.3); + parameterEstimate(peaksdata, number, threshold, mu, alpha, &mufore, + &alphafore); + cout << "foreground: mu = " << mufore << " alpha = " << alphafore << endl; + //outParameterFile << mufore << " " << alphafore << endl; + //outParameterFile.close(); + // exit(0); + + for (j = 0; j < 10; j++)//200 + { + double aa = logIntGenPoisson(j, muback, alphaback); + double bb = logIntGenPoisson(j, mufore, alphafore); + double cc = logIntTrunc0GenPoisson(j, mufore, alphafore); + cout << "j= " << j << " " << exp(aa) << " " << exp(bb) << " " + << exp(cc) << endl; + }//end of j + // exit(0); + + //+ lambdaback = readcoverage * totalMapReads /(3100*0.9); + // cout <<"lambda foreground = "<= 0; j--) { + ratio = 1 / (1 + (trp[1][path[j + 1]] / trp[0][path[j + 1]]) * exp( + logfnh[j][1] - logfnh[j][0])); + logproba[j] = log(ratio); + /* + if((j>=143890)&&(j<143895)) + { + cout << "path= "< 0) { + logyfac = 0; + for (j = 2; j <= y; j++) + logyfac = logyfac + log((double) j); + } else { + cout << "error, y is negative. " << y << endl; + exit(0); + } + result = log(mu) - log(1 + alpha * mu); + result = y * result + (y - 1) * log(1 + alpha * y) - logyfac; + result = result - mu * (1 + alpha * y) / (1 + alpha * mu); + return result; +}//end of logIntGenPoisson + +void readData(const char dataFileName[], vector &order, + vector &data, int *nRow, double *totalcount) { + int count = 0; + int temOrder; + double temVal; + istringstream iss; + string lineString; + double sum = 0; + + ifstream inFile(dataFileName); + if (!inFile) { + cout << "Error opening input file" << dataFileName << endl; + exit(0); + } + count = 0; + sum = 0; + while (inFile) { + if (inFile) { + getline(inFile, lineString); + iss.clear(); + iss.str(lineString + " "); + iss >> temOrder >> temVal; + if (iss) { + order.push_back(temOrder); + data.push_back(temVal); + //07/04/08 sum = sum + (double) floor(temVal); + sum = sum + temVal; + }//end of if + }//end of if + count++; + }//end of while + *nRow = count - 1; + *totalcount = sum; + cout << "There are " << *nRow << " nonzero counts." << endl; +}//end of readData + +double unran(int *na, int *nb, int *nc) { + double random; + *na = (171 * (*na)) % 30269; + *nb = (172 * (*nb)) % 30307; + *nc = (170 * (*nc)) % 30323; + random = (double) *na / 30269.0 + (double) *nb / 30307.0 + (double) *nc + / 30323.0; + random = random - floor(random); + return random; +} + +double gammaln(double xx) { + double ser, stp, tmp, x, y, cof[6], gam; + int j; + cof[0] = 76.18009172947146; + cof[1] = -86.50532032941677; + cof[2] = 24.01409824083091; + cof[3] = -1.231739572450155; + cof[4] = 0.1208650973866179 * 0.01; + cof[5] = -0.5395239384953 * 0.00001; + stp = 2.5066282746310005; + x = xx; + y = x; + tmp = x + 5.5; + tmp = (x + 0.5) * log(tmp) - tmp; + ser = 1.000000000190015; + for (j = 0; j < 6; j++) { + y = y + 1.0; + ser = ser + cof[j] / y; + } + gam = tmp + log(stp * ser / x); + return gam; +} diff --git a/t/analysis.t b/t/analysis.t new file mode 100644 index 0000000..c6a7112 --- /dev/null +++ b/t/analysis.t @@ -0,0 +1,490 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 146; + +use DETCT::Analysis; + +use File::Path qw( make_path ); +use POSIX qw( WIFEXITED); + +# Compile quince_chiphmmnew if necessary +if ( !-r 'bin/quince_chiphmmnew' ) { + make_path('bin'); + my $cmd = 'g++ -o bin/quince_chiphmmnew src/quince_chiphmmnew.cpp'; + WIFEXITED( system $cmd) or confess "Couldn't run $cmd"; +} + +my $is_ensembl_reachable = is_ensembl_reachable(); + +my $analysis = DETCT::Analysis->new( + { + name => 'zmp_ph1', + read1_length => 30, + read2_length => 54, + mismatch_threshold => 2, + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + hmm_binary => 'bin/quince_chiphmmnew', + r_binary => 'R', + deseq_script => 'script/run_deseq.R', + output_sig_level => 0.05, + chunk_total => 20, + } +); + +isa_ok( $analysis, 'DETCT::Analysis' ); + +# Test name attribute +is( $analysis->name, 'zmp_ph1', 'Get name' ); +is( $analysis->set_name('zmp_ph2'), undef, 'Set name' ); +is( $analysis->name, 'zmp_ph2', 'Get new name' ); +throws_ok { $analysis->set_name() } qr/No name specified/ms, 'No name'; +my $long_name = 'X' x ( $DETCT::Analysis::MAX_NAME_LENGTH + 1 ); +throws_ok { $analysis->set_name('') } qr/Empty name specified/ms, 'Empty name'; +throws_ok { $analysis->set_name($long_name) } qr/longer than \d+ characters/ms, + 'Long name'; + +# Test read 1 length attribute +is( $analysis->read1_length, 30, 'Get read 1 length' ); +is( $analysis->set_read1_length(40), undef, 'Set read 1 length' ); +is( $analysis->read1_length, 40, 'Get new read 1 length' ); +throws_ok { $analysis->set_read1_length() } qr/No read 1 length specified/ms, + 'No read 1 length'; +throws_ok { $analysis->set_read1_length(-1) } qr/Invalid read 1 length/ms, + 'Invalid read 1 length'; + +# Test read 2 length attribute +is( $analysis->read2_length, 54, 'Get read 2 length' ); +is( $analysis->set_read2_length(64), undef, 'Set read 2 length' ); +is( $analysis->read2_length, 64, 'Get new read 2 length' ); +throws_ok { $analysis->set_read2_length() } qr/No read 2 length specified/ms, + 'No read 2 length'; +throws_ok { $analysis->set_read2_length(-2) } qr/Invalid read 2 length/ms, + 'Invalid read 2 length'; + +# Test mismatch threshold attribute +is( $analysis->mismatch_threshold, 2, 'Get mismatch threshold' ); +is( $analysis->set_mismatch_threshold(3), undef, 'Set mismatch threshold' ); +is( $analysis->mismatch_threshold, 3, 'Get new mismatch threshold' ); +throws_ok { $analysis->set_mismatch_threshold() } +qr/No mismatch threshold specified/ms, 'No mismatch threshold'; +throws_ok { $analysis->set_mismatch_threshold(-1) } +qr/Invalid mismatch threshold/ms, 'Invalid mismatch threshold'; + +# Test bin size attribute +is( $analysis->bin_size, 100, 'Get bin size' ); +is( $analysis->set_bin_size(200), undef, 'Set bin size' ); +is( $analysis->bin_size, 200, 'Get new bin size' ); +throws_ok { $analysis->set_bin_size() } qr/No bin size specified/ms, + 'No bin size'; +throws_ok { $analysis->set_bin_size(-1) } qr/Invalid bin size/ms, + 'Invalid bin size'; + +# Test peak buffer width attribute +is( $analysis->peak_buffer_width, 100, 'Get peak buffer width' ); +is( $analysis->set_peak_buffer_width(200), undef, 'Set peak buffer width' ); +is( $analysis->peak_buffer_width, 200, 'Get new peak buffer width' ); +throws_ok { $analysis->set_peak_buffer_width() } +qr/No peak buffer width specified/ms, 'No peak buffer width'; +throws_ok { $analysis->set_peak_buffer_width(-1) } +qr/Invalid peak buffer width/ms, 'Invalid peak buffer width'; + +# Test HMM significance level attribute +is( $analysis->hmm_sig_level, 0.001, 'Get HMM significance level' ); +is( $analysis->set_hmm_sig_level(0.1), undef, 'Set HMM significance level' ); +is( $analysis->hmm_sig_level, 0.1, 'Get new HMM significance level' ); +throws_ok { $analysis->set_hmm_sig_level() } +qr/No HMM significance level specified/ms, 'No HMM significance level'; +throws_ok { $analysis->set_hmm_sig_level(1) } +qr/Invalid HMM significance level/ms, 'Invalid HMM significance level'; + +# Test HMM binary attribute +is( $analysis->hmm_binary, 'bin/quince_chiphmmnew', 'Get HMM binary' ); +is( $analysis->set_hmm_binary('bin'), undef, 'Set HMM binary' ); +is( $analysis->hmm_binary, 'bin', 'Get new HMM binary' ); +throws_ok { $analysis->set_hmm_binary() } qr/No HMM binary specified/ms, + 'No HMM binary'; +throws_ok { $analysis->set_hmm_binary('nonexistent') } +qr/does not exist or cannot be read/ms, 'Missing HMM binary'; + +# Test R binary attribute +is( $analysis->r_binary, 'R', 'Get R binary' ); +is( $analysis->set_r_binary('S'), undef, 'Set R binary' ); +is( $analysis->r_binary, 'S', 'Get new R binary' ); +throws_ok { $analysis->set_r_binary() } qr/No R binary specified/ms, + 'No R binary'; + +# Test DESeq script attribute +is( $analysis->deseq_script, 'script/run_deseq.R', 'Get DESeq script' ); +is( $analysis->set_deseq_script('script'), undef, 'Set DESeq script' ); +is( $analysis->deseq_script, 'script', 'Get new DESeq script' ); +throws_ok { $analysis->set_deseq_script() } qr/No DESeq script specified/ms, + 'No DESeq script'; +throws_ok { $analysis->set_deseq_script('nonexistent') } +qr/does not exist or cannot be read/ms, 'Missing DESeq script'; + +# Test output significance level attribute +is( $analysis->output_sig_level, 0.05, 'Get output significance level' ); +is( $analysis->set_output_sig_level(0.01), + undef, 'Set output significance level' ); +is( $analysis->output_sig_level, 0.01, 'Get new output significance level' ); +throws_ok { $analysis->set_output_sig_level() } +qr/No output significance level specified/ms, 'No output significance level'; +throws_ok { $analysis->set_output_sig_level(1) } +qr/Invalid output significance level/ms, 'Invalid output significance level'; + +# Test reference FASTA attribute +is( $analysis->ref_fasta, undef, 'Get reference FASTA' ); +is( $analysis->set_ref_fasta('t/data/test12.fa'), undef, + 'Set reference FASTA' ); +is( $analysis->ref_fasta, 't/data/test12.fa', 'Get new reference FASTA' ); +throws_ok { $analysis->set_ref_fasta('nonexistent') } qr/cannot be read/ms, + 'Missing reference FASTA'; + +# Test Ensembl host attribute +is( $analysis->ensembl_host, undef, 'Get Ensembl host' ); +is( $analysis->set_ensembl_host('ensembldb.ensembl.org'), + undef, 'Set Ensembl host' ); +is( $analysis->ensembl_host, 'ensembldb.ensembl.org', 'Get new Ensembl host' ); + +# Test Ensembl port attribute +is( $analysis->ensembl_port, undef, 'Get Ensembl port' ); +is( $analysis->set_ensembl_port(3306), undef, 'Set Ensembl port' ); +is( $analysis->ensembl_port, 3306, 'Get new Ensembl port' ); +throws_ok { $analysis->set_ensembl_port(-1) } qr/Invalid Ensembl port/ms, + 'Invalid Ensembl port'; + +# Test Ensembl username attribute +is( $analysis->ensembl_user, undef, 'Get Ensembl username' ); +is( $analysis->set_ensembl_user('anonymous'), undef, 'Set Ensembl username' ); +is( $analysis->ensembl_user, 'anonymous', 'Get new Ensembl username' ); + +# Test Ensembl password attribute +is( $analysis->ensembl_pass, undef, 'Get Ensembl password' ); +is( $analysis->set_ensembl_pass('secret'), undef, 'Set Ensembl password' ); +is( $analysis->ensembl_pass, 'secret', 'Get new Ensembl password' ); + +# Test Ensembl database name attribute +is( $analysis->ensembl_name, undef, 'Get Ensembl database name' ); +is( $analysis->set_ensembl_name('zv9_core'), + undef, 'Set Ensembl database name' ); +is( $analysis->ensembl_name, 'zv9_core', 'Get new Ensembl database name' ); + +# Test Ensembl species attribute +is( $analysis->ensembl_species, undef, 'Get Ensembl species' ); +is( $analysis->set_ensembl_species('danio_rerio'), + undef, 'Set Ensembl species' ); +is( $analysis->ensembl_species, 'danio_rerio', 'Get new Ensembl species' ); + +# Test chunk total attribute +is( $analysis->chunk_total, 20, 'Get chunk total' ); +is( $analysis->set_chunk_total(30), undef, 'Set chunk total' ); +is( $analysis->chunk_total, 30, 'Get new chunk total' ); +throws_ok { $analysis->set_chunk_total() } qr/No chunk total specified/ms, + 'No chunk total'; +throws_ok { $analysis->set_chunk_total(-1) } qr/Invalid chunk total/ms, + 'Invalid chunk total'; + +# Test sequences and chunks before adding samples +my $sequences = $analysis->get_all_sequences(); +is( scalar @{$sequences}, 0, 'No sequences' ); +my $chunks = $analysis->get_all_chunks(); +is( scalar @{$chunks}, 0, 'No chunks' ); + +# Mock sample object +my $sample = Test::MockObject->new(); +$sample->set_isa('DETCT::Sample'); +$sample->set_always( 'bam_file', 't/data/test1.bam' ); + +# Mock sample object with different reference sequence +my $sample_diff = Test::MockObject->new(); +$sample_diff->set_isa('DETCT::Sample'); +$sample_diff->set_always( 'bam_file', 't/data/test3.bam' ); + +# Test adding and retrieving samples +my $samples; +$samples = $analysis->get_all_samples(); +is( scalar @{$samples}, 0, 'No samples' ); +is( $analysis->add_sample($sample), undef, 'Add sample' ); +$samples = $analysis->get_all_samples(); +is( scalar @{$samples}, 1, 'Get one sample' ); +$analysis->add_sample($sample); +is( scalar @{$samples}, 2, 'Get two samples' ); +throws_ok { $analysis->add_sample($sample_diff) } qr/use different reference/ms, + 'Different reference for sample'; +throws_ok { $analysis->add_sample() } qr/No sample specified/ms, + 'No sample specified'; +throws_ok { $analysis->add_sample('invalid') } qr/Class of sample/ms, + 'Invalid sample'; + +# Test sequences and chunks after adding samples +$sequences = $analysis->get_all_sequences(); +is( scalar @{$sequences}, 5, '5 sequences' ); +$chunks = $analysis->get_all_chunks(); +ok( scalar @{$chunks} > 0, 'Chunks' ); + +# Count sequence in chunks +my $sequence_total = 0; +foreach my $chunk ( @{$chunks} ) { + $sequence_total += scalar @{$chunk}; +} +is( $sequence_total, 5, '5 sequences in chunks' ); + +# Recalculate chunks so one sequence per chunk +$analysis->set_chunk_total(10000); +$chunks = $analysis->get_all_chunks(); +is( scalar @{$chunks}, 5, '5 chunks' ); + +# Recalculate chunks so 5/3 sequences per chunk on average +$analysis->set_chunk_total(3); +$chunks = $analysis->get_all_chunks(); +is( scalar @{$chunks}, 3, '3 chunks' ); + +# Count sequence in chunks +$sequence_total = 0; +foreach my $chunk ( @{$chunks} ) { + $sequence_total += scalar @{$chunk}; +} +is( $sequence_total, 5, '5 sequences in chunks' ); + +# Test test chunk attribute +is( $analysis->test_chunk, undef, 'Get test chunk' ); +is( $analysis->set_test_chunk(1), undef, 'Set test chunk' ); +is( $analysis->test_chunk, 1, 'Get new test chunk' ); +$chunks = $analysis->get_all_chunks(); +is( scalar @{$chunks}, 1, '1 chunk' ); +is( $analysis->set_test_chunk(4), undef, 'Set test chunk' ); +$chunks = $analysis->get_all_chunks(); +is( scalar @{$chunks}, 3, '3 chunks' ); + +# Test constructing from YAML +$analysis = DETCT::Analysis->new_from_yaml('t/data/test_analysis12.yaml'); +isa_ok( $analysis, 'DETCT::Analysis' ); +$samples = $analysis->get_all_samples(); +is( scalar @{$samples}, 2, 'Get two YAML samples' ); +throws_ok { $analysis = DETCT::Analysis->new_from_yaml('nonexistent.yaml') } +qr/does not exist or cannot be read/ms, 'Missing YAML file'; + +# Test validating analysis +throws_ok { + $analysis = DETCT::Analysis->new_from_yaml('t/data/test_analysis13.yaml'); +} +qr/use different reference/ms, 'Different reference'; + +# Test summary info +$analysis = DETCT::Analysis->new_from_yaml('t/data/test_analysis1122.yaml'); +my @bam_files = $analysis->list_all_bam_files(); +is( scalar @bam_files, 2, '2 BAM files' ); +is( $bam_files[0], 't/data/test1.bam', 'Got BAM file' ); +my @tags = $analysis->list_all_tags_by_bam_file('t/data/test1.bam'); +is( scalar @tags, 2, '2 tags' ); +is( $tags[0], 'NNNNBAGAAG', 'Got tag' ); + +my $seq; + +# Set FASTA index +$analysis = DETCT::Analysis->new_from_yaml('t/data/test_analysis12.yaml'); +throws_ok { $analysis->set_fasta_index(); } qr/No FASTA index specified/ms, + 'No FASTA index'; +throws_ok { $analysis->set_fasta_index('invalid'); } qr/Class of FASTA index/ms, + 'Invalid FASTA index'; + +# Set Ensembl slice adaptor +$analysis = DETCT::Analysis->new_from_yaml('t/data/test_analysis12.yaml'); +throws_ok { $analysis->set_slice_adaptor(); } +qr/No Ensembl slice adaptor specified/ms, 'No slice adaptor'; +throws_ok { $analysis->set_slice_adaptor('invalid'); } +qr/Class of Ensembl slice adaptor/ms, 'Invalid slice adaptor'; + +# Get subsequence with missing parameters +$analysis = DETCT::Analysis->new_from_yaml('t/data/test_analysis12.yaml'); +throws_ok { $analysis->get_subsequence(); } qr/No sequence name specified/ms, + 'No sequence name'; +throws_ok { $analysis->get_subsequence('1'); } +qr/No sequence start specified/ms, 'No sequence start'; +throws_ok { $analysis->get_subsequence( '1', 1 ); } +qr/No sequence end specified/ms, 'No sequence end'; +throws_ok { $analysis->get_subsequence( '1', 1, 10 ); } +qr/No sequence strand specified/ms, 'No sequence strand'; + +# Check getting sequence from test FASTA file +# First 10 bp of chromosome 1 should be CCAGGCGCGG according to: + +=for comment +head -2 t/data/test12.fa +=cut + +$analysis = DETCT::Analysis->new_from_yaml('t/data/test_analysis12.yaml'); +$seq = $analysis->get_subsequence( '1', 1, 10, 1 ); +is( length $seq, 10, 'FASTA subsequence length' ); +is( $seq, 'CCAGGCGCGG', 'FASTA subsequence' ); +$seq = $analysis->get_subsequence( '1', 1, 10, -1 ); +is( length $seq, 10, 'FASTA reverse complement subsequence length' ); +is( $seq, 'CCGCGCCTGG', 'FASTA reverse complement subsequence' ); + +# Check getting subsequence outside size of sequence +$seq = $analysis->get_subsequence( '1', -1, 10, 1 ); +is( length $seq, 10, 'Negative start FASTA subsequence length' ); +is( $seq, 'CCAGGCGCGG', 'Negative start FASTA subsequence' ); +$seq = $analysis->get_subsequence( '1', -1, -1, 1 ); +is( length $seq, 1, 'Negative start and end FASTA subsequence length' ); +is( $seq, 'C', 'Negative start and end FASTA subsequence' ); +$seq = $analysis->get_subsequence( '1', 1_000_000_001, 1_000_000_010, 1 ); +is( length $seq, 0, 'Large start and end FASTA subsequence length' ); +is( $seq, '', 'Large start and end FASTA subsequence' ); + +# Check getting sequence from Ensembl database +# First 10 bp of chromosome 1 should be TTCTTCTGGG according to: +# http://www.ensembl.org/Danio_rerio/Location/View?r=1%3A1-10 +SKIP: { + skip 'Ensembl not reachable', 4 if !$is_ensembl_reachable; + + $analysis = DETCT::Analysis->new( + { + name => 'zmp_ph1', + read1_length => 30, + read2_length => 54, + mismatch_threshold => 2, + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + hmm_binary => 'bin/quince_chiphmmnew', + r_binary => 'R', + deseq_script => 'script/run_deseq.R', + output_sig_level => 0.05, + chunk_total => 20, + ensembl_species => 'danio_rerio', + } + ); + $seq = $analysis->get_subsequence( '1', 1, 10, 1 ); + is( length $seq, 10, 'Ensembl subsequence length' ); + is( $seq, 'TTCTTCTGGG', 'Ensembl subsequence' ); + $seq = $analysis->get_subsequence( '1', 1, 10, -1 ); + is( length $seq, 10, 'Ensembl reverse complement subsequence length' ); + is( $seq, 'CCCAGAAGAA', 'Ensembl reverse complement subsequence' ); +} + +# Check getting sequence without FASTA file or Ensembl database +$analysis = DETCT::Analysis->new( + { + name => 'zmp_ph1', + read1_length => 30, + read2_length => 54, + mismatch_threshold => 2, + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + hmm_binary => 'bin/quince_chiphmmnew', + r_binary => 'R', + deseq_script => 'script/run_deseq.R', + output_sig_level => 0.05, + chunk_total => 20, + } +); +throws_ok { $analysis->get_subsequence( '1', 1, 10, 1 ); } +qr/No reference FASTA or Ensembl database/ms, 'No FASTA or Ensembl'; + +# Check getting sequence from Ensembl database with explicit connection +SKIP: { + skip 'Ensembl not reachable', 2 if !$is_ensembl_reachable; + + $analysis = DETCT::Analysis->new( + { + name => 'zmp_ph1', + read1_length => 30, + read2_length => 54, + mismatch_threshold => 2, + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + hmm_binary => 'bin/quince_chiphmmnew', + r_binary => 'R', + deseq_script => 'script/run_deseq.R', + output_sig_level => 0.05, + chunk_total => 20, + ensembl_host => 'ensembldb.ensembl.org', + ensembl_port => 5306, + ensembl_user => 'anonymous', + ensembl_pass => '', + ensembl_species => 'danio_rerio', + } + ); + $seq = $analysis->get_subsequence( '1', 1, 10, 1 ); + is( length $seq, 10, 'Ensembl subsequence length' ); + is( $seq, 'TTCTTCTGGG', 'Ensembl subsequence' ); +} + +# Check getting sequence from specific Ensembl database +# Get database name via: + +=for comment +mysql -u anonymous -h ensembldb.ensembl.org -P 5306 -Bse \ +"SHOW DATABASES LIKE 'danio_rerio_core\_%'" | sort | tail -1 +=cut + +SKIP: { + skip 'Ensembl not reachable', 2 if !$is_ensembl_reachable; + + $analysis = DETCT::Analysis->new( + { + name => 'zmp_ph1', + read1_length => 30, + read2_length => 54, + mismatch_threshold => 2, + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + hmm_binary => 'bin/quince_chiphmmnew', + r_binary => 'R', + deseq_script => 'script/run_deseq.R', + output_sig_level => 0.05, + chunk_total => 20, + ensembl_host => 'ensembldb.ensembl.org', + ensembl_port => 5306, + ensembl_user => 'anonymous', + ensembl_pass => '', + ensembl_name => 'danio_rerio_core_69_9', + } + ); + $seq = $analysis->get_subsequence( '1', 1, 10, 1 ); + is( length $seq, 10, 'Ensembl subsequence length' ); + is( $seq, 'TTCTTCTGGG', 'Ensembl subsequence' ); +} + +# Check getting subsequence outside size of sequence +SKIP: { + skip 'Ensembl not reachable', 6 if !$is_ensembl_reachable; + + $seq = $analysis->get_subsequence( '1', -1, 10, 1 ); + is( length $seq, 10, 'Negative start Ensembl subsequence length' ); + is( $seq, 'TTCTTCTGGG', 'Negative start Ensembl subsequence' ); + $seq = $analysis->get_subsequence( '1', -1, -1, 1 ); + is( length $seq, 1, 'Negative start and end Ensembl subsequence length' ); + is( $seq, 'T', 'Negative start and end Ensembl subsequence' ); + $seq = $analysis->get_subsequence( '1', 1_000_000_001, 1_000_000_010, 1 ); + is( length $seq, 10, 'Large start and end Ensembl subsequence length' ); + is( $seq, 'NNNNNNNNNN', 'Large start and end Ensembl subsequence' ); +} + +# Check if Ensembl is reachable +sub is_ensembl_reachable { + my $handle = IO::Socket::INET->new( + PeerAddr => 'ensembldb.ensembl.org:5306', + Timeout => 1, + Proto => 'tcp', + ); + + if ( defined $handle && $handle ) { + $handle->close(); + return 1; + } + else { + return 0; + } +} diff --git a/t/data/test1.bam b/t/data/test1.bam new file mode 100644 index 0000000000000000000000000000000000000000..9866c53442cd77d8d7a220ef31398f3ca7b1ea7c GIT binary patch literal 77231 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 literal 0 HcmV?d00001 diff --git a/t/data/test1.bam.bai b/t/data/test1.bam.bai new file mode 100644 index 0000000000000000000000000000000000000000..0177392cc2632921bc2196b61bb003988bf87f53 GIT binary patch literal 416 zcmZ>A^kigZU|?VZVoxCk21X#wz#t5!GT3xYKs>KW5P5X*B@o37d>}1 +ccAGGcgcggaAcGTcaGGGcGCttAAcgaAcaCattCAGTcatTtcccatcTTGTcaTaCtAaAGATTCtAGATCAcga +CcttCttCcGtctGAGGCcggcCGcAAataGGtgCGggcgCacTaagcTcCAtACCCGttctcgtgcGGCActaCAtgAg +ATCCaAcCgCGTCcAAGTAgAGAcCTgaCGTATgGaTatTCttAacATAcCtAtGaatAttaaTaATtaAGgaACCcTgG +ttTaTctcCtACTtCGCtCgCGTTGGctATgGGTGAggCcTagTGagatTaAtGgTtTAgGgcgTccCtGCAgAgCTGGg +GCcCaTGCcGaTgcTgaCgATgAgAtGaaAGCGaATgGCGCGcAAtTCcAcAAaTAgGCtGatgTgCcTAAcccCgtAcg +GaaGCtGgtcCTcTTTaGctGtCGCacTacTCacCAggCTggcTATtcaagAcCgTaTGttcTGcGtgcGATcGatCtaG +agtgAGCttCCtCTtTAcCacACgCgGacagaCtgGGTAAatAGcTAcGtAACtGagAtAtgCgTgcATaggaTCtGCgC +agtACGtggtagGaGaGaTACAGttTccTACTTGAATCatACgcgAGCGatCGGcCGTACAcGgTtgtgcGAcTatgCCa +gaCCATgaGCTCCCtGAgCcCCtcaTGGTAaAgAtCCgctcAGGTataAgCttACgCaGAGacCctgacaggtAAcGgaG +ctcTAaAAaCTACTgCcCTCGgCTAcAccTatCGTgAaTcaAGAaTGAaaacAaAagCaaGgcgGCCGTaATATTGCGGg +GTATgTagGTAcGGtGgcaTcTgTTCCAaaCCacCgcTgtCggcTGTaTgAAgaaTcGGAaTaTGAgtAGctgTgGaTaC +ggcATcgTacTGcTTgcTtGgAGCCGtCCggAATGGCCAcAgCcaCctCTGcGccaggTGtcTaTatgCAccGgtTatcg +CCTagGctCaCcgCgGAaGGGtTTTGcagcGaCcTTtgcccAgTcgctCATGaTCggtgtGTGACTCAcCCtTatGCacg +gatagatAcaTGATtaaGtcccgcGtcatAGCGTAggaTggAcGGgAgcTtaCgACCaGGTCGCtGTAgCaGCATTTaCT +gccgCgAtAAcAcGGATtTtTATaCTGgGAgCcaGCcAgaCggCtGcgGggagTGCctGtcTAgTGAGAtAcaAgtGttg +ACcTgGCTGagAagtgcctAAAAtCCcgatTCAcCGGtcgcaaCTatAtTGgCTTactgCActtaCTctaAtGGGgcATt +AcACcTTTTgTtCtAtTCccTgctcTCtCctGtatAaCtactTtacgtTtGcGatAgAGAttCccgtaaGCcgcTGCtcC +gACtCCcgCcTGaTgaAcAATgTcAGtaTTGGcaggtaaTCcCCGaaaGtgCTcaaggacggCCatatGagaaaAgGTaA +caAATCCGtatCcCtCTaaGTCgcAgtgggCccCcTAAATAcgCgAAAAaCAgaaAAGtTCCcgCGaTAacTCACtctaT +GgcgATGCttggAGCGAAgGACCCCattCCAGcCGtAcaTcGcGCAgAgggAaGAcCCgggAgtcgcTCAtgccCtCTgG +TtAagAcaCTcTCtTccttTCCAAatAACTCtgaaCTGacagCGTaagGttacCtacGTGtCCaACGTGAcATCagagTt +aacctAGatTAcGcTTGGaaGcAGgGCCtttccCCcGAaAGccTacTcgtGAcGcGTATAggATgACAtAtTtaAaaaCa +GcaCAccAcGTTCtcttgtaAgTTTGCCagTgtTGAtTtCTCgGTtAcCgGtgAcgcCAccActaAcGAtGgcaccgcGG +TGaAaGaGtaTCcgTgGgCAgAGAGtAggTTTTaTCaAtgAgcGtaAccCCcgCCTcGGTctattGaAacaTCCCCcGAc +cCgCCAtggagccCcAGcCaaGtACGcgTAcACTGgAaGAGtACGgTGgAaTaCaTGAgTTaaGTacttcGtgGcTccTt +CGCacgCCaTGacatgAcTTAtgatcGGaAgGGCTaCGgTtATtCagGtaGggtTGTtTGaatcgGaATTCGGCgtTccc +GccgcgcgcAGTgCCGCcgcAaGTCgGGcaagTTTaagACTGCAtGcGggAcCTTgaaaGtctGGttatTAAAaTaacCA +cTcCccccTCCcCCGAaaAATCTgTGgAaGACCcCAcaAggtaTgGActgGtCGggGGgaGgaTAtcaAcacAttaTCaC +AgGGTtcAAAtcGTcggTGCtcCaTAcACtgGGGTGgTtagcaaccgtGcgGTCCCtcgtAagcTTTaccgcTAgCcgTT +tgtTCcggaACcgcTcaCCGCtCgTttAAGgactggAaAaCAgTgCgAGtgAtaGgCaaGttcccTgTacaTTAgAaGTa +GAAtaagTCTTGCaTtaGGgTGtGgGCaGcacgAtgcggAaTcTAcGgacCctAgcatggCcTgTCAaAcAAaGgcgaTG +CCAtaTGAAAgTcCAaaCTGTATGGTctTTGAGGtCCAgTgGCaAcaGcAgcgaGACaAcCggCTaacTTcCcTgtAATg +gaCggtATaaCgAGAtggGgAtAaCCCtcGCgCgcCAaTcGctTtttTgtCTaggatTaggTGcAaTtCCcGCCgtaCgT +GtTTcCCGgAgATaCAAAtcGGatCcaagTGcAAGTaTttcACcCtCCCGcaaCCCgttAGtCtCgAgcGtCcaacgaAG +TgCatCCctGAtTGtCCagCTAGtcaTCgTCGcGAtaCGcaACACCCccACTcCGAtgCTGaTtAccTTAcCGcgtGCat +gTCtTTtcTTTCctGCGCGTtGaTATgATttAatCAacccgGtcccAGtGAAcCtaCTaCAtaCtgTCCTCGACtTtaca +gTcaGAAAAgCTAtaTtaAgaaGCcAttcgaGtATGCcTaaGttAATgcGTcccagaATaggTCagCaAgttaGGAGatt +tCAaCgCCGGacgCggccCaaTTTtgaATCtACACAagCtAgAatgCCCCTTggaTGGAacgccctAGtCtgtGaGgGgg +ccCAaaACGGtTTcaCtTAccCcTCtCgaagCGtTtAAgTctcAatACggcaGgCGatCCCCgTCcTTCcGgGgtgtAAc +CGtgGTaATAGaCcTCCGCATTtGCgcCAtTtgggCCtTtCtGtTaaAcaCAtTCtTaAggacAtcTATTTtcTGCatTC +taTtGgCtgGgttaagCTactgtAgccTCtTTCGgctcACTCCCaTAttAcTCACcaGctTaTGATaAtTCGgTCTttaT +aTtGaCcTGaTcctggctTtTTGACGTGctgTAgGtGTaaGTgTAttCATtcctgAcAACgTtAAaAggCCaTAGtGctt +cacctcTCGtAtGATCgCtCaAatccgCGcGgtccGCcCacGGCAgaTTcgGcacacgcacatTTCAGgtttcGctcCAt +tatTcAtcTTTgCaTgCGaGaaCAgaTcgAGCCgTCCcAGctCAtAtGtcgTcaTaACTTAtcACGCGTCAgTccCAaCa +CGcatTagTcagCaGTGcacTTCaaACgctgGttAagcaGtCTCGAAaattaTacGAAAGGacAAttTtGCTaCagTcAT +gCaTGtttgAaAaaaTcaGTacAttaAagatGcGtcaCttCTaCAAGGACgtTAgAaATctGtcGgAttGcTcCtcacTT +cGtTgcaCaGCtCgGCgAgAgatcaTgTGCcAACgTTtggaccCAGtgggTTcGagtAtTGgattcctcGTGtgTtAaga +AagAaTCAttGtTtTagGatCCgACCGtCCTaGACGtAgGaggattTgTAACCGTgaTCAcgCCattGtTTAcgTTggGt +gtACCcgattctcCTtGTGgGAAgGctagcatAgcCGatGaCGGCgGGGtcgcCccatcAtacCTcAgTagccgCGtTac +tCCtctTtcgAtGCTcGCcTGCaCTACAtGCCtgaTcCTaGaCCGaCACcgGTgatcTggCTACcggctAgCcAAGtaAt +CGTcGatcaActtCgCttTTCCccGttCCcgtaATTTCaTAGTAGcAAAgtatcgacCtaGGcCtagatCgGGgCGGCcG +tgCAtCaCTTcAttTttGAcagctTgtAaatCtaAcacCTcActTCcgttatAacGcGActaCgCagAACACGatcCtgg +ATgccTaGttGTTTgGcaGtCtaGtaTgTGCtCtatGTtcgTcATtTtcCCggTttgCTttGTcGACccAccTGTAGCCG +tCCcgCgtTaagTaccTGAcaGCCtcGTtattaTcaTGTCAaGTAtGTGctgaAGGttatgTTcGtgctTgtgccaCGCc +aGTCTGgActTcTTAaGgcaGcTGGATTAaCTCcggttGAaATATGGTaAGgTaCGgCTaaaTTCGaTCCACcCGatccC +ttAcGTgATctaGgaCtTtTgTTTCgTcctGCtgAAttCttatTtTcaCgTTtcGGCtGCCCttaAttTAcTGGgCgTaG +ccTtaaaggACatCGTATcGGCcccATtGggacTGAccattCTccgaCggTATAtGGGctGGGcaCtgactGgGcaCCgA +GTtAGagTcAcACAGTTCtACtTctaAccTgTaTtAaacgAttCaCCAcGGaGtagatCGCGCgaCAtaAgtCCcGgcCc +AGAcgCgTTGTaAGAgATTactagcgcGacGggGggGcGcTatgACAGCgTTcAtGcCaTagGATGgTGtGCaCTatctG +aTTTccCCAggtCCAgTAGAAGAgAcTTtCAgACggCTAtTCGGtcAAtGAaAtTtCCggtaGaTAtcAGCTGAgaAtcT +GaCgacaaAgCCCttaaAtaaaaTCgtccTacaCGaTAgGGtgGttgatcTtCaTTCttTGgATATGCaGccgAaacGaC +CCCctTgAGAGCttgCAcggcGTTtCggtgaGTtGacCcCGGgTCgtgcAaAGGgCCCgGAGCAaTgAgTtTATcctGgT +gTTCGgCgctcGTcCaTcgTcCAagCgtCTAaGATcGCatcaCCTAtTcAcGAATTcGatcgAtTGCgTTaTATgtAGTC +aGTttaCcGtcgtagAgaGAtCGTAacgtGGgtaATTCaGACtTtACTttTGttggTgagtGtccAcTtCaATaTTTTaa +ACaTGGATGgATcaTACGaGAaaCtcCtaaaAaatTcGtgAgGTgaTatcTGGTGCgGaCTcacTacGTTCGaAGTAAtA +aGGAGacAcCctATAtAaGTaCCagccctaGTtCtGctAgcaTcttATATAcaaCACTATtaaaTCAgACttatgcAaGc +CGGtTaATGACGAgtaGTCccacaggGCAGaccaTtCTAggCtAaAgttATgtCactTtGTaGcAtAacgCCtCACtAGC +TattACaGtcaATCCTGTgAGttttaaCcGggAtgTTCgacgaCGcGGcTTcatAtACAttCcgAtGACAtcgATTaAAg +TGGgCGcTCTctattGtgAAtcAaCCATaCGgGcaCTATaCGcgCCggAATcctCcgGggACctGTTtGccGTcGTgtTG +CAgTcCtgtgcaTTCAGAAtTttTTTTAtctCcTCACCgTagTTTcGCAagGtAtACgCTtGCcaaGtgACggacTaCac +gTccAAGgGGAatacAgtaggTAgAAgACGCGTaCccactAGTgaaTTgTtgaGctcTcCcccTttgggaAGtggCGgac +gctcCCttGACGatTTCgCTGCGAtCggCGGTttgatGgAgtcTaCTGtcacCCacgctTCgAAcaGaAtCgCcgTTtaC +gATcACTTCAtttaacaccgcATAcAAActGTCgagaAaGtGAgTAgTCaaTAAtccgagAGgaTCATcTtaGcacATCa +CaGGGAtAAAtTAAttGaaaCgCctgATcgcTCGgGtttTgaCGGctCCcCgataAGgCAgtaTaCaggtGAaCGaaGaG +TaCCTgGgcGgtCcCgtGTcccCTCTtcagcggctTCtacTcgcGagAAcACGGaCAGtgcGGaTGCTAAaGatCAaccT +aGaaAttgCGgtATGCcGGCAtcTcacctaCgACGgaTgtgTCCTaAATTAAaaTtaGgtcgCcAaGGtGCgtTGattCc +ccGCGaTttaGAcGagGGTgCGTGcTTCCAAccAagcgCaGTaatTggTaATCCagTcTGaGtGcaTgcggCAaaAcGcT +CCAactTCCatTcAgCtAaacTtGctgTcctTcaAGtctGTGTCgCcTaGGgTGctacgGGAGcgCCatGcTaGtAGCTC +atTACGGGaaACaCCtcTttCGccgAGAcCaTTgACCgagtAAgtgttTcCAATCtCgAggGTaCCcCggCcTaaTGccc +AtcTCttGaAGGgTCAcggtTgttAaaaAGCaATAGcaCCaggAAtATcCCgcAtACggaAgcCATtAaGGatGcAtCag +CcGAtTGcTCATaggAtcAaGgCtCctgagcTGcCGTCtaAcgagTcGGtcctcAcTcTGgtcAgcAggtctcttCcCct +aACaTAatGCggtTCgACCggtTatttAaCGaGGgtAtTtGCttCCTgacAcAcTCtGCaATGGAcCCagCgGtCTTggG +cgCACtGtaCCTcAcACCatGgTaAGGcCcCTcCCcTGaGgGTAcTCgcggggAgctGtgcgaTGTTTTacTgctGTagc +ggAAgagTGGcgGCCgcGGaaggtggtATTGCTTTtgTTtCaGtggGtcCcgaTCAgGaacACctcgGGCTAattCTGcC +gaacGccctctcgGAGAGATtGGGcGtatgACCaGCcactgaAcgGTaTAGcTTtCAgACcATGTTCcCtGtccATgacA +CTtCAcAGaAAAgCgCcGtatTCAcTatCGtggGaTAtATgactGGtgcgcTCaaCCAggTCgAatAgtGTtggAtCccg +cCcCGagtTCTtTtttgCttttAtGGtgAGGAggaaCTatCTCcAGGagCGtGTGtgtTCaTGCgccACtATATaaaATT +gAaaGtgAGtagccaaAaCGctTAtAaATtAgAaTACACtaAacGagTAGAtAtCTcAcgATCTgcGatTTgACaTtaCG +AGgTaCaGctcTatAcGtccTTaTagcAtTaATCAcaAgcTCCgtAccACTtgtggTGcCACAggAacTGgtaGcaTgGC +CGtctGtGAgCTGTCAaCacaAtgCTGTcACCAaCcggCagtcGcCcAcaTGcACcTCTcgCTGCcaTGcGTgaGcgACG +ttgtTCTaAattaaaCAGAtAACTGcCaAcGCgtatTtcgcgcAGAaGATCTtaCAGgcctCAcTATGtgCgccGtCCCC +GTACGTgtcAtGTTaATgtgtGgGTTtTCTCGaCAcGAcCatTgggGAcaGgcaGcaGTATGcatCAccCGtAggGCtcG +AaGTgttaTtCgagAaAgACAcGatgatCgAattCGGtAttaGGgagcTtgAgcGAAcCATctAgCAttCCTaTGtAaGt +TctgCcagGAgcCTAcgtAcCaGgCCCCaGCTcTaTGacGcaagGgggTctcTGtcgAAACgTctAGCcAGgagggGGTg +cGtcgAgtACTTCCAAACTTCcAAGcatTcaTaCgctCTcccGGaTtAGCTTCcTGCgACcaGtaCaCAaCGGGCCCcAa +TgaaGTagTTGgCTTcAcCaACaaCAAtAttGAatccgAgTggtCactTtgAacaCtACaggAaACGtAaTTTtTGTAca +AACcCtgcacGTaaaaaTtcttTctTGTCcAaGtCcAcgCaGGcgagtTAGGtcAaCcGgGGAGcaAaTTaTAAcgAACG +CGAActcggcCgGCaAtaagcGgAgACgagttAAGcTtttccgCgCgcTGCAActgGcGggAAcTtgcACTTacCTcCCA +gGcAgGCACcAGaAaccGaACctTtgaaTGGAAtATACGCgaCtgCtGTCAtacgcCTTtCCGCataTAAtGGCCGaGTC +AgTgAAtgtTaaTTCTagATAccGTaTgagGCaaTTaacCAataCatAgCGaaGGTaTCCatAGCCAcggTTcAgttTCa +GacTGaacCcAcCGACTAGGAtAtAcAaGcCCAgCGTcACcTcGCctAcTcgCaTGtAtgcTTTATCgcACAaCTTTgTT +actCtTGcTGtGcgCgtAACtctTTcCTtgTacggCGAGTAaaggGaaCcGtCgcTaAGACCcAaacGaGCCgccAgagg +TTtgATCAcTtCTTtaAaaTtCGAAaGCACTCcAcAtAgGGGCcaatgCtGcCtctgcgctGCAgGcgGcaGGtgCTCCA +cgtGaagcGctgGcaCTAtcAACgAgActtTtctTGcaCagATaCgcGcGcctTAcctaaAGccGaTGTTGgCctGttgA +actActtcAaatGcaAcTGCATTCGtGAtTtctacccCcATGaGTACCcaGTTcCctggCTCaatgttcagtCAggGCat +ctgACtTTtGtagTTGGCagctGgCtgTAAtCTTGgcGacCcGCACgCGtGTACGTTAtacggACATcCGAgaCGcTgtG +ccGGCcCGGaTATTGgCCaACaTGAtCgacaGagcggGGtCgGGAAGtaGttGAcTAAcAtGaGcgTCCAGcacCCgtag +CcgCtGcTGGctcCcCcGATtTccCGTcaAtttTtTcCCTGaaCtgAGacTCACTTaccTGgtCGcTGcaaaatAgTCcC +aaTttATGCtaAagctGCtgAgCaTTcgAgacgatcaggaTtAcACcGGCgCTttCtCCggAAgggacTaGttatGgAcC +AcatTTCtGtGctcTacATtaAtCTTcaccacAgCAtGaTGTTtcgaActCtttGTgAAAAAtAgCTcc +>2 +CtcAGcATgTaaGacgtcgtgagCgACCcaTAAggaaTAtTcAgcATaAgGCtgccGAagAgCAtCTATttTTaGTACtt +TatAggACCggCAGGtAcccTTgccaCctGGaCtTTAGACgaGagcCgttGTatTcActcTccaGAaAcTcttACtGtAA +CTCcgtTAAAaagCACccgCaGCaacagCtCTgCAaGccCtgCTGGacgtaaAAacccGAATTACTcctgaCaggagcAc +CTGTGtTTaTCCGtaGgTCttActgCaaAcgATCtAgcCagctcTGTCAcTAAgAacgCGCCacacGTgAagAGATgGcc +ATCtTggtCCCcCctaCttGtgGGAACTACagtATTctCTAcCtcTATaagGaAaAGgcACaatGggACTatCcTGGagt +AaTgcAaAGCaGTCaTtCCaACgTACggAgatAGctagcgCGGGGCgCcCAaGaGGGTTgAttATagcAtGGTCGgtAAt +gtGCAcGgATtgaggGTgacGtGGACtCCAAGTtCgGCCtTcTcaACTGatcTTGACccGATTTGGTgGcgTgGTTaACC +GgaTGGAGgCAaCGgaTccgGGaTaacacCaaaACGggCAAAGagTTgtattaAttCGggtGTtTcGcTACtTcACCGcg +TcTgctAgtccTaaGgtAAgtCTTtATttgaGACCcCAgCcGgGgTTTcTAGgGCCCATTCgCTTgctgTCGGgCTgCGT +aaCgCtAAagGagtgtCaCgAtatGTCATgtataaAGcGCaCGGGgcTCTaTTaTAATcgGggcACTaAgGTAaTCcaag +AGcGCcgtgGcGtTCGTtaagACACtAAGAGTcCagtCaAGtCGaCTaatACATtaAAgGaACccCtgAtctCAGAagGa +GactaGgAtgccGcCcaGcGagagcCGtTTCcCcatgATCCCcAggcTACGTgATaGGTGagtCgtACcggcGaGAAAGC +gtggCTcGgCgttTggcggCtATTTaTtcttCtCgttgggCTTAGGACtaagAGgtCACtTGaGggCtTTtTACcttgAt +tCgcAgAtATAGgacAATggcgACATtTtGGTtTaTGTGTAcCtAcTaatACcaCTtcgggtTaATgcTAcAgaacaGtg +GTAcTAtAGTcCaAcaTGgGactGaCCCaTActGGcCagTGtttgATCtctTGacTttacaCtCaCAaACGtGTtgACTt +AtTatCtGAGTCCatTgtCAGTaaaGTTgCcctACaCCCtgCtaaTtcggAaaGtccCgtcAccAcTTgTTcTGTtaTaC +tATaTCacATAacATaGccaagTgtCTcTcTGCtTAGcGTgaTATTcGGgCcGAtgTgcccGTgGAAaGCtGAGCttaCT +GtcACcGTAcAtTGCCGAgTTTTtGtCcGGcctAaataaccGaTtcAcctAaaAtcagTggaTCccGgaTggCAgCCCga +ggGAcAACaTGgcgCcCtatCcGgcCCgGcgtCcaaAtCttCcTgtCGtgcaAggTATgGatcTAgcTcAtagTaaCCct +taCGTgCCaGgCtgAGacGgGGcaaaAAtTggTcCaGTGccatgagCttgTtAaCTgACtcccCTcCCgAaGccGGGatt +tgaAgaGacGgATcCTtgcTaAATTATtGTgGTGtTACgctgCGTGCCTGTcCTCGCGTtTAACTGcTAGcGTAttcCcG +CtaAAgATGaGCgggcgCAAccCtCAcCATtaGCaCCTTgcGCtACTtcaCcaTAtGtaCtGcTgCTtcgacacTgAGAc +ggCgtTagtgCaccggGaAgAcagggGcAgGGGCCggGCTGaTGgCtgGTGgtAtGTGgAGCaCgGGGtgAcGagAacGa +gTcaaGgGtatCgAGaCtGGtagcctcAccgGGggGCAtcCtgtgATGcAtCatGAgAgaAGCTgcATtaAAAcGTcctg +cGtCcaTaATAcgAAAgtTggAtTatGCaTcCttAgaGGAggATcAtgttcaGttAGatAAcTTgtAacGAcatTgtCgt +gAattcgCgtgataCGtcggTctCCtAactgTaccGcgAcTCCTgaAccACcaaGcGCTGgatTTTAGGACTctGcaCCa +AAaTTtTAgCgCatgaAgccACCgTTtCACtTGTCAccaagtTaGAtCaCTgttTaTccaaATGGcccCCAcCCgctggt +tTGTtTaGaATtTctcaAcTaTGagACcCgaacacgctCaTCGaCgTATgcCGGtCctcGGggtGAGTggtAcAGTTagT +caTtcTgAcaaCTcgcACtCTTTgaCACCTaGTgATCtaAaCtTCgcGAgAttgaCAagAacTaGtcTatATGaAacCCC +TtTGGTgCTAggCAaaTCCcTgAgAgaTAgTCCataGaTTCGTATActtGtCaTatCGCaAgaaTAtTgAgcttaatGTc +gaTGTgCCgGtgAtAtccgTCcTtcGaGCgcTaTtGTaGtagtcTGcTCgTCTACGtCTaaTTtGGGGCgtCttcTcaAA +TgCaGaAggtGttTGacAGTtGTCttTAAGatGggaAaggCAcgtTAtcGCACaAtcGACcaAcaaAGCGTgCAccgTAC +gaGgCgcAGcaGGGtATTtcCaAAcTAatACaAAGATGTtGtGCatGgGGTgTcAAgGgTCGcTGgtgGgGcgGtATCgt +CcgcgcCcAGaCaAgtGaagttaagCgTTccTcCActgcTgGAGAAggtgccaTCTtGgtaAtAataTggaGcCTttATg +aAActCTgGTACCgTtCCTTgtTttCGGattTCtaATGtTcCacCAactgGGTAaGcTGcGACGTcTtCaaGTACgcAGa +GcGTGATcGGCaCtgtTACtTTcCgAGCcCCcAGtagAaACagcttgGatcgTAtCACctAACaAtCgGGGGTCGgcacc +ActCTGTAGGaGgGAcACCGaAtaTgCaTtgCatcGTCTaagCCGaCatGcaaaAatTGtAacCATtcGAaGcAActcCG +aAtAgCGaTGttaaGGCGaggagatCcaGtgTACaatgtAGtggtatggatACCttgtGatagatTAggtCtaGACcTgt +CtttgGAaGCgtgcaTGTTaACTgATacTAaTGagAAggAgcAgCGGCAgCATCtTGTagCCaTcTcaAtcTTgctgggG +CtTCAgCaACtGcctgTTTAacGGGcAAcaATCtcAAGtggGAaTGGgAtccCaAAgCAAgTTtcacCgcGaacaTGTAc +TTgCCtgcGgcTaTtCCatCaAcgaaTGtTAACtgTtAGtgtCTgAaTcGCAcCAtaCaAcAgaTAtctcCAAGacGaaA +ttTctaaCgCaCATaAcccGgCCTCtgGCgaGaGACcTcggCTCaCAACAtTtTgtaaTGCCcgagcTtaTActCccAAt +CcAGCtACgGtCGAgTGaAaTTaagAcAaATagCaTatgTTGCCActaGcCtGTGatGGtgacAggAtAcatGTtTcAGA +aTGCATtAtaCgGCTCCtaagCTTtGattAtcAtCgGATTCCcCattgtccgTcCCcGtgcgtGaGTTacaTtacAaTTa +agTcaGCATTGacGatCaaAcGgAaTGAttgacgGCCCAGAcTTGCCCttGactCAACtcCCatgCatGCTcAGtGTagG +ccCcCtatgGCtCgGaGAcatGggggTtATTtgCCtCTTGGGACgGaggCGACCtGaGAGCTCctccGtcCgaGTgtGAt +TacCgccctaAgTctTtCgTgTCcaatAACgTcaGggtTcGatCTtCACctAtTggaataCgCtTaaGaAaccCgCaGaa +GGTaGaGggcgttacaaTTAGaGgctACtGcTccCGcTtcgGggcatCtGctgcCAACAAGtcTgAcaacgtAtcACGAa +GaTgATaaTtTTAGCGCcgAacaTGGGctGAaaccTGCgtcAgCtgGTagGaTtTgtGATAgcCcttccgctcAaagatT +aGtaAacctatCCTGTagGCtgGCgGGcgaaATAaCcTaaaGaTttagattcaAGGgCcAtcGTgaTaatTGTGtgCctt +cACcACACTtAAgGTcGgaaGcAatcgActCtCGgTTAGacGGatgcgcTagAAGagcCCCaTCGTaCGCTTaCaaGagT +GAtTAGatGCCcGaTTAaAagggACCcAGTccattaCtATgTTaATTAGcgAtaGGAtCaAACagGtgAAGgcCcACtca +agGTagagGatgAacCaTCTAAggAgCagAtGGCTtgGCttGTtGccAtgAAatGtaAcCAGTtgGCAacACGAcATTAA +GgtCCaAgtacCAGCAGgcaTtgaGCCGggaCCaAGtcttacTTcttCtcgCCatCtcGACaAAACaCggcAcgacACTC +AttcaTGAggttAaagCtaaagaCAGAAGaaCTAAgAGTGtAATTtTGgtAcGTttTAGTaCTgaCaTCCCAGGcCCAGa +aatActAagTtatgATGtCCccGgCCTtatcAgGGCAtgGgCGACtAATgCTTAAGGTTCatCGtcaacATcCaactGgt +TtATcgCgTGAcGAGCATGtAAaCAccTGCGccGtTaAaCcgCCttGAgaGTgcAgaGctgGGgGcCtAgtATCcTGcgA +cgGgAaTaCgTAcctagAgTcGCGGgttcAgggcCgAgaTAgGgCgAtcAAgTCcAcGATCaCCgGCgatgTActtttAC +CCCcCGGAAGCCAaCAcatgCtGGaCttCcagttAtCACaTtacggTGCGcttgaGTaACtCctgGtTTGaCaAcTTTaA +gACTCACGtTTTtGagGgatTcCtAaGgCctCTGTactCTcaGGgcAGCAcCAaTcGGgTctAgcactTaggGaAtcaTa +GttAtGCccAtagCTgagActggccGAAAcTggGtGATAgtaTGacTCAaCCCgAATCCccgaGagcgTGaatGAGcTaA +AGgTaTatcAAgtGGGTaGGActGTaaTgtCTCtgAaCGagGcGgTGCAgCgttAcaGAagTtTGcGtCTCggAAactTa +AAccaGTatGCATACGtgagaTgcGcCTctCCTCCgacAagTGcgaGAGGatattGCATAgTaGgaCgtgcCAAgTCagT +TgTGgtcatcTCaATgCacaTGtTttAaCAacctctAagacgAagATtCgTaCGtCGttcTGTAagATtaatCCTCttTG +cAtTACaccacGtcGgAAgCCGaGGaTCaTtTtgCCACcGgcACAcTCaaCCGAATGCTATataTTCgTtggatGGtTGg +gcGgatAAGaaCAgggAactAtGAGaTTggActAGGCtTctTcCTgaAcgtCcAaTcGtCttCggatcCTtaCGACctCG +AtcacAAGTAGCGTttccTgTgtTtGgtGtgTTAcaTGtaaCgCctCgtAcCgCaactTACcAAtCtTAtaggGTGtAcG +AgatgCCGcACaTTtGtTGaAGAtGTgatTAccTTAgAtATACccgCtaGaGCcgCTAccCCgCGTATacctaTTcgcAC +GtTgGCagccACTttGgAGttgCCGTTcatTTgAGCCAtttTTgttCAcATCgctCTaaacAaCGcAtCagAtCaGGGAA +TacgAgtGGTgggGccAcgTcCAtGCcgTCTcCGgcaAacAcaGtTCtcgcAaATacGcCaATTCGACAcgcaaaTgatC +GtccgttcGTAgACccgtCtaCTAgtcCgGGCTaGcGCccaATCCAACGCgaAGtCtacTgCtCgttgGACgGccaGgTT +GgCTTcggGGGAGggaAGgGCGtGGcgtgGACAttTAgaAcGaTatcCgtTAtAgAactCCGcctaGAcTgAacGgttcg +acAGggCaaCtCaacGTCTagAGAgtAACGgACATgcTGCTgaaCTGactgtAtAataGacCAgGaActtGTGGagCATt +ACcAccaTGctATCtCTacTgTCttCTGTcAtgatcGGtttAaAtcTGGccAcCtgTccGccAATAaACCGGGtgCCTGt +ctgAAGTttACCGtaAACaGaCTtAccctGCttaaaTacCagctagtTcagccataacgtGgATACtGAcTaCaAAaTgg +TGaCACtgCTaCGGtaGGgAgaACcACCTgttGgGaGcCCACAaAagtAcgtcTCCGcggTTctaCGACGATTcctcGGa +gaACcTaTtGacTGTgccgAcGGGAtaTTtTCCCtcAgCgTCgaTgaACTtcTtacttatgaCTcATcgGttCgTCGtga +tacAaGGtgaAGCggAgcGTCacaGATcgAtTTTcCcctAaactctTTCtcCGCaCaagaCgcTcgTcGcATTTCcTTca +aggaAgTtCcttCAaTGTCTATcCAcAGtGCtCCaAccgaGTccTTcAGcaCCgaatgTATcttTaCacccAaTaAcCgg +tccCTtTtTttagAggccagaCGtttTTTGCtaGaTGtTcTCttTACACtcTTtcCaaTAgGCGtCtgagcTCaCCCcga +aCAaGTtaagAcaCggTCtGgTataCtTgCTCactGTtGCcgtcGgGAaTAgAcAGcctCCTaAtTgaTtTTGGtccCCA +tcAAaTTGCAcggaAaTactaTCGTcgGTGAGCaTCgAAtcTCAatCaCgGGccTgcGCGataCgcATAATccTCCGaTa +AcGCAtaaACTGgCCgAaactCgtAACtCGAgTcacacCctCGAaGaTgAacATaGaTcACGTgAgGTgAcGTgaCcgCG +GtCAAACGgGaTcgCaCCGAgCtcTTgaCCatcAgctgaGTtgAAgaagCaCtcTGgtCcTgCTagTTTaTccaGCtggg +CttGAGCcGtaTCGAgCGaTTATcgCgctcgGGAGgCCAgCATCTgCGctaggagAcaATtgGgcGGgtTtAtgCgTCTC +TCTACatTCacaaCagATTATcGCggACgCGttTcGGCtcacctAaTTtaTGcCaGAGTgagCcCaCCtAAtAgAcacaG +GGGcGagaTGCgaatcagactTGCAGtctTTCgatcTatGTTcgtTtgAttgACcCaTCAGgcAcGagCaTCccgaaCCC +TaTTGggAcacCTaTCgGaaTtgCGaCTTCtCGgGaaAtAccggCTAaTggCaATATGAtTAaaCCgcCtACaCaCtttG +gaAgctTGaGtagCgtGCGattatcTCtgttgTgAgaCTctCCcgGAGGcgCtAATCgcactcTtCaAAtTCTggtAGTT +gaaacGgaaGGaCctaCtcgAGATaAaacTaagcgtTCtTggCcgcTttAaGcatcCcgaAcGggcTcCgTGTagaTgAA +ttGTAAtgAcccGGGcCTgaaaCCcCCTcaAGTaACttaTTTAcaAGatcaCATagcGGCGacCgctctcCcAtGATGgc +gaGcGtggtTaTCCTgttgTaaATgcCgacATGaGCcAaCAgtcTAccgAacGgGcGGGgGaTggTtctgtCgtgGataC +aggTTcAcgctAAAaGAaacaaGgTcACGCagGagaTCtcgATcCAggTGTgCaAaacAGCCcctaGccTaAgtcAaAgG +AcAcCttCTtcttCCaGGATAGtAccAGATtCTAtatcATaAAaTgTCctgtcgcgcTAtAcAGcAcTtcgAgagGcGgT +ataTGCtGTGttCggtcGcATAcATtacGtaGaCagTcATTGAAggcgCcGCcCtACtCgTgGgaGCcATAActggaGGT +tcCAtTttTTtatTgGaAAAATAgGaCAGgtgGGcgaGataggGCtCcaTTGGaTcCgaAACAagAgATGgggcAGATCA +CTTcaCaTGCaCgtcgAatcTaaAtTgtTTTtGAcAtACacTgCGTctctGttActCTCaTtcgtTaCACcGtAgtAttA +ACAcATCAaCtCGGtggTAtaggaCCgcCtgAtcCcAtaAcGtTAGcagACaaAGggACTcTagacAgaCAgGatatTAT +GttaAAccaTGgcCcaAtccATAcAtgctTgagagCTgtAtATGAgTTatGCAcCCtTaACCggATggtCAGAttcGtat +CttcTgCAccggCCGCGaAgttCTGcataacCgAtCGG +>3 +agCgctggTtTaGTCAATaGGcCccATcaCGcGgtatccgaAcaCcTTctACtTATTGcGACcCGtATgcccaGacgtGT +TAgTATcgCgtTtGGTccAGGGACtCAgaAGtagCtTCCgTGGCtaATcTCgCGTTTtgcAGAaTAtGGGGTaTagaGAA +cagccAcAATTCTGGATTtTtcTAaCtcgacctaAcacGgtaagggACagGacGcGccCTtTtTTCacgAcaGGCgcgAT +gCTgCgcCgCAgCctGACAtCGGAATTTAAttAGAaGCtcCGtTgACgaAATTcTGGGCCagAcCgAcaaGcTcCGTgCA +tGAgtCccCgACgAtctgttgcggagCTTTTaTaaggCcTGatTAAAtcGAttGtttTTggtAcTaGtCcaaaccAgGAt +cGgtGGCgtTGAtTTCTaAGgAGAagCGtaAcgcCcGTggcAatcatCtcAtcCcCCGAAAccTtccAGAAGCaAcGatg +TAaATTACtCTcgAgaagATCAGgcAagGGGGatTtgatCGgCGgGcCTGtccTTAaTActCGTatgTGgACCTTtcCAT +gAgTTTgCcCgTccatcGgACCaGtAggGactgaTattgcaGAattTcAcTCtctgtgGTTCTAccgATTaaAcCgTaGt +tcGTtaGttgTTggtTggTcGGAAAgtcTgCcTCCtAgTgcTcCCtcAgcTaTaaatGCgTGTcTtCTtCAATcAAaCCg +aTgaCAGCGcgTGGATTTGTaGGgAggGGGTtgGTcGgaTtaACtcGAcGACCCAagActtTcCtCCTatAGttAcGaAa +cTtGCGTgTacGtaccGCCAgaaGcacgTcCgTTAtgACGTCTGCgACAaTGAGtttatgATtAtAgcCcCtACaGCcgt +AAtAGCagAGTcaaagGCATcAaaAaaACGcgtCATtAcgtcaCttATTCggaGcttGCTtattaCttCtTTgcGTgccA +TTTCgTtttCgTaaGtccggcagAggaCTGGCCaaCTCtcACctTGGTtCtccCGcGtGCctaTGcGcAtGgaGgtCCcG +tGtCGAcaggTCCgactaggTGGTaaAAtCAcCatggtAgcCGaaTtAgatttcCGACcTgTTtGaAgTCCTTgGGATGA +agtcGatCtCCcTGGattGgtcAAcGACAaaGTaAtCgGtgacGgGgTgcCtcacGaCggCACAAgggGtCgactACCCC +gTTCtCgCTgAGcAgaTTAgTCTCtAcgcaGgaaCAccccTCAGTtCcGtATcggcGCCgtATgTggtagGtaAGGCaCt +aCGCTActgGCcGggTCGAtagacTctggcGAtaagGGggaccGCcCaTgGATTAGTaTAtgTCcTtTAgAGTcttCgaG +cGcGAAACGggaAATCtTCAcTggAGaCaTttAtCGGccGagCtCGACCGGCCgcttAtCGCcTgCCgtCTTAgaGgAaa +AGaGCtCGAgtgTtggcaAcTccAtCtggCATtaTTgTCgTaTTTCcGGctagTCCGTgtCgACCAgAcGaaatCccAgc +GtgcTGGTaAGggCGtGAgtaTCGgtAACAaTGATATCAtggtcaaAtaAccggCtTTgCCTgaaggaaTgcGTtGTTtA +tgCTTtcTGtTGcAttaGgAGaGGactATtcCGttGgcAcTGaACtGGgAacaGCTtctgcaccgaTACgTATtgCaGGA +tgcgccTcggataTagGcAgCACgaGGACgCcCagACGAgAtgACTcgctgGTCcCTCCCcCAgctaACGaTTACcggCc +gcCtGcGGCactaAacAgTCtGtACctccgGAgGcattataCtAGTaaggatTCcCATtAaAcCcCtTAaCTTCccCaGa +CTCtggctAaTactctgCGAcatTTaGaaGcTttccaGGCTtcgCgCgcacgtcCTTAtCCgAacATTggttATTGtcaT +gtgagACagtgGGaGTaaAGtGccatcgCtGaTActcATcagcGCgTAGGtaacccGTtaGtaataaaAGgGGAcGcCAT +AcTtCTTGTTCCTcaGGTgCAgcCCgAACCGgctTcGtCAGGtTAGcCTaGgGcTGGggAtacTtGCGCaAgtcccGgat +AtgCcAtTgGaGtCAcccAaCtTcGTATCtaCgCCCTCccCgGcACAcAAatCaGTgAAGgGgcgTcCcAACcaGcatGC +ctCtTaTGTgATGacctGCggAAtGTGGACtTaGAtCgGgtCCggtCAgGtGtTCGCGaTAacTTAAaCcCgCCcttTcT +acgGAaCAAGgTaagaGctTAAaTGtcTGgAtGAaAGgacCGCGcgAcGAAgcTgCAcCgCgCAATACtCCTTCtaGgCc +AaGcGCGGGgcCtGTgtGcaTgACcAAaATgtGaGGTTGggcTcCTCgttTcaTccTACTagctctATGCcGcgATaGgc +GCaagtcGgTTtTgtgTCTtaTTGAGccCaaTGgAAtCTTGtCAtCtgtTAtcGcgGggactAGGTtCgCaggtTtaAaC +GCctccgcGAtgagCgtcCGTTCcATGAtaaGAgGCtAcTagaTaCGggTaGTCtCAaCGtACCaGttAgGcCTGtGGGC +gGACgtcttAtTCgTtTGTCGCtGaGaGTtcTGaGGGAgaGGgCtGcatatTAtGcGgcTgcgcaCaGGaTtTgaACGCt +tcGAtgCgTGTcGAgggccAcGTCTgCgtAaTaTGggTTgCATCtAAcGcAaCCaACtAgAgCCgcGtaGCtccatgAgc +tGTGataTtccacAACaCGAttCTGCAtTCGGAGgcgAAacACTgttTCTGcTtGTCcacGaATgGcTaGGcAtTaTTAg +aAGaCaTtatAGgACCcaCgcTcCagAcTtCctgaaTtTgcCtcCCcAcCAtcaaCaTAGgtAtCCCGtctTATggacTa +tgGcGTGgGatGTcTgAtaGGcaaTcgtGcCAGAtCCAtTtTCcgcCcGtaCcgCCAcAGgGCAgTTATCcctgctaTgT +ATtctgaGAcgaagCCCTGAtttgTTTCCacGAgGagAAACAtgCAggTGTcTaTcctCTCAATcaTCatattttCggCG +aGttgACTaGcTTTTTTTGAGgGGAcTATgCaaAaaggGcCcAAATTAGacAcgAtgGCtAatTatCCcTaGagTaGTac +GagAaGTtTGCgTactagaCTaCCGgTCcGACCCgAaTcGGctagAcGcGTtTTCtCcTaaGTgtGccgCcCTCtaaccg +CcGcGtgtgGCTggTgGTatAggCgaAtAgCTgTAGtCgcgggcaGCcCCAACtGtTTCCAaTCGaGagtaaCTGcaGAG +TGgTgAtTggtaCACgAttaCtgAtgGaTGCAgCAGATTCGaGTcTGGTctCgTgGcTgtacAGActCaGgCGACCatcg +aaaGgTAaAgTtCCCctGCAGgtCtgGGtTcCTgtaCtTGgcGGGcaTcttGCGCgGcgtTAGcCgCCCttGATCAGtcg +CtAtGtTaGGtGCgAggaGcacAagAagAgGTAATGtTTtcTGTtGCTCTAgaCCgAtGtcGgCtTCaCGaatTgCGgGt +gtCCtAacGTTCaGgaaaGgaGgagacacGCTTgctTTatGaATcGcCAgGtaTTCCgCgaATgCttAtGTttgatcCgT +GCCaccCACGcTaCGgcattTacgGGcGCcaCtTAgAaattgGcTCAcGTtcaaAcTACTTGCATtaCGATTTcGcAgaT +TAtAACtTTaagTtagtgAGaTTATATCAttGCTgTgACAatcgAtCttcGgAACgaacTAAcTtaGttCatTgAttggt +caTaaAGCAAcTGGcATGtAtcgACAAatCTcgTGAagcGcgAaGaAgAGgCaTcaaCTtAGatATCaCcCgaaGatGAt +caAagAtgTCccCTaTaAtGTAGCtAGCCtgagtGgtGtGATGgTttgcggTtcTGcAtgATCGTTCgTTaGTTtaAGCA +gACAGTTctAGtaaaCaatacTAATtccttagAtAtTCTGgCtagGgtctatgTagatgTagattGCCaattGAtTatga +AccCgTaaTCaCaCgAGgTtTCGTTaCgtgcAttgtcAcctCTaTttgctTaGttGgGtaAAgagGGGgtcGaatcTATg +TacatctaAcCGATTGgTgAGccTAgaTGgTAccAaaCCcCcAtAGTGGCtCTgcagACCgTtAGtTgTcTAttTattgg +GgCCtaTgtcATaAcAtAtacataGtGaTtGgttgCgtcAgCACgAcgATgCAtACtaGACAGtggAAcTacTGtaCaTA +TtCtaGTcgCGAgaacCCAGgaAgGAcaCTcTtTCaACGCgAgttCTaGgGTATTaTTccacAAtcAagtATAtAAaatt +acAgCAgCGAATaGtaAGCAAGcAtaTaTtTtTGGTGGtggCatTAgTAatcaGgcTGTGTAGgtatTTCgAgGCAggaa +GGGcCgctAgtCgAGTTActGTTcAAATCgAACTCttTCGGcGGCgcAtcTggtAaaaTgTacaAAcATggCaGGaCTcc +CTAGaTTtAGagaAcaaGGAGgAgggCtAaacgGcGgGttTctgACccTaACaCGTtCacgaTCCttaTgGCaTgCcCAc +atTACGtCGaCttGTCtacCTgAgATccaTtagCAATGGAAGCgtggagTATctaaGtgcCttatGtcttagTacGGGGC +GAACctaaCtcacgacTcgaaGCAAtaAAtCCcTAGGAtatCGTgaAacATactTgcCgggGtaTaGCAGggAaGAtaac +GGGGccGGaaAaggCTCggGTCGTCtGcGcCTgcTgtTaTAgTcCGaTgCTttGagggatGcCtCTCGagtAAtCgAacG +gTtTAGtA +>4 +GaAaTagATgGAATAAAgCTCtaCcctAGtatCgtCGAccAGgGtGAcTcCGTCATcTatGAGgATAgtTtGaTTcGGcG +gggAGGgtgcaGGTCTCCcaaTCaTaTCCgtgGCatTtcTTGCTTtaAAaTAgCcTACctGCgAaatCCgcGCAGCgctT +CcgGtCCaTtatAGGGttAGcCtAcAtgtCTGCGCtTccatAtGTCctgttCCttGgcggATcgcTAAACGCAGaAgACa +aGaTCtcCgActgaGTagccggTACtCtGgttgTcccTtcATcgcAGcgAcCacAaatAGGttcAacAGacCaTCccGaC +GCAcAGCtaCtaCCTgTACATAttgtTaATttCTcaaCtCCCGACGTtCcgaGCgtaCAggcctcgTACCaGgTCgCcCC +CAggCAgTCCtCTtcgGATaTccCggTgggAcTcAgGACTCTtgctcaaCActaAtcaaATTTAgctgGAgGCgTaTCcc +ataGaGTATtTATAcCacggAcgcGTTgtCcAcGtagacAatAcaATTcGAAaACATCCcTGCaAgGgattAtgcAGcCt +TgGtcgtaGTgTgCaGCcaAtacgCgttGacatgcTagTTTaGCaAAtGcgGccGggCtATAgAGaaCTGtagCGATtTc +gcTtTaGTgtcgaCgacACTCaCtcaTTaTcGCCGGGTTCgcagcCAcTtcAGtAgGTaCtATCCcaAAGACagGccccT +gGCaCgTcAccCGatCGTCtTTCTTCGGgCGgGCAtGcGGActCGaTCAtCcCctaaCATAAGTtgcCTTGggccGtTag +acAtGAcgCggGaGGATATgcggtCaAgaTggCtTGgCGaTGGgAagtcGATgctgGcACagGAcCTCcaTCAttCtgTT +CgaTAtATTCgagAAAAcagACTtGtAAtcGCTtccgTtcGGGCtgCcaagctgcTAgCaaCcCGcCaTaaCggGaAAaG +GTGgaggtTGtccaCaAacGAcaTtGaCCATCacTcGgGTttgagctcgGCGtCGcataTAccCgGACTCgTCACTTGGT +acCtTCGGcCGgcTggCAaGTattACGGaGCacggcATATATtGtACcggAcccCtAtGatatACaActtcgGTataGAg +GCtggTccGCTCAcTATcGagtagcGAgcCAAAgTcgCaGAgCcAGTctccCTTtTaAttTcACGtCggGcAaaAGTacC +cGgcTGGGcGTgCTgAACAtAAttGtcAgCgcctGTACgaCaTtGATTAAggGCgtTATTtatTaCTGggcTATtATGtA +CtAAtAAAggAgaaCagAcgTgCcCaAcgGTgttcGGgCaGgAAttActatAGaCaAgaAaCCcTCtAaAACGtgAgcGa +AaTccgtAGcaTACATaTAtTaCaAGGGctGGaaGCAaaccGcGgATTcaTGtGCacAtCCGtcGaCgGcGAGcatAtcG +ctctCCAAgATcgACacacCttggACgAgtaataATgTcAcGAAggGCGtgcTAaCTATGCAgTtgCgGTTaGgctATCT +gCAaTTTtGCcaacCCtgagGTaCAGgCcGaaCGgcCaAgGCCTtTcAGCGCTgGttCaAcGcaCTccGTCGCcaAACac +AGAAccTgATTggAtgaagGcGCtTgccAcCCCaaTtTtcgCaTttaGctacTtcCcGcaGTaTaCAaGTTggGtCCGgc +TgCGcgtcgtcTaaaGCatAaCTGaaaAcCAgtCAaacGaTtcCTGTtatacTGacAcgaaATTGctGActaAGCaaATc +cgaGCcagaAgGCtcagaCCCCgGaAacAacgTATTATTaAaGaAgAcGtTTcCAgCcTtGTttacCtTtTtATCcttGt +aCAcgGTgTAGgaCgTCTCaAACgTgTttCCgTCATtAcatCCcCcGCcTtggCCaCttCATtGatAtGgAcTAAAAcCC +AAagcaAAtgTGCGttGttCggggtTagtACgAccCTtGTccggGGcgTtagCCCcGGAgAaatgACCagAgGaAtActA +CCgCCGcGatTacAAtGGgTctctTTcctGctcGaacCCCTtAgAcCgagtcaGtgTcccTCACtgCcgGaTTGcGtgGT +TgtGcaATcaCtcatAAgTCAcCcGaAggcAcAtTGTatccgCAGAgTTGAacGaTAAtaCcggTggcAAcaCTAaTctC +GctTAATGCatcgACcAgcCtagtTattgGAgCTGcgATATActAgCCtATagAGcCAGaCcccAgCAtACGGCGtgtac +CGGGGGgGcgatGaGtcaCGaaAGaGcTagTctctTGGcCTgTcaggttgtggTTctcCttAgTTCGAGAGtactaaCga +AAGaagtCCCccaccgGtgttccCTACcGcGTcTCttAGTGTTtAagactgCccACcgTTgagGGGCAAAttcTtgCagt +ATTAtgGtAgaGTtAtataaGaATTcttgCCtgTACcatccTtgacTatTagttGgTaTggtcGcTCCCCTaCAtaactT +tctATAcCgCgAAACTATTtACTCTATTcgCAGgTaaTGtcattAaAAaTAGCCgtacggGAgctccAGAGcTGcCGTat +TCatttgtcGTaaTTGTtaTAaAatcgGaaaCtctcacCgtaAgacGcTGcccGACTTtTCTgACAacaaAtAAaGgGCT +gcAGcGACAtcaCcCaGTCTCCCtTTaGtCcGTGGCGgtaAcGGCGAtCAAaCCAATttTGTgTgtTaCcATGATgaTTt +CTatGaaTgCCtgtGagcgccTAcaTAggGtGATTTgaAAaCGACcTcgtctGGAtGtCcGcTtCcGaCtGaAcGtttCA +ATGTgcTGgttttaAAGctaAcCAgtTtcCaaCgTttaGAAgCGTCccagTgtgaggTGCctCGcTgaaaACtTcAGCTA +TGtcgCtTTatCcCAAaGcaccaGtacAtTAaAaatgGCGATGggcCGtGCCTgCGTcTTTAGagtCaCaGcgaAgaACg +AaGAttaacgtagccgGgTtGTgCTcAtGggtGTgaCtCGcttcCGcActGcggCAgcCcAATctaAtCtCggtgCccga +ACcGCaAAGcTtCAacGTCcAAgaacgTgGtTTttaCCAcTCCCGCgaCcgGTggaTGcACAgTGGtacTcCCCTgaCGc +GgatcCtgtgaCgTataGagcGAACTgATGCTcATTaTctGtTTgTggCAaGGtCgAgtgATGGCGtGCCaTGAGCGGga +TcaaacGaTgGcTatcTATTTCaaAttGaTaTactACcCACcGagagcACgCAAGcTgcgctcgTGaaACtaaTTtgAtT +TcTGTgTtAGaaAAGgGCGtAACcAACGCCgaGCCTCcAcAtGaGtCaTtaGgGTaAAcTtaCAGGGaGtggActatCAT +CcaaatcAAgGAacgATTgcattATtgAcCGaggAtTtgtgGAGaACCTgttCtgACCgtgCcCCgGAGTtcTgAaaTaT +cGTgAcaGGttACaAGgcaAaCgaTaGctTTCtcttgCCCcaatAAcgAgcATttatCgctcActtGCGtcACAGTtagA +CCtAcTGaggGGAGgccGAgtacCgCACCcAacGTAGCtTaCcCtgtTTCGTcCgtaGggTCgaCtCCaTGgCCtCccGt +CtGtTaCgcGTCCGAGaTCcATAGGTAGgGccgAAgTGgcCGTtgTGgGAtgCgAgGCGgAGTTCcAGAggGCtAActca +GgTATaaCaGgCCgcAGgctTtCcaGgAacCGcAcGACCcGgaGcGCgCAGTaCGgaaGgtaTGaCTtagGActCGaTGT +TgTCAtaAGatGtCgggAATtcatgtGttccGcGCtCTTTaGtGGACCaATGAgCACTCtTTTAcCATTgGCTtgTtgCc +ctTttcGCccCctatCcggAcATaGGcaACcggcataagCctGccCtcgACGgTTCgaTGCTTAtGAcACacgTgCAtAa +gcTaTTAcccaggAtGTCGgaatGatTCAgAGACaTCAAgCATgCTGGCaGaCtGgGtcCagtAgcTtgtTGgcgtcgcA +agacaCgCgctGTTTttCCTAgctttGgtgCAtGggtaacGtCgCCgCAGtCgtctcGaTaaggctaTtTccGgAaagAC +CtAGGAGCaGCgagAcaTTAtctAgCCTcGTacGGTCtcCTATcGctGcCgCcCAtGtTGgaCgCTtAAacTCGAcCCCa +TAaAAGaagggagaTCaaTCgATTcgTgaTTcCTGCccAcTCggacaGataGCcGTtTTgAcaTGTTCacaGCgAGAgtA +CtCtgtATCtTTCAGCTGCtaaaAgTAcGcgGctgacccgTgcGAACcGcACAgtgAaCTgGGCCtaCctGcAAAATAGC +CgAATCCTAggtCcAtGCtaaagGcgCcTTtatTTcActcCCGTTgacAgcgTccTCGccGCgTGgacCgactCacaacC +GgAgAaAgGggAtGtAaCGgtCgcTcACcGccGgaccCCgCGgTgCTaaCGGTtGtTgatCgCAcTAGccTgGATtAaCT +GgTGAcGaaaTCacatgGAtGGACgCAaGgtaCtaaCaGCGcTGcGataGgaaacGCGGCgggCtGgAcTACccGgCaCg +gtAaGtCTgGcgCcCtgGCATACcGtActCGcaatcAgctGTTTGCgTcGCACGAGCgGTAcCgCaGAGTCTTtAgCTAg +tagAcGAaGcCTACGtCcaTgAcgGgcAgGgagAGAGTCATGAtaCcgcGcagtTCatcTaccAcgcCCgCacGgCgcaG +AAtAAagTtcGTGtGttCtGcGgcTgcAgTgggtaGgaGcaATGtatcgcaCcGcgCcgGAGCcCcTGGTACCTGtGAgT +GACgttgCCgcCcAgGaGgcACTcAGTATCgcAGaattAgtcAAtcagGCgGcCtACTccaTccGcCtGgTATacATAgg +catactCTACAGcggaAGGAgcctTGCGaGaGtAtcGTAaActTAtACagAAGTtcttTAGcCcgcCCTGacTgACtGcG +ATattGtAGagTaAATgCaAGgGTAgtaaGGAgacGgactTcgCcCtgggTAcaAgccATCgccGtctGCaTGaCCtCGg +CatTGttcActcCctaTGAaCaTtCaGacctGTCgaATtgcgTAgCcAGAaGatTCgTcCGttaacCAGAtcTAgActcg +CgtCCTcgAgGatccgttaACTCTcCATgagGCAcAcGcGgCGGcAGTcCgTccgtaTTTGtgAttgCCCCACcgtGGcC +GgaaTtcGatGttAccAcgccgcCATaTtGGCaTaCtAACGccccTGATCtAgtTAc +>5 +gatATGGGTgGgAGatCgGttaAATgtCaTTccctCgccaGagTAaGGtCtTcAcggAAggGTTtTGTCGttGTtAgAtg +tatGcAAaCcccgGAAgcacTCccGgTccTAaGcATcAAcgcgttgGatTcAttCaAtCtAtGTActTAttTACATtcAA +AgTGtatCgaACCagaagATttCGTATCCAatCccATcGaacAGAtctCcgaGgGtcacATgGaCACCTTcTaCTGATca +CaagtttAcTcCCAATcttgAGtcAtaAttCTaTcATACgcAaCAgGtcTCCgagTGacaACACggCatTgTAtACtaGg +TgCagccGcgTGGGCaTtAtCaaACgAacCtgCTaGCGgcGaGgCcCagttaACgTTaaTATggtAAcGCgcacAGCaGG +AcGtTttttgggggcTCtaTCGccGgTgTTGTCGgAAcAGaggcGcgTaTtctCAatAaaATaTTgcactatTaTgCaCC +CtACaAATCTtCagAATtcaggTtAGaGgAttcgatatCtaaatTTCTgccaTgAGaggAAcgaCgCcGcAaGATgCGct +TcATaGTggcCCcgtccGTaagcTATTTtGtAgCtTCgTCACCGGtAgcaTtgTaAGcCGaGtgTCATatGAcaaccCAC +TGcgATgGgCTaggtaGcCgAcTGagGcGCacAgAGAGTcaAatcGGGaTTcCGcCCAccgCcAGTTAcgaaatATtCCA +cttGGGCCAggCATCaaAgtGaCgGGccGGGCgAgtgAATGAtcagaAGcGacTCtCTccgcCtGgggAagTTgagcGaA +cgcAGGgATataaTcgAAatctatgaCAtGcaAGggtgtcGGaTcaTTAtcCAATgtCGtcTAACtGGatggCCCtAGaC +atCaAccCCgTCgtacgtgTgacGaGcattGaGCtTCTCgAtTgcGgcCGCTCgaTgCgCaacCGccGAggTcCtTcTga +gcTCGTTAtaTttGctgTAcCTaTcAaCggCTggaTGGggGAcggAGtTCAcaCTaCGCTTcccGtTggAaTttTcGAcG +gTtGaTgCtATGTttCGgGAaTcgggATtCGtCCAGagTAAcaAGCgACAGgTaACGaCgcAATtACagAccgCgaGTct +tcgGCATaaaacAcgAGcttCGgGcTaaaTtcagGCACAAGcGActACGaAaTAcTaTAgAAaAAgGTcgGcTtcgcagC +acTtcgGcTCCgAAAAAatTGacgGGgTaCtTAaGGACctcagtaccaTAgCTGTtACgAcCtcCCgCgCTATgCacGaT +ActACaTgTAgGCcCgGacCGcTgGGtTACcagtttgcATtTGGatGGgcATGacgGCgcGgAtcAcAtggaTtTtTcTT +aGGCcGACGcGtTaggaTGaGacCGTcGGAagTaCAAAtttccaTACctCgAtCGcGCCtTtATagCAAcaaaAAcTCgA +CacTTtGGCtGcCgccCctcCTgCctCtttGgAggGATACtTcCAaTgtaattgCagtGGGTaCtaTTGaaGaTTgTGgc +AtACCtCCcaCtgaTcAgaaGacctaGttttAaGAaTCGAtgacgaTGCTGtCGcgtcgAGTccACGcccAggtcGAgTC +tcttATaaAagTagtTCTcgGGaCACgCcaAccCgActTATgGaTGtTATTTcgCAgGattCTCCtAcaGaaGAAccGgc +AcgATagtCGTaTaaaCtaATtaaACaggATTaCtACccTccAaAACCACTctTGTTTCTcGGGActaagGcaGCgGTga +aCtATtTCTTtCAGTaCGggCCgcaAAtGATCCgTgAAAaCccAgtTcCagTTAAcgAGACCcggCtCgCCTaGtTTtAG +CGAcGGGTaAGtaAgATcGCgttATCtAacCtaAAaCgGTAAAGggttAATccgCcCtacagGTAaaCAACCTgcTgGTC +cGaACgtgcTcCCcAaTAaCCTcCacTgCtcTAcCCTcTtCTGcGtCCacaCtTGgaACGtTtGGaGatgTAGCAgaaCG +aAActGAcccttTcactggTgttgtAAGcCtGtTccCaTGaCgAAtCagctGgtTACCGgcAcGtaCGctCCTGaTAAgt +cTcTATatGACTaggaACagAggAtGaGACgCtgTCaATCAGATtacGaCcTCCgtaTGCgtTATaaTcCtACgaCcTaG +CgGtTTaaGctAtgtggaggtcAtcttGCaGCaACGATcGttTcTtCTtTctgaAGaccaAacGGaaACcCggcgaTAgT +AtCtATatgtaCaGgTttcgGACTCaAAAgGaGAgTttTTTAcgCCCgATgCcagaGTCTAAaaGtCTGCaCCcacGaCT +CCCTACttcAttgGAGAgCCTtAtgGAacgaGcATCCaTcGTgtTCtcCtCTTctcCcAAACCgCCagCGGttCcagtac +tcTagCTAcgTgtTgCAgCAAttGTaggAacGaCAAACtTgCagCacAcaATcTGTTccgGgtGcaaACTTGTGTgACCG +CGGCcAAtatTTCGgtGTaTaGGtcCCTgTGaaCaaGtAttgCGaTaaCaccGgttGATAaggggtGTCattagtTgTtG +aaAAGAcAgGGCGgTGGTtGcCATAgGgCcgTcttgtCTggaCCttGTAAtAAGGctaagGaagtTgGtcgAcAtctCCA +CAaGGcttTGaCatcaaAGGTtGcgaAgAcAcacCcTtcaCgcAtCgGgcTtGGgCcAcgCATtcCtGgTcAtaGtgGTG +GaGaCAGcATagcAttaACaACAACGcgggACTgCtaAAAAgaggAcCcActTCATagaAAgGCAcGGcaaAAtatCgaa +AtCCGTGtgCgcGCACgGAGATTGaAAcGCtTAgaGTTTACCtgaatTTAaaGGggaaTGcgGTtttcaCACGCggCtTA +gccCtTTcAAAacTTcggtgTAaGGatctAgTTcTaATCAagCTtGggCAtcgTcCCCAagGgAggcCTaCaCACCctaa +tTGCcTtaatTaGctACcTggAcaAtTGcTtagcgGcGGgctTcGcCGgttAAAcTTAAgGccCgCGaCtaTtcGAgttA +GCTcaAgacAaGaGCTGtgtaTCCggaccGaCAtCGcggcccACtAtcGCaTcCATtTGGaAttCaCcAccAcCCgatAT +TGcaGtgCAGaTaCGAGATattGgcGGTaGCcTTAcgCaCAaGGccgcCaCTcCGccaTgtGCCTCtgtcggCGtAgTAa +TAGAagGtCGcGACAcTgAccACcTGCctGatTaTGtaCGgACAGGGTgaCccCGGtCGaGtgCAccCGtTtCGCTtaAg +tCgGGttCTCcgGAtgcAttCTGagTaaAtgGgatCAgtccGgcTGtTCAgAaCATGttagtccTAACaccCGgAAactg +ATcTttCCtcAaggGacCgGcTaGtCGcGgGatAcCtaaTcgAACCccctgaACccAtgGGaACACCgtcATatgAccCG +cCtaTGcTcCagctcaCGAACgccTTcCAgctTtgCgTAAGTcttTaCtGtTtTgCGTGGAccgGAttTtGGcaTagCTa +tgGgcgTcGgAtAGgCCCaGAgcggCgggCggCgaCGCGAcGCAtaTGAaTtaGacGtGtACtccGaGaTGcTGtGcCGa +CtTACgCAcccgcTCCttaCTCtCAacacAgcCACctCTtatgTtaacgcTTGGtgtCccgtactCctgtTgtagAgCga +ggACccGCatgctGctgCaAtATTAgGgGTTaGgggctgaTtTTGcaaACGGGagtGTGCCTAtGgTatGgCgcTGtGTg +tgtagGGGtCGCGACTaaCgCCcGaCAaTAcTTtGccCgCctGtagGGCtggAaAgcGTCAtgagGaaGGTatTGCCAgA +AGCCTgcGcTggtgcGTTttGATCGgaAgGgAccctgAaTgtTTGttCGttacTgctaCCTTtACGCTcatTtttTTcCg +caTaCtTcGaAaacgatCtAAggtAGtaAgGcGTtAaTgtTTAGGCatGacgCgTCactCttgaGAacaaTCtTctAggC +gCAgTGggCCcTTgGTAcCGGgAgAATTaggtggAtaTgCgAGcgAatGaAtttcAGcAtCgTAcgGaCcccAcGtCGCa +CgTTtcctGGctgCtcCTTCAattaGAcTGGtcAcCgAaAgGACAaCacTgccTCTtttGgTcACaTcTgGcAAAgGttC +cTtAGCcacCtAATcgaAAtctCGTtcCACaAAGTTGatCTAgtaGGgtaacAcaCtTaTgAggAAGaagtAGCCacgTG +TgACTtggTatCCAgtgGCGaTgcGGggcCGGGtgCTGGCtTcgagtgtCaGaAgagaCCacGCtACGtCAcgAcGtcAa +AtTCGtTCgcCActtaggGAACTgGaCgTAAacGtCTcGgggTATtTCGGAAAATcgccaGaAtcCAacAaTATAgGaTA +TTCcGagCTTaTCCacTgtgGAgAgtTgGgAgGCCcTCacAcgTccATaATgATGACccGTCCGtGGgCCaCCtgtcgac +gctgGGAaCtAtatgGAAcGGGGaTTGatActtCgGCgctTAcGAGacAaATTtTTtGttaTtcCggGTTcGgTTtcTtg +gGtcATcgaGcTGctAgGatGCGtaTaCaTat diff --git a/t/data/test12.fa.fai b/t/data/test12.fa.fai new file mode 100644 index 0000000..93a5ab9 --- /dev/null +++ b/t/data/test12.fa.fai @@ -0,0 +1,5 @@ +1 8789 3 80 81 +2 7958 8905 80 81 +3 4808 16966 80 81 +4 5257 21838 80 81 +5 4592 27164 80 81 diff --git a/t/data/test1_1.fastq b/t/data/test1_1.fastq new file mode 100644 index 0000000..d8e3b08 --- /dev/null +++ b/t/data/test1_1.fastq @@ -0,0 +1,400 @@ +@HS3_9090:7:1008:11310:354/1 +TGGCGGAGGCTTTTTTTTTTTTTTTACATACAACTGTCGTCATAATATGCGCGGCCTTGTCATCGCGGTGCCCCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1438:2003:100013/1 +CAGGGCGCNNTTTTTTTTTTTTTTAGTCGCAACCCGGTGTGCGTGCGACTAGTTGTGCCACGTATGGGTCCCAGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1271:1905:86956/1 +GAGACATTGGAAAAAAAAAAAAAACTGGAGAGACACATCCCGAGCGGGCTAATCCCCGATACTGCCGCGGTTAGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1305:791:83162/1 +AGATCGAGGCTTTTTTTTTTTTTTGGTGCCCGTCGTGCCAAACCGCGTCATTGAAAGTAAGTGGATATCGAGTGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2746:14451:329/1 +TAGGTAGAAGTTTTTTTTTTTTTTCGAGAGAGTCATACCCGTGTGAAACCGATAGACTAATGAATTGATAAAATC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2708:18759:118692/1 +GTGGGAGAAGTTTTTTTTTTTTTTGTCTGGCACTACACGAAAGTGAAAGCAACTTCTTCGAGAAGATCATTATAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1588:3468:141624/1 +GCATCAGAAGTTTTTTTTTTTTTTAGTGATATCGACCCTTACGATCCCCAAGATCATGATCCCGCTTTGCAATGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2921:15856:57939/1 +TCCTGAGAAGTTTTTTTTTTTTTTCCGCGTTAATGCCCAACTACATGCTACAAGGCTACATCTAAAATATGCAAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1417:15923:95590/1 +GCATGAGATGTTTTTTTTTTTTTTTGACCTTTTGAATTAGAGGAATATATGCTCGCGACGTCCAGTGACGCACGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2102:10514:9268/1 +GCAACAGTAGTTTTTTTTTTTTTTGGGTAAGCTGCATGGCAACGGTCCGTTCTGGAACCACCTCGTTGGGTTACG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:54:3501:193579/1 +GACCAGCCTCAAAAAAAAAAAAAACGGTGCGCTGTGTCTGTTGCTGAGGTTACGCTGTACGCGGCAGTCCTCGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2888:15166:71020/1 +TTCTCTGAAGTTTTTTTTTTTTTTGTTAAATGGGGACACCCGGAAGTCCCTCACCATTCTTGGACATATAGTCGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:782:4801:172498/1 +TCAGCGAGGCTTTTTTTTTTTTTTAATTGCGGTGAGGGCTCCAGATTTTGTATTCGGGACAAGAAACCATGTGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1840:12105:187126/1 +GTTGTCGTNNTTTTTTTTTTTTTTCAATGTGAAATATAGATTTGCTAACAAACTCTTCTGTTAACGTTTGGGTGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2153:1390:90531/1 +GCCGGTTGNNTTTTTTTTTTTTTTGAAGTGTCCGCTCCGCCTAAGCGGTTACTGGTACCTCTAGGCCGTCCCGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2362:6530:45744/1 +AGGTCAGAAGTTTTTTTTTTTTTTCAGTAACCAAGCCTCGAGGAGTCGAACTAGATCGGCTAGGGCATTCGTCAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:992:19260:109704/1 +AGCTGAGAAGTTTTTTTTTTTTTTTCGGCAATAAATCGGAGAGACTCATTTCTGAATTATTTTCGGTTTTAGTCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:70:16099:51055/1 +CTAGACTGNNTTTTTTTTTTTTTTTGAGCAAAGCGTATACAGTTAACAGTCAGCATAGCATAGAGGATCTTTCAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1603:19029:119019/1 +ATGAAATTGAAAAAAAAAAAAAAAGCTCCGAAGAGTTTTTCATGCGCAGAGGGCTATCTGCTGAGTATGTCCTGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1872:3885:17311/1 +GATGACACNNTTTTTTTTTTTTTTTCCGGTCTTCGGGAGGTACAAAGCAGACTCTATATAACCTTGGCAGTCGGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1547:15205:108796/1 +AGCTCACAAGTTTTTTTTTTTTTTCATGTGATTTTCTCTGACGGCCAGTGACACGGACTGAGGGGGCCACGTACC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1307:9506:116191/1 +ATGAACAGNNTTTTTTTTTTTTTTTAGGGCCATCGGCGAGACCACGAAGGGTTCCTCACACCCTTGGAGCTATTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1293:13399:104992/1 +TGCTCGAGGCTTTTTTTTTTTTTTAGCCTATCCGTTAGAGTACACGATGACTTGTGCTTTGAGAGCAGCGTTCAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2245:4790:19502/1 +TGAATCTANNTTTTTTTTTTTTTTCGACACCCCGAGGCGTTAGAGCTTTTGTTAGCAACCCGCCGTCGATCGGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1630:16736:9274/1 +AGGCGAGAAGTTTTTTTTTTTTTTCACGGTTTGAAGAGTGCGCAATCGCGGTATTGTTCAGTAAGAGAGTCGCAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1548:4346:62294/1 +CCAACCAGGCTTTTTTTTTTTTTTGGTCACTCTTGTTTAATGACCCGCACCCCAATACAGTTCATGCCGACATAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:32:14349:156816/1 +CTGAGGAGGGTTTTTTTTTTTTTTGACGAAAATCCGCTCCAGTCAAGTCGCCCCCATTCCGTCATCTCACTGTGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1322:16932:51209/1 +ATCCTAGAACTTTTTTTTTTTTTTTATTTAATTTCCGATAGGCCAAGATAGAACCGATTCGATCAAGGCCCGCTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2657:15332:141945/1 +CCTCCGAGGGTTTTTTTTTTTTTTTAATGAAGATCTAGTTGGGCCGCAAGTAGGTATAACCTCCTTGCGGATCAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:681:17025:20507/1 +GTCCGCTGTTAAAAAAAAAAAAAAGTCCCTCTTGTTACGTAATGACAATCTGGGAATGAGCGATGAGGGCGTGAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1904:5984:48273/1 +GAGTGAGAAGAAAAAAAAAAAAAATGCCATAACCCGGAGTCGGGCACTGGCGCTCACGGCTTCAACGCTCAGAAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2002:6401:148580/1 +CACTTAGANNTTTTTTTTTTTTTTCACAAAATTCGTCGCTCGACTGCCTTATCTAGATAGGATCCCAAGGAGCTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2051:6388:64396/1 +AGTATGTGGCTTTTTTTTTTTTTTGCTTAAAGCTTTGCCAGAAGACTGTGTTTAGCGTTAAGCGACTGCGTAAAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:333:16095:70445/1 +TGTTCAGANNTTTTTTTTTTTTTTAATTACTGCCGGCTAAACCCTATTGAGCGTGATACTCGGGTTATAACGCAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:235:4347:163096/1 +CGTAGCTCNNTTTTTTTTTTTTTTCTCTAATTGTCTGAGAGCATTAAAGCCGAGCTGTTACTTGGTCGCTAGCCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2459:19347:74593/1 +ACCCGGCCNNTTTTTTTTTTTTTTATATGTAGCACTACCCTGCCTGGAGAGCTACAAAAGATGGTGTTCGGAGAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:810:16180:96914/1 +ACCAGGTATAAAAAAAAAAAAAAATTCAATAGGAGGTACACACCAGCGTCACTAATGTATCAATTTGGGGCGCCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1634:5631:85585/1 +CATGTAGAAGTTTTTTTTTTTTTTGTCCCCAATCCTGACAGAGCCGTATGCTTCGTGATCCGGCTTTGGGAGAGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2962:11381:21217/1 +ATGTTGAGCCTTTTTTTTTTTTTTGATCTCCAGGGCCAAACACTGTTACCCAAACGTCACAGAGTCTCTTGAAAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2633:13732:192447/1 +TCGGTGCTNNTTTTTTTTTTTTTTCAATTTACATATAACTGTCGATTGAGCCTCTAAGAAACTCTAATGGAGTGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2087:17402:125788/1 +AGTCGGAGGCTTTTTTTTTTTTTTCGGTCGCCAGCGCTGCTGGTGTTATCCGAGGGTCTGCAGTTACCTTTCTGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:781:9732:169661/1 +TACTCAGAAGTTTTTTTTTTTTTTTACGTCATCACCCGTAGGCGTATACTGACAATGGCCACAAGCAACCCGGGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1662:16108:137898/1 +GAGCTGAGGGTTTTTTTTTTTTTTCCTATACTTCGGACTATGTTTACACCTCATTATCAGGGACACCGTTCGTGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:919:19941:114306/1 +CACAAAATNNTTTTTTTTTTTTTTGGAGCAAAAGCAACGGGTCGTGAACTGTACGATTCTTTTTTATGTGATGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2754:8750:58584/1 +TCAAGTACNNTTTTTTTTTTTTTTACGTATCCGCAAGTCTTCTCTATGGATACGTGCCGGGCCTGATTACCTCGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:336:1521:165232/1 +CTTTGGAGGCTTTTTTTTTTTTTTCGAAACGTGCCTGGCTCGTTCATCCCATACAGGAACACACGTCTACTATCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1355:15756:164948/1 +TATTCTAANNTTTTTTTTTTTTTTCACGCTCACGTCTTAGTTATTTGTTCGCGCCGCGTCTATAACCAACGGATG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2231:4273:60186/1 +GTCCTAGAACTTTTTTTTTTTTTTCGCAATTACCATTCCCATACATGACGAGCATAGCAAACGCCTCAGATTGCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2316:6407:47755/1 +CAAGTGTGGCAAAAAAAAAAAAAAAACGGGGGGGGGGGCTGACTTCTTTAAACTTACCGGGGTACACCAGCCCGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1180:9151:105392/1 +ACTTGGAGGCTTTTTTTTTTTTTTTTGACTATGCCCTGCTCCTTACCTTGCCATTTTCTCGTACTCGCGATCATT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:316:19348:39980/1 +ATGGATAGNNTTTTTTTTTTTTTTACCGACCACATTTAGGACCTCGCTATGTCCCTAGCTTGCGATATTGCCTAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2293:5852:41801/1 +CTCCGGTGNNTTTTTTTTTTTTTTTGCGCATTGCCAAGGGAGCCATGCAAATGGGCCAGGAAAGACGCTGGTACC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:194:2629:116686/1 +GACGCCGTNNTTTTTTTTTTTTTTTATATTATCCGCCAAAACGAGGCATCCACGAAGAACGTCGCATGTATGCAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:504:19694:38065/1 +CGATGGAGGCAAAAAAAAAAAAAATTCTACGGGGGACTATCGGTCAGTATATATACATGGGTCGATCCTGGGCGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:40:16690:103457/1 +ACGTCAGTAGTTTTTTTTTTTTTTACTCGCGCCGTAGAATATACGCTTAAACGTCCGATTGCAATTAATGTACAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2665:14543:75970/1 +TTACAAGAAGTTTTTTTTTTTTTTACGACGAACTATGATTAGAATCTGGCTAGGGAGATGATATTCATCCGGCCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:238:8472:192625/1 +AATACAGAAGTTTTTTTTTTTTTTTGGCCTGAATCACAGCGTTGCGTAGTGTTGCCTAACCTCTTATGACAGGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:457:4583:118080/1 +TATCTCGANNTTTTTTTTTTTTTTCTATATCAATAGCCAATTGCTCTGTCGAGGCCTTCCGTTAGCATCAGTGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2124:7309:107717/1 +TTGGTTGTNNTTTTTTTTTTTTTTCTTCATATAACTTAAGCCTATCGTGCCCGATATCGGTATGATAGCTCCCGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2793:15293:1228/1 +ACTCCGAGGGTTTTTTTTTTTTTTGCTGCTTTTCGGTAACGATCAACCTGCACGTGAAAATATATCGGTTGGAAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1021:19152:59391/1 +CTCGTAGATGTTTTTTTTTTTTTTACAGTGTTATAGACTGTAGTTTATCTTGTACCCGGATAGCCTTATGGGCGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1834:14761:60846/1 +ACCACGAGGCTTTTTTTTTTTTTTGAAGACCCCAGCACGGGTCCGTAGGGCTTGTAGCAACAGTCGCAGTGGGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1085:18337:179806/1 +GGGAGGAGGGTTTTTTTTTTTTTTCAGCTAGTAGGCGGCGGACACTCCCGTTTTGAACCTTTTTGAACTACTGTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:224:19111:133561/1 +GACCGTCGNNTTTTTTTTTTTTTTAGATTGAGCCCACTAACCCTAGACTCGCACTATTAACTCCGGGGCATATGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2983:826:9193/1 +CTTTCAGAAGAAAAAAAAAAAAAATTACGAAAGGACAGGCAGTAATCATGGTCCTGAGTGTCCCTTTGTATACAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:338:13920:17267/1 +ATGGAAGAAGTTTTTTTTTTTTTTCTGTACCTCATCTCACTTGACTGGAAACGCACCATGGCTTAAGTCGCTGAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1364:13563:167748/1 +CAGGGAGAAGTTTTTTTTTTTTTTAATTGATGCGTAGGGCTCGAAGTCTGCACGTAGAGGAGGTGCATTTTCAAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:607:13103:139333/1 +TAGCTCCTNNTTTTTTTTTTTTTTCCCAAGATCATTGGATCCTTGTGTCCTGCGGTCAAGTACCGGGATCAGAAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1747:1408:125904/1 +CTTGTGAGGCTTTTTTTTTTTTTTACGCTAACACATCAAATAGCGACCGGTCTAGAGGGAGACCGCACTATAGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1148:16465:13804/1 +GCCGAAAANNTTTTTTTTTTTTTTATACGAATCGCGTTCGGGATAACTATGTTCGTCCTCCCAGCTAAGGTGCCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1176:3250:145877/1 +ATATGAGCNNTTTTTTTTTTTTTTTATTCTGGCGCTATATGCGATTTAGAGCACAGGTACGAAGGCAATCTAACC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2792:6902:44593/1 +ATCCGGAGGGTTTTTTTTTTTTTTCACTGAGTCGTTAACGGGTTACTAATCTTCCTGTCTTGCTCAACTAAGGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2857:19869:180991/1 +CCCTCGAGGGTTTTTTTTTTTTTTTTCCATAGCGGGGGAAGCCATCCGAACGCAACTGGACATTACTATACCGTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:755:19931:178280/1 +TGGCCGATNNTTTTTTTTTTTTTTCAACATGTGGGGTCCAACATACACACCGAGCAAGATTAGTTCCAACTGCTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2480:155:63389/1 +TAAACGCGNNTTTTTTTTTTTTTTGTGATCCCTGTTCGGCGTGTCGCTCGCCTCACTACTAGCCAGGTGTGACCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2843:10575:3131/1 +ACCCTGAGGCTTTTTTTTTTTTTTGTACTCGTGTTGACGGATTTCAGTCGAGGCAAGCACAACGGATTGATATTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1770:4081:197491/1 +GTTACAATNNTTTTTTTTTTTTTTACGTAACAGATACCATCTTCTTGCACTCGCTGGATCATGAATCTGCGCGGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2062:3642:13147/1 +TAATTTCCGTAAAAAAAAAAAAAATAACGGGTCAGGAAGCTGAGTCGTCAAGGGTTCACCTCTAGAGCCCACAGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:193:6561:99678/1 +CGATTGAGGCTTTTTTTTTTTTTTTACACCATGTCGTCAACCCAAGCTAGACCTTCCTACAGCAGGTCCGGACGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2562:3408:19709/1 +TATGTCAGGCAAAAAAAAAAAAAAAGTATGGGGAGCGGCGCGGTTTGCGCCGTATCGGTTCATTTGCCGATTTGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:97:6403:33976/1 +GAAAGGAGGCTTTTTTTTTTTTTTTCGCCTTTTTCGTCTTGTTGCCCGAGGTTAGATATGTTAACAGGTCTATGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1693:9857:189526/1 +ATTGGAGAAGTTTTTTTTTTTTTTGACCAGTGACACGTACCAGCTGTTGCCTGGTATGGGTCCCGAAGTTGCGGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:723:3632:121816/1 +CGCACATCNNTTTTTTTTTTTTTTCTATAAAGCCCTAAGATCAAGGGTTATATGACGATGCCGACAAGCTCATAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:590:19844:171664/1 +CGGATTCTNNTTTTTTTTTTTTTTCCTACCTTACAGTCGGCCCAAGGACAATCGATTCTGATGCAAGTCCATGGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1274:4201:171834/1 +AGTGCTTCNNTTTTTTTTTTTTTTGGCCTATAATATATCTGTGGGATGCGCTAATATCCCTAACGCGGGGTTTGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2299:4436:81404/1 +CTGAACGTNNTTTTTTTTTTTTTTCGTCAATCCAACCATGTCGAAGGGTTCTGCTACACTCAACTATGCAGGCTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2962:13933:163719/1 +CGGACTGTNNTTTTTTTTTTTTTTGTTGATGCGTCGGCTCGGACCCTCCACGCGCCCTGGATCACCACAGGACAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:79:10717:191559/1 +AATGCACTNNTTTTTTTTTTTTTTACCTTCAATGCGGTAAGCCCTCCGGCAGATTGCAAATCTGTTAGTCTAGTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:728:6848:44913/1 +AGCCTGAGGCAAAAAAAAAAAAAACTGTATACACTCCACAGAACCCTCCACGAAGTTTCAGATTTAACGAGGAGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1805:19028:29375/1 +TGCATAGATGTTTTTTTTTTTTTTAAACCTCCGTTCGCGGAGTATCAGATGATGGTAAAACCGGTCAAAGTAGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2525:11691:147816/1 +CTTCGGAGGCAAAAAAAAAAAAAAGAGTTTCGAGTATGCTGATAGCATCCCCGCAGCACGGTCAGCTAATGTTTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1456:3792:55151/1 +CTGTTACCNNTTTTTTTTTTTTTTATAACAGCCAAGTAGTGCATTAAGAGGATAGGCCAAACCAGCCACATACCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1759:15822:177463/1 +AAGATGAGCCTTTTTTTTTTTTTTACGCTGAACAGACTAGAATAGGGCCAAATCTGGCGAACTTGCTATGAATCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1434:12716:156687/1 +AAGCGAGANNTTTTTTTTTTTTTTCGACTACAAACCCCCATACGAATAGATTGATATGGATCCACCGGGGACTGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:79:14794:163993/1 +TATTAAGAAGTTTTTTTTTTTTTTGCATTTGCCAATTGACCACGATCGCGCACGCGTGGCTTTCGAGATGGCTCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:313:3610:41049/1 +GTTTAATCNNTTTTTTTTTTTTTTAACGGCCCTAGCCTCGAATTGCCCGGACCGAAAAACCGTCACTCAAATCCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:766:16681:39314/1 +TCTCCGAGGCTTTTTTTTTTTTTTTCGCACACAGACTTTTAAAAAGGCCATCCGAATCCCGATAGTTAGCGTAAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:494:9805:67437/1 +TCTTTGATNNTTTTTTTTTTTTTTAATGTGCCGGTTCCATCCGGGCTACGGTCTTTCAAGCTTGGACACCGTATC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:638:6498:188736/1 +TGGACGAGGGTTTTTTTTTTTTTTGTTCTCAAGACTTGGTTAGCGTTAACTTTCTGTAAAGGGGTTGACGCAAGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:350:3180:176176/1 +GGCTAGCANNTTTTTTTTTTTTTTACTCGCTCTATCGCCCATTTTGTCCCGTCCACTTACCGGGCTCGTTCGTTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/t/data/test1_2.fastq b/t/data/test1_2.fastq new file mode 100644 index 0000000..afa3c2d --- /dev/null +++ b/t/data/test1_2.fastq @@ -0,0 +1,400 @@ +@HS3_9090:7:1008:11310:354/2 +AGCTCCGCCCTACACGTATGCTCTGAGTGTGTCTGACTCCTGTCTCAAAATTCATGCGTAGTCTGGGCCTCTAAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1438:2003:100013/2 +GACTCTGTCCTGTCGTTGCACACATATGGCACCGGGATATATGGGGCCATTGCTTTTCATCCTGGCATAACCGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1271:1905:86956/2 +TAGATGCATCGTCTGGTTCTCAAAGCACAAGACATGTAGAAGATAATCGGCTTCCCTGCTACAAAGACACTTGCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1305:791:83162/2 +CTCAGTTCACTTTTGCAGAATCCCGGTGTATTGTACGCGACAGTAAGGGACACGTCGACGCTCAAGGCTCTATAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2746:14451:329/2 +ACACCCATCTACAGCCTCCGAGACTCTCCCGAGTAACAACAATGTCCATTTCATGCAACCCGACTGTAGCGGAGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2708:18759:118692/2 +GAGAGACTGTTGCAGATTATGCTGTCGGGTGTGAGGGTAAACCTTGCTCTCTTTCTTAGGCGTTCACCCCATGGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1588:3468:141624/2 +TTCTAGCAGTGTACTAATGCGAGTGAGCTAAAACGCAGCATCGTTGAGCGGGTCATTTATTACACTATATCCGTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2921:15856:57939/2 +ATTCTCACCGTGGCACGGATCCGAAGCTGCAAGCAGTTAACACATGATATCCGCACGTAGAAAACGCGTCCGATT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1417:15923:95590/2 +GAAGGGCCAGTCCAACGTCGCTAACGAGATGAACTTAATGCCGTGCGAGATTCTGCGGGGGCATCAGGGGTGCGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2102:10514:9268/2 +TACTCAATCCAAATATTTCAATCCCCTCTGTTCAGACGAGATTAAGCCAGTACACATTACTCCAGACCCGGTCAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:54:3501:193579/2 +TCAGGCTGTGCATTATTGCGTTGCGAAAAGACCATGATTGAAGAGCTCGTTTCAGCGTCCGGTGCTAACCGCATC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2888:15166:71020/2 +GAAAAAATATGTCTCAGTTTATCTTCCGTACTCAGTCCCTTGTAGATGATCGCATCTCGTCGCACCTGGGGTGGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:782:4801:172498/2 +CATTATCCTGGGTGTAAGCGGCCCCGCGGCAAGTTGCTTGGAGTCAGTGGAGAGCATCCAGTAGTAGGTAACGAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1840:12105:187126/2 +ACGCAAGCCTCACCTATTACCCTGCGGATCTCGCGATCACTATGCTGCGGCTACTCAAGTCTGGGCACAAGACTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2153:1390:90531/2 +AAATTCAGCTTAGGACAACTTATGCGCCGGTTTCCTCACCGGGGTTTATCATGCGGCTGCGCGTACCGGCCTAAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2362:6530:45744/2 +CTGCACGTGTTGAAAGGTAGCGGTGGACCGCATGACCAATGGCTATGCTAACAATCGTTAAAAGTCGCAACACAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:992:19260:109704/2 +GCGCATCAATAACCCTCTATAGAGTTTGAAGTCTGCGAGGGTGCCAGGGGCTTCAGCTCGACGAAAAGGGAAGAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:70:16099:51055/2 +TGTGCAGTAAGCGACGTAAGCAGCTCGAGTCTCCCGGATTCAAGCGTACGGACTTGACTAGATGCGTACGCATGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1603:19029:119019/2 +AGGTCGTAGAGTTTCCTTAGCTTGTAAGGTAGTACATAACCAGCTGGTGCGTGCAGTATGATGCTATACCCTTAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1872:3885:17311/2 +TGCGCATACCTGGCACAGCCGACTTAGTTACTATACGTCCGTAGTGCCCTATGTCATGCTCACGACAAAGTTGCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1547:15205:108796/2 +AAGGTAAAGATTGTATTTACCACCGGCGCATGCAATTCTAGAAGCACTGACACGAACGCTCCTCAGCTGACGGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1307:9506:116191/2 +CATTTTATTGATCTAAAGGCCATCTGCACCACACAAACGAGGTATTCCGCTCGAACTGGCCCCCTGAATAGCGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1293:13399:104992/2 +CGTAGCCCAACCTTGGCTTGCTAAAAGCTACTTGGTGGACCAGTCTTGGACGTGTACACTCGTGATACCAATCCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2245:4790:19502/2 +CTTTAGGAAACGTTAATAAGACGGCGCATCGAGATGTACACCACCCCTGCTGTTCGTACTCACTAGCGACAAGGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1630:16736:9274/2 +CGCATCGACGAACCAACGTCCGCTGTTATGAATCATTATCTTCTAGATAACGAGCCTTAACTCAGAGGTAATAGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1548:4346:62294/2 +GCATATATTTGCCTACCGAGTCTGTAAAAATTGTCCGTGATGTAACCGACACCTTCATCCCAGTTCCGGATAGCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:32:14349:156816/2 +AAGGGCCTAAGGAACTTCATCAGGTACGGTGCTACGGTCACAACGTTTTGTTATGTGACCCTTATTGGGATGGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1322:16932:51209/2 +GAGCCTGGATTGACCAGCCACTATACAAATATAGGAAAGTTAGAGATGGTAATCGCCGAAACATGAACCGCCCAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2657:15332:141945/2 +GCTAGCATGAACCACTGGTGGCTGTTAGAGCTTTCGCAGTTTGGGGAGCCCTGACTGGGTGGGTCTAGAGCTTAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:681:17025:20507/2 +AGCAATAGCAAGATAACTGATTACCCAGCGCGCCATTGCGGGCGAAGTGCTTAAGTTCGGCAAATACCGAATACG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1904:5984:48273/2 +CAAGTGTTTATGGGCATCACTTCGACGACGCTTTAAGGTAGAATGTATTTAGCACATAATACACCTGCTTTGGCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2002:6401:148580/2 +ACACCGACGAAAGTCGGCAATGTCTGCATACTCCTGTTGTTCGGAGCTTGTAAGAGCGTTGCATTCTCCCTGCAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2051:6388:64396/2 +CCACTTAGCAAAAGCTGTGCAAATGCATCTACTCTTTAAGTACAATGAGCGTATGTTGGTAATAACCGCGGCCGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:333:16095:70445/2 +ACTTTGACTATCACTAAAGTTGGCGCTATCTGATAGTCCATAATAACCGTGGTTCTGAAAGGGACAATATGGCAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:235:4347:163096/2 +CGGGCCTAGGTCGGGAGCCACCTGCATGGGTCTATCCTAATCTCCAACTCAGCCGCGTGTTCACTCAGCGTATGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2459:19347:74593/2 +TCCTCGTCCGGCTGGCGGCTTCGTTCCTCTACCGCTGATGCAACCAGTGTGGGCCTCGTGACGGACTAAGTAATA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:810:16180:96914/2 +TAGGGACCCCACGAATGCTAGAAGGTCGAGGTAGTTAGTGATCTTTACTCGATATCTCTACCGGTCACCGTAAAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1634:5631:85585/2 +CCGCCTATTCGTTGCCCGGATCGTTAAAAAGACCGCTGAGCTGGCACGAATATTGGCAATTGACGTACCCGCGTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2962:11381:21217/2 +AACTAAACTCCCCTCGCGCGACAGCTAGACTTGAAGGCCTATGCTCATCTTTTAAACGAGAGCAGCCGGGTAACA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2633:13732:192447/2 +AACTGAGGGTCTTATCCAATAAGCTATTAAGGCTACACACCTGTTTTCTCTTTAGCAATTGGACTACGCTGAGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2087:17402:125788/2 +GAAGTCCATCGAAGATACCACAGCATCCCCGCAGTACAACCTTTCTCGCGGACAGGGCCTTACAATACCGTTCCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:781:9732:169661/2 +TAAAAGTGTAAGCGCACCTTTCGCGAGCGGGACCTTAGATTTAAGTTCGGTAATTGAGTCGACGCCACGAGGGAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1662:16108:137898/2 +GAGAAAGGGAAATTTGTGACTATTTATGATGATCCCCTGTCACAAATTCTAAGATGATGTGCTAGCCCCCTCTGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:919:19941:114306/2 +CGACTTGCTAGGGTCCATGGGTGTTCGTTATTGGTATTCTGAATTAGTCCCCTCTTTTTTAACATCCGAGTCCTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2754:8750:58584/2 +TCCTGGTAGCGACGGTTACACACGCAAAGCCTCACGGGTACTGTAATATTCTATTTGCTCCTTCTTACTGGAGAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:336:1521:165232/2 +GGGAGTATACGTAGTGCCGACAAGTGATCCGGATGGCTAAGATCCCCGACCCGTTTTGGTAGGTTCCGGGAAGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1355:15756:164948/2 +CGTGCGGCCTTATTGGGACTCGACGCCGTCACAGGATATATCGCGCTGAGCGTTTCATATATTCATCGGCTGTTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2231:4273:60186/2 +AGGGTCATGACTACCCGCACTGATATCAGTATGTATGGTATGCTCGCGGGCATCGGGGAGCCAGTTAAACACTGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2316:6407:47755/2 +CGTGTAGGCCCGACTCCATGTTTTGCATTGCTATTTCCCACGCCGTCCCACGTCTCTGCATAACTGGGAGGTATA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1180:9151:105392/2 +AAGTTGGAATGAACCTCGAGTTCAAGGAGTATCCCGCAGACTTTAGCCGTAAGGCAGACAGCGCAAACTAATCAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:316:19348:39980/2 +TTGTGCAGTAGCGGATGTATTGTAAGCTGGATGCGGCCGACGTCGTGACCCTCTTATTGAGCAGCTCCCACACGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2293:5852:41801/2 +CATGTTTAGCAAGACTTTCTCTCAGGGTGGAAGAACGGCGCCGATATATCAAACACAAATGCAAAGTGAAATAAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:194:2629:116686/2 +AGGGCGTACAACAGATGGTAACCACGGGGTTGACATAGACCCTGCACCTATGGATTATTCAGGGGATACGCTCAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:504:19694:38065/2 +GATGGGTTGTGCGGCATGGTATCGTGGACTAGTTGGACCAGATGTAAGCGTGTATCGCGACTGTAACCACTGTTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:40:16690:103457/2 +TCGATCACAGTGCATGCGTTCTATTCCTATGAACGAAAGCTGGACAAGAAACGCTATTCTTACAATTAATATCGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2665:14543:75970/2 +CAGACTGAAAGACACCCCGCTAACCCCCTGTGACGATTACGCTGCAGGTGTTGGATGGCTGTCACTCGCACCAGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:238:8472:192625/2 +TCTTTAGAGATCCCTGATTTTAAACGATACCCTGAGTACCGTAGTGAGCAGAGTATGTCAAGTCCGAGCCTCGTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:457:4583:118080/2 +GGGATCGCTCGCGCCGTTAAGGGGTATATAGGCCTCCGTGACTTGTACCGGGCTCGACTTGGCGCCTAAGTAGTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2124:7309:107717/2 +GACAGGGGTGACCTATTATCGCACGCAACTCTGGCAAGATAGCTATGTAACCCAATTCAGCGGCGATAGATCGGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2793:15293:1228/2 +CGTTTCCCATGCTCCCTTAAACGCCTCATCATGCTCGGCCATTTTTCCCGGGAGCTTCTTGGGTAGGTTCGATTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1021:19152:59391/2 +CCTGGGCCTAATCTATCGTAAGACCCGCGGTTCGCTCCACCGTATAACAGTCAGCAGGTCTAGTAGTAGCGATTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1834:14761:60846/2 +CATCAGCGGGATGTCTAACAAGCATCATATTCGTCATCTAGACTTATCATCGTACCGGCGATTGTATAGACCGTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1085:18337:179806/2 +ATCTAACTCTGATTATCACATATATTGGTGGCACGCAGAATTCGATGTCAGCGAGGCTAACACTCGGCAGCCAGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:224:19111:133561/2 +AAGGCCATGTATCCAACTCAATCGGCCTGCCATAGCAACTCTGTGTACAATGAAGAACCACCTCCATCGTGCCTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2983:826:9193/2 +CATACTGCCATGGACCCTAGAAGAACCAGGAGTGGGAGAAAGAGGTATCGGATTCCTGGGGGTAGTATCTATCAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:338:13920:17267/2 +TCGATGGGTGTTTTGGCTTCATAGATTATATTGGCGCCCTCAGAAATTATTGCAACGTCCGCGCCTGACGAGCTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1364:13563:167748/2 +TGGTGCTATGACGCGTTCACAAATGAAGCTCTAAGAGAACAGCAACACCACCCTGAATACGTGTACTCTGCCATA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:607:13103:139333/2 +AGCTTCGAACACGTACACGGTAAGAGTTGGGGTAGCGCGTCCACTGCGAACTGCCGGGTTAATCAGAGTGTGCGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1747:1408:125904/2 +AGAATAGCGCAGAGGGAGCGATCGTCGCGTGACGGGCTTAAACTTTAGGTTTGACTTTTTGTTCTCAGTCCAGCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1148:16465:13804/2 +GGATATACCCTGGTCGCTACGGTTGGTCCTGTACTACGACCACGACGGAGAGGTCGGTTGGCTTTCCCGACTGGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1176:3250:145877/2 +CTTTGTCTGTAATAGTACCAAGTTGAAAGCTGGGTACGTGGGCTGGGGCGCCGACACTCCCCTGACCATATTGGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2792:6902:44593/2 +ACTTGGTAATGCAACACTTGGGTGACCAACCCATCACCTCGATCAACTGCATGGGCTTGCAGACATGAGTCTGGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2857:19869:180991/2 +TCTGCTATCTTCCCCAGTAACACATCTTGAAGTATTCTGACGCGCGGAATGTGGCTGAAGGTTCCACCAAACTGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:755:19931:178280/2 +AAACTATAAATATCAAGCACCCTGGATATCAACGTATTCCGAGTGGCCCGAGCGCATTCCGTGCCTACCCGGGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2480:155:63389/2 +ATGGATGTCTACGAAAGAGCCTAGTAAAGTACTCGCCCGACAGGAAGCTTCACTTTTGTTAGTGGCATAGTGTCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2843:10575:3131/2 +TGACTAGCTTAGAGAAACCAGTGTAGCGGTACACTTCTGTCAGGGAGTTCAAGCCGGAATTATATTAAAAAGGTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1770:4081:197491/2 +CTCTTCGGAAAATTATAAATTTGCGAGCATATGTTTTGGCGCGTGTCTCCCATTACCATCGATAGGGAGTATGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2062:3642:13147/2 +TAAGCCGTGATGTCTCACTTAATTGGCTCAGCTGGCCCCACAAGTAAAGGCCTGGAAGTGTCATACACGAAACTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:193:6561:99678/2 +GTTAAGGAACTTAGGTGAGTATCATTCTTCCACTAGGGCAACAATTTACCATCCGCCCAAAACTCTAGGGTCGTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2562:3408:19709/2 +AAAGACGTCAGACTCTTCTCCCATTGCCGGACTCTCAATCCTCGACAATAACATACGAATCCCACGTATCACCAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:97:6403:33976/2 +TCAACCTAGAGCCTCGCACATTTTGTAGAATACTGAAGGGTTCGATCCGATGGCTGTCCTGATGAACGCTTATCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1693:9857:189526/2 +CACTCAGAACGTCTTTCCAGATAGTACAATGCGAAGGCCATTACCGTGGGGATTCGCAGGAGTTGAGAAAACCTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:723:3632:121816/2 +CCAGCATGCTCCTACCTCCAAAAGCCCTTTGTCTAGATCTACGAGTAGCGCGTTGAAGACGTTAATGCCCACAGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:590:19844:171664/2 +AGTTGGTACTATTGCGGATGAGGCCAGTCAATGGACATGTGTATGATCACACCACCGGATCAACCCGTACTTTCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1274:4201:171834/2 +CAAGCGCAACTCAATACTTTGCTAGGACTCTCTTGAGCTGAATGCGGGCTGTAAGTTGGTGAATAAGGCGCCGGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2299:4436:81404/2 +AAGAGGGATGTGAAACTTAGGACATGGGCATAGGATGCCAACTGTGGATGGGTTGTCATATGCGTAAAACCAACG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2962:13933:163719/2 +CGATCAACAAGTACAGGATAACTCGCTGATATCTTTAGCTCCGAAGCTAAGCAATGAAGTACTCACATTACTCTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:79:10717:191559/2 +CGGATCTCGTACATGTGTCGACCCATGAACTATCTGCCTGTAGTCCATTGCACTGAATATGCTATCTGGTAAGTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:728:6848:44913/2 +TCTGCTTGATTTCGCGTATGTTTCACGTAATCAGCTGAAAAGTATACGGGGCGAACTATTAGCTCCCCATCCGGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1805:19028:29375/2 +CGGCCGGCATCCGGAATTTGCTCGTAAATTTAAAAAAGATCGTTTTGTCGTATCATCATTCCTTAGGTGCCGTCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:2525:11691:147816/2 +AAAAATGTACACCCATTGACTGGTAGTGACGAGGTCAACGCACCATTATGCTATCTGCCCAGAGTCTTCTAGTTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1456:3792:55151/2 +TTCACCGGTACTGTTGTGCAACCGAATGGAGGACGGTCTTTTGGTCCGAAAGAAGAATATGTATACAAACACCGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1759:15822:177463/2 +TCAGCTGAAAAGAAACTAGTTAGGGGAGAGCGGCGTCGGCTCGGTGAGCACCCACAGCCGTAGTTTTACCAGTTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:1434:12716:156687/2 +GCCCCCTACCAGGTGGAGCAAAGCGCAGTTCCGGTTGTGATATACAGCGCCTTGGCTTAGCATTGACCGCAATTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:79:14794:163993/2 +TAGTACGATCCCGTACAATAGTTGGTGATGACTTACGATCATTAATAGTCCCGCGTGGGGGGTGATTGCTGAAAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:313:3610:41049/2 +CGGCAAGAATCAGTTTTTTTGTTTCTTGCACAAACTGCTCTGTTGGATCCTGTGGCCGGACGAACTTGTGTTTAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:766:16681:39314/2 +ATTACACCGTGTAAGTAGAGACTGGAGTCAAAGGTTGCGCGCGAGTCCACAGAACTTACTAAACCCTCACGTTAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:494:9805:67437/2 +CGGGTCGCCAAACAAGCGGGTATTCCGTCGCACTCGCCGTGGACTGCGCAATTTGGAAAATGGGGACGGCGACAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:638:6498:188736/2 +GAATTACCGTAAGTCGAGTACTGAGGAAGACAGCCGAGTGTCAGCCAGTGAACGCCGACTTGACTGTACAGCATA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS3_9090:7:350:3180:176176/2 +CACAGAAGCGTTTACACTTACTAGATTCACTACCGCGGAATCGTCCAATCTACCAGGAATCTATTTCACCGCAGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/t/data/test2.bam b/t/data/test2.bam new file mode 100644 index 0000000000000000000000000000000000000000..18aaa1bb19c59abe57d110970cdd477594ee5734 GIT binary patch literal 87517 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 z

M8*+>2QbAR^v2Y=!Er;k?O{Ct6G9*LuJs1SNn9#K-r zwP=QA0Uafq#KEc+8?}+zC`Af1tIGmP611c-(2R2!HTHm=V{Cb0Knurgxn>F0EsYjS z9AuhPkIMqeu?^~gS=HwMip6FI6=-1*jWtrXK{+chG|XjKYih0NvVc}q_Ad^i31lMm zw3opC>GQIHMovjaCCcPHO1ZQaK+xlC%L1Bou+)m7My+MU(m-*9+F)T3wIB^BG~i)@ zLqaSLvQ^ZDMKo$F9?}U_2PCo3Laml5(z1YNDw&1`;5>4Ph9Xf44s2yvKshwDm`5Q@ zKvpp46f`O5!U#X0_MVtqj$TTu;1nqsQ|${AB_U%URRdGb^EMjZ~QYtSjnZPJyN}z$ANt=$4`z%qsIC>(X5L)&^9}NB9mU6Djg=ASkwUSKv zB+#fpW(P~XrfOv@3usMUJGcfBYF^!m>Veh=H30H+Ggov62(f2u}Y? z0~#|13|W%agp|Tay-Sm@FwaELi-c6D^^*G3`KrjRHda?_+eL^X9LSeg}y(XBD31|uf- zbWTwX&`RiKVT2Em2-#QYh{jG%%2j)g1D2*oU5;=Ni(QB3f0e`v!{tfig#qQrVqBoB zme>bbs;MQn&bBa_H>|r$V!;r5sA{#!4vd#DItOhs=HBlp^{=>7Ety0 zxpXphwHKi~Mfsvts%2?=Pk|9KnWW00*KAE|RUKJjVLsq_?U=y5NF-3pf+oF|(So@j z9SZ}hOge&p{_ZxEBuW)^F-4sgCY%$uo*i1RBn~d6!ET?O1(v4pH6%a~H%Yn1Dnu=+ z=Bkhu_UYX$OJj}0s$ob{y^Mx2OqN#b-6)rAkXnY8#IO&ydG1X8^Dhf%FbJd60-jK* z6cK}_U9HN(%$;z;xq}Z= z70MXuP;AbmxG=(Z@rffyY}F&0m5^m_K4_$c)%uRW)Pq)XJtZ$m#^4g#$aG;_I_^Rg)W0~oST4DgrWkS< z(iZmV38IITI*-;HIWsw5saWnyyYfcvmU0Ew!JJ2o6>_kO2wNZD{PR0df7OA~1E9b7 zL#x$~f9Xx2M;Di8S4T&O>-E+8;{5XBfF0!P@`-y)v5WI(%np3(pZJl7NY`gq>q`&p`T6?v@ZkB)wa4@pXz{=DrzY6mAUfjg7BqWg z)x~Mg1bOQRBmEB8H(!4Lzq0zncYpTtX|?*j@Fv)cv(u}i^V3t0?eWFM<-yI(^?g+T z#|N|EYY~UVwUM+9uI?p>Z`}RBf8ikh*lomq{w0X{XMg>R4}?| z#liJ8KY#xG+JUUV|I&Z_VD5mYk$bjvvdj4)*fuY<=Wedd(B<%Y|=wsN@iv z>)g8OhKhsu@D{|k?tb7$Uw9F-*Ga89^Zx8757_tk?C{Dl9D5R8o}C|^9XO|5-@J&~ zt$Xb}w|-g;7$ijx7;x^~AUytd&+3hM{md8Mj@Pewtp3JF?}*pg;l

E-%pJz4GM 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 K0000000021lcsb4 literal 0 HcmV?d00001 diff --git a/t/data/test3.bam.bai b/t/data/test3.bam.bai new file mode 100644 index 0000000000000000000000000000000000000000..d98d42b80c12d118790d021c2444b9490c704efa GIT binary patch literal 416 zcmZ>A^kigZU|?VZVoxCk21X#wz#s&swy(1?0`a^iLFCcJqd}U0AQwWR=tVXUW)AaQ z$r_MSn0e^p^2`u}M3KycsX;amW{x%s(|2U^(8WJN&ErEd52gm$JeWB*!se)eJPvao ny7+ZS;4s7@nFmvYY#z)U28QSwWb@F)r+_p80kZoLY9Mj|4bm%H literal 0 HcmV?d00001 diff --git a/t/data/test3.fa b/t/data/test3.fa new file mode 100644 index 0000000..954edce --- /dev/null +++ b/t/data/test3.fa @@ -0,0 +1,360 @@ +>1 +cCaCcGAtaaGtaTgttaaccCgAgTACATTaCcGGGCGaatcaTccgAAGaAtTCTTgaAtaagCCcCggcgaCtctTG +GataggatCTtgctcgtCcaTatGCtaagCAGcAtgGcGTgGaATAgTTcGgTCAtTtTTTGccTCGgacCAGtTgctga +ACCaCAttgctgcTaGcgagaaCggCaGCtGAGTgCAAAGtaTTaaGgTCGTgCaGgtTTTactaaCACGCACctcCtCg +CGAGtgATCGAGCtcggcgCgAtcGtcaGcgctaAGgCtAGAAGggAaaGCacCAcTAaaaGAAgGCACacgCtCtcAtt +gCTacTGgcTtcGACtGGGgcAtAGCaAaTggTTtgggAgCGaTaGACGcccaAgCTTGgtGggATgGatCaCaTGgCTT +GAcaCtAacaCTcTTgAtAGAACggtggttGAgGtATcCaGGTCgGTtGATtCGctATgTGGaAtTggtgaTAGAtcaGg +gCAtggCcAATTctTAGCTTcgCCggcAACcTCAgcgCGaCACCAtATGtggTGatTCTCCGGCtAaTaAAAcCTaTGtT +CgAtACCGCtaGccgCCCacaTaActTaAcAaACcgCgACTTgACCCtaAAgGgtCtCGgTacAatAGggcAaTGGGaCA +gAcGGcGgGaCcAAaGTGtaTCCaTCAcgGgCATTCaCggGCgGatcTCCgTAGCCGcaaCAtTaTtTGgAGcGgGCCAC +ACaCgACctAgGaAACcAagCAcGtgtacAaTTATTGTAtAGTcctgAtAAaACgcCTgGAATcCtTtCCGTCaGTcTag +ATAcgcGaCAaCgTActgCAagaCTACtTAtTctaACGtTcTgACATatACaTcTTgCacatAGttAaATGGcgatatcG +tGAacacgGTtTtTTtcGccgAAaGACtccCGGGctCCCGgAatTACAGaaATgACCTgTGtcTAgCCaaCctgtCTTAc +gaAtTAGGctCacGGtCGTcaCGcGGtggGACaggTAgtCGcAcCtCTTcGgAAcGgActCcATGGcTgatcacAcgcaa +gtgTGGaggtGaTCGGtCtaatTCgtGgGattCgcCAcattaggttTatcTCtaGCcgCctgTaTttatcttCtgcCCGg +GccCccAaggTctCCgAtcccgTTTcTTACGgatCTACcGAattGttTtcAacGGaaactCtaCGgCTTTgcGCaGacgC +gAtctGgAAagctcCCtACcGCtTtaGgaaAAtGAGCAgAAgAtttgCTTGCGgcGGgcCggTGAGAgaGCCGtGaGCTC +CTTCAAtTGtAgTcaaTtgCGcActAGAcctGGaCagtAGacCcgcACTAtcgAtCTtaggtcGGcCgaAGttCgCaTgg +tAGGGATtgTCtGGTCcAataTtacatgGataCCgcGTaacgGCaCCtTCGTtgtGgaagCAAgCTCGCcgCGgCCtTCA +CgggcCtCctGGaTCcCCaACTTtgaTAAcTACGgcACCAgActgaTATcGCtaaCCctTCaGTTTTAAggGaTctTAca +gATtAgTTTGtCggTccCTTctCaTtACAtaTTTCCAgAaAggtattctAAccCCatggcTATCAGAAgggAGGGtTatT +ccTtgcGtggAaatcaGCAaTaGcAtATtAATGTtAgaAgTcTCCTAGaGgCTagGTccgtGgaTtgAGCGAagacgtTT +GgCaCTGGtctTGTATaagTgTagTgcTAGcgaAtTcTtGctCtaaACggtGgGaCgtCTTCCGCTGtGTCAAgAactga +cCAttTgTCTgaaTaacggaGtTcAagGcGCCtaCactGAGccCAtTggtCaaagTAcAtGTGGTgaCtGcAGccCCGCC +cCAtCGCCGACTaAcgAcAgAggGTcCAtagTTGAtGcCatCTtaaCAGtagCAaCTTGcTcaTcTTAggtGtCGAAGtC +GgatgtggTaTGATtgAaTgccGTCGAGAtaATGGACgGgGGgAAaGTaGttaTGGCGtATCTATtTACtCCgcccacGg +ttgAagCAtTTTCtAAggaTCAcCTgctaTgaCaGCgActACTGATTtCTGttcTaCacaAATaTggGAgAgTGaagacg +aGgaGtacTACAggTTTGGGctACCTATagccaccGtctCcATTgctttctGtACcCTTacTcaCcTTCcaAgtGtCaAG +GCAcgTCtgtCAAAtaTctCaaAtgGggtataCgacTcaAGTtAtaTaGgGgaGAcCGCccAtgcTgGgcaatTGaCaaA +GAtCAtgACAtAaGCtAtGcAACcGtcCcTcagTAgtTtAaCctcCTcgTgCtACaCaAAtcATtaCatATGgggcCCcg +GAgaCcTaCCGTAGtCtCgGccTGCtCcacccatcACTTcaaAagAAtttTccGtGgGggGcCGctACaCaaTCaaacaA +tCcttATGTTCctTgatACTGtaTTGgAGCGGAcGAcTTCgTgaGCgTAaatgAgcCATCgCctCaaGAaTTcaCtTAcT +AgaaGtCaGtgccTcCTCAcATtggtTggtGAAatcCcgGGAGATCacaccGCCGTTacGagCgCAaGGgtGcatttcTT +agTttGttATAGaAgAAAccatTagatgACcttcTcTGgacagtgTgtTGgtGATGGgagGCCtCCTaCGTCtCTCggCg +GtAtctcTaTcAtCgtCGGaACcaACgGTTGCgcCggCCCggatatAAagAaaTccgTtaAAGtGgaatTCCGAAgCCAa +atTgccggAtCcAaCAcgaTCGaaTtTTTGaaTAAcAAaGtaCAGAcGccTCaAggAgagCCaTtcGcGATttTATAcTg +GtcGGTAcaaTgtGTtCcgAcCaTcCgTGaaaCATttTAaCTaCAtatGCCCgGgGgCggGaAcgCAgaatctAAtCaAc +cCgATCGTcgtcGTgTgcGaaCcGcaCAcAgTgAGAgttcaaccgCctGGtCGGcATAtAACGTcTggtCaCcGCGTAAt +TagGaGTttTtttAaCgGagAgCgGTTATATagTgCGcgacacGtGcTTgaAgtgaTAtTTcaattcacCctGgCAaAcG +CTActgTtTGcTGaaatTctgcACtcTCATCtaggacaCtTtGtagtgGgTAgGccTcaTTTTAGCttaAGCcacatTtg +aAtttGcggCatATaAAGCGtggcCGtCTAAGTTtgGGTgGCtTGaaAcAAgTtacgggTaAcaAcaAaTATCtTTAggC +TgctTggaGCATgcaCTGcGgtagGTCAGccgTTAgAGTGaGaCtTgacgTTcacgTcggTaAagcGCTAaTTAttAtaG +CTGCTGgcaTCtGGGccAGtGGtCTgcgctCccCaTAgctGgcACaggactTAccaaATgAcctgttTgaCTGTCTCCtC +tTccAtcCAAaTaTgCCTaACctGcGaAGtCgaTCAGAGGtAtGcttgAtTcgGttgGAGGcCAcCatTCtCAgaAaCTg +TtCggAtGaGaGgacTCCGCAGgacctATcAaatGTTtgatTAtcgtGAtgaacgcTcAcAgtAATagTaCgCAacTCTC +GAccCtgcgTCTGCCTagcgcCGtacCcaCCAcgATAttcgAGcTtTCccgCaGatggacAGgTtgATctTtATcAGtGG +cCgTcGtACaCtaCCgCGcaTtgTTgTCcaGgGaaGAcTttGAatGAcgaACTAtcGgaAaAAacTGtTaCCTagatGCG +GCgtGTaCttCtCAAgagCtcacCcaTagGtGcTAaatgCGTgAAAAAccAgTaAttAAtGGtAGgatGacGGcCTaAtC +GTtaGaaTtGActGGCgcCAgaATggGacaaCgCcGggttAAaCccatATttGCtGTTgacacGcACccgCgctCagGct +TaTcTTcaTGaCGcgGGaTatGCgCTTagCtcCAtTCGAcctTCTaatGTTTCtcAGTaatctCGGgatCgGcttGCgtc +TTtAtAtAGgTgTaGacagtagaaaTtGtAaCcgtaGgCcgtgtaTtCcaCTAGTaagcCACggGaAttcGTgtGCacAT +acTGtctCtCgATAatccAgGtgactgTGgggTGgtCatAaGAGCCAcgCtcaGCgGtacTGGCcgaTaCaTAtAGAaCG +cgGaAtccATCgtatctTGCAgCTaAGATgaCCGCaTtCCaTctCTCaATCgCgGcGATcgATAataGgcTActtacgaT +ttcATgTCTAActGAcCCGCAATcAAactAGAcctAtcGaaTAAcCAtccaCgGCAcGGGGatcAggGCcGTAaAGcTcG +TCcACtCACATggTAAACTTtgcaGTttCCCAgCataCAaacAACGCctcAatCtGtCaaAgTtTaCgGGGgATcgTcCt +atATcaTatCtCgGATAaaTAtAcAGgAaCTAGGTCTCtTTTccGatACgCtATGCgCGCTctAcgAAGacgCgaaCAgA +GaCAtTtgTgaCGtagGatAcGctGcggatAaCcTTGAGatggCagTtcCCTGcgtgtgagTccGGgtAacGtGAtaact +tctaccgGaTCatgcAATCTGtcggTaTaACaTCCGaTGcGgGGTAacCtCaaTGtATaAtActcctGATcaTActCaGt +AAgCgAAcaATGTtTAAATtgTCTGcgCTCCTCcCCattaaCGtaTATtTAccAGttatGgcaAaGAcAAACTgAaCtCT +tAAcGgcTgcCCctTGAtTttAaCACCAcAtAGttCTgAgttCcCcCCcTGtTaGtCcTTgTaAaATAacCCCCAgaaCT +cAatgGTAAtGCtcTaCaacTcgCGgGGTGATacaGATCtaTagTctTtTGAGtGgTatggTcGCcgcCcGtgggaGGGt +tcTctTAgAgATgaCcaGccACCgTtgTTtaTggGGTGCCtTAaTAGCctcAGTttCtAatAATAtACAGGCggccGctG +gtctgTgTGgAAgaAgtAaCCCAggatACACGcatcTctatATAgcgtccTttTgCGcCgtATAcatacGGGCTTgTcCt +ggtcgGtccTATGTcAtGtCctTctaTcTCaTgggcaTcCatTAACaGtctcTTCGtCaGttGcacgtgattGGgtTtAA +ccCGcATcAgtTcTGtgCGGcTtGTcttcAtCAGaaTTaCtTcCTtcGcTAtGgGCaAGaCACagCtCcTgGGGtGATtC +GtAaaaaCaTGatGCgTcaTGgcatGTGACcgTgCAGAtGtCtCGGCTCGCCACTGtCgcgCCGtCgACaaTCAgtATgG +ttcaTtCgaTgcCAtaaatacacgcagtGcAgcTTaCcAgcGGTggttgCAATttTgttAcgGtCcccAgGgGcCCcCGa +tAAgAGAcATTGtTTtacAcTtTatATGTTGgGActcAaCcTCTgcAacgaGCTTGgAgtGATagGACtTCccgCatgAa +aAGCgcACATGcGcggAagTAaagcTATGaccCAAaTCGGCtCTcTGaAGcgTAgCAcCCtagGCCActagttACTatgA +gaTTcCcTgcTgggcCGACCTcacccctTTAGCgTGCgcTCggtgcaggGaatTACTcTtCGAAtGTTTatTcCaGCcGG +CAtcagATgaatCGagtCAgGttgTttCgcaTTAAgGGaAcTcCagTGGaCaTTtTCcGCTctatGCctTtAaTTtcCTa +TcCGTcctgGacCgAGACcTCAGctcttGTcGTtCcTAAaAGCGtcACaCggaCCtttcgAtttgcgCtGtCagcgGGCC +GcTCaCATGAAttgtGGaAtcGAtACATcTGAGtGcTGTAaCAACtGtCAAGagtggAAaGtCATTAcGGTcTaGTATTC +GcGatTcagAGaTatcTttaTtCcAgccgACaataGagcTgtgAtTCttatATaATGGGAcCaGatGtTacGaaGGcaCA +tgacGCttActGTcgCAtCTccGCTgAaG +>2 +GtAATGcCcgAAcGgacCgAtTaaGTtTgGATgacGaTAAAgtcGgacCcgCaGcTGgCAGaaTcttcGCgAgCgTAAcT +GTaaactaacGCaAgAGttGCGGcACgTaCcTtTTTAGacgAgGTtCgCtagCcGGGCGctAatgaCcTgagagTaGagc +aCcGGGActaaTGgCcaGgttagTaCACaTcTgCatGTaCcTGGTcCaaCGacgaCacatAgCtcgAttTtTTgTTatGt +TaAGagggAgGatcATAaACCgtgGAgagATTGGGaCAtGCaaTcTGcaTaTAATcCaGcCaGtAgttaCaatgGAagtc +ttaCAcacGgTgTtcTGGcGtCgtAatCTggACcCcagCCCgGGTcAAGCcacGaACATtGCCGccTaTaAAaAtttGaG +tccTaCccagatgtaTAAagATACtAggTtGgCTACgcAcCtAtgtatatacCgAtcGtctGgACTcCCccGaCcACtgG +gaaTGgatTCCcAGgTAgGacTgtGTCctaTGCtATcttgCCCatCacGGAcTCaTCtctaCgACggTccTGAGGgaAgg +GCAcgAcCCTaTTgGgcCCTCTGACAgCCctgGCcTAtCgTaGCactaAAtatCcgCCtCTcCtACgCCctaAgGaACGT +AaaATCTCtgCCAatgaCaaAGtTAtcgttcgATCcGTtaCGGGgAATTacGATtGAtGttaCGtTgAgGgTTaCttGtg +GAgTTAaCCcCgaGCCcTGcaTGCcTTGCgcGTtcgCgtGCaGCaAtGtAGTCCCgggAgcaGtTaaGgAtCCtaAGctC +TcgGgAtcAagacTTTTacAtTaaAGAaatgTatAGaggCaaTgcCCtGAgaCTcTCcTagCAcTAAGcTCtAGTGtgtC +tCGaGtACAcCcGTcGGtTaattacgAagACTCactAACtTgTCAggactCTTacAAGcTtgTcAAacCCgATGaATCCt +AtcAggaGaGTCCCGTgCttCaACcttATcAtGctgTAatCCgtTaACTTCAGcGGCGTCaAaGAATGAaAaGggAGctA +TcGcGCtttAtgcCTaGCTgTTgcCtaCgtcCtTAGCaGtcaATAACccCgcactgTCgGcTatTcTGGggggGaTtgAc +aACAgTTATAAGatatAgATacCGTtAtacgctcAagatAcggtAtAaATgAcGaGCTCCgCGCgtAGTgtCCAAGgcta +ggctCGaCaCCgGgtCTaaTGGTCGTtTATTTCaGgAtcttAtAtaaGttaaCgGcttccCggcctGtgggCtTAaAATc +TACAACcGtAaaCAGAtcgcGgATggAgtggcTGcGATtattGtTgCCAcCgaCtaCCcTAAtCGCtcGATgaCCccaaT +gGAAaagaaTGTAGaTCatACgTGgCCtTtCCAcgaggtCCtcAGcggAcGcgATcgtGacTcGgAcCTACACTGtCaTc +GTAGaatGAaAGtgCcgttAGAcaAaaTGtGTAATtaGAAAcggggaaATCGGgattaCAagGCtgcAcTaactaAGTgC +AtgCTAATCGatcaTCCatGTTatAggTGtCCgttGGgCGGaCgAGAATTtTgGTtATcgtTtcTtAGatcTCgAaGttt +ttGttATTCgcTGcCcctGCcGtActtGctGGTgacaGTgttagATaGcggctaGaaGGAtACtgCtGGccTTaCcCTGA +tCTCcaGatCTgacgatTTtCaCCAttcAGaGAatGgcAactAtgGaagGgCtGTaTcgGgCCctaccccCTACtCgaCc +GATgtTcTATcTaAAgccCCtGCgaTACTtcATAtaGTcAatggtcgtatcaAgGTgACGgATCTaTCCgAcaGTattTa +cgGTGGCGCgcTAaCGGcACtcAaccGtGTGgAtgGtggCgACcatGGTcGTaTgttgCcTGGGcGaaGACacTaaAtGc +aCTaTccTtAatCTTCAAaaTgcTggGTGATCgTGAAcgGGTCCGAcACAcAtCTtTGTcAcaggTgACTtAAcAATCct +ctTaatgagTcacgGCAgCCCcCTGCaAattgTaacGatcaGaTATGAAAcTGTTaGCAtcgttTGcTCGtTGCccTgaC +AtgAcGaCCatagctacGgCACcAGtgccTATTcatggAAGCTgcatCtCAGgCcgTAAGtgcaAGgCAGAgAcATgcCt +gtGaGaCCAtgGtAcATAACTTAGCGgTCcgAcCcgGTCtCGAaCTtCCctattAGAGgGctGgAaGtTTactatgCAGT +aCGcAAAcGTTAaaTgAaTGgGGCtCgGtAaATcAaGCgccATaaacGgCATCgAccGtcCGgCtAAGGGGCAgTtGTTT +GAATGGGtctttCtCGgTgCtGAtGtaAGTTccaCccAGAGcCTTcgTaGtcCAtaTcTCtTCATTAGAattCtCCcgtc +TGcggcTTGACAACcGATcaAGCCaTCcAGcaCtccGtGatGTctTAATTgtATGtTagcTtcCtCAAtCTaTGTCcCGG +GTagGAcCgCACttGCGGCGCaaAtGaACAtTCaaGATtggcTTCcTcTtCTccCtTgTGaTgAgaGcaatCCGCCATAa +GGcaCccgcAacGCctttGTtcGGCctcAaTgAcacCGaGactTtAaCtGTcAcAtGCgCaTTaaTtTCAtCAGtcAcgt +aTtAAtcGcTgagAgatcTTgtCGACTaGgacTcaGAtGCcGGgAAagACaaCggTtGtgtGgTACtcgCTtCTGGCATT +TATgGGaTaAaTcCTCatGtcGTAaGgTatAgTAcGCGTtGaATGcAAaTatCCGtCtgtcaTGcCtaTCTaGGCCaGGC +cAgTTCCCCAATAtCTgAaGcAtACgTTCcTcgtgaCtgtGttatTGgtcacTCTcTCgaCgaagGTcaGctTCAAcACc +aTggtgGcGgTTgACCtGGGgGacaCCTCtaagTTagcTgAaGCGAtgATAAgactTatTtGgtacTCGGTCAcCgACtC +ATCAaaAcAtTGTgtgCGGcaCcAtAgttCTcaaAgGaAatgtaCCATctGTGttcCGCgagTgAcGCAcaggGCGAtAg +CTTAaTAtAcGtacagTtCacCaGCTGTActaggaGaCaCTCCtcgGtaGgcCTGcGTgTcTTtTTCgACtccatACCGt +cgCccTTCgGCgAatgTcaCaaCAGTgAAtAtCCAACAaCAccTatGAaGCAGtACAccCcataaaATaGGaTgtTCAGg +aATAttgTgtcgGGgCaGActaACAttgcCaAcGgTgGTttaaACACcGgtcgACactGatcGCagtACCacTaacaaAc +aGAACgAGCTttctaCCCTgtcCGAggggcTTgTTCCTGcatCGcgAAcCgTTaACgaAaCgCAccAaatTTgCtGGTaC +TcaGATagaatCacCTtCtaGCacGGTgactgacCcaTAtCgcTtgctttAGtTgAAaACAgcCgCAGTTgAcGtgcCgG +tTTcaCttcGgCTAtAAttcTTCcTggCAgTcAACgGaaTcTtGCAgGTtGGCttGgACaTagcacgaCaAATtCATttA +gACCtCGGgGGATCGgcgtgtGtTaatGatGcaaaaGGtAcCcCCTATgCGcCCCaTCacAGACatGaTGTaagCaGAgA +ggtaTtAgatggGGcaCaCTAGACaACTTgcCCCGCCGagTGaaaGtTGctgaaCCtgCtCtAtGtctACaGTTatTatc +gtCAtcCCgTtacctcctActagGctagcCcAtCGtTTcCCGccgGCacCTGGaacCaTAtTtCtGgctTcCTAGAgGtT +tCacAAaCgGTAccgcGaaGCAcCTcGAgaTTAGtagaaCcAAtaaccAGacCAcgaGTCaTacaGtAACaCataTaGcG +TACgGGGTAACCAAtCaccACctTGCGcatTTAtgccAccCccGtCcAAAAtTgTTtgTacATGatATaGCATaGCgtGA +gcgAAAtCtTGCAaGTGAccAgAgTgggCagAcgcttgGcCCagaCCCttccTCcCCaTttAgTTGAatgGtATGatTTc +tAtgACaAAAtgtcCcaAtTACcgatcACTTaGagtcATTtATgCCTGgAgCAAAagggTGaTaCggaTcCGGGTTcACa +CccgAgcAaTcgtAtcgATtCGGcCCtGTGaGccAtCtcaCtgGgCAaaTTtggAAggTtgATCgAGAAAAattGGAtCC +CtCCTCcACacTTGGTggaTTgGTcTGaCacAgAAgAggacgCGtcCGaCcaAGcAgAtcCcacAGGtaAcCAActcTca +TctCgTcTaTCGTgGACGTctaGaaAtacTTACTaTGGTgACaGtAGaTaTGgtggTCATTAactgTttAGaATCATGgt +AAcccAGtCttAtTAAtCaCtGaaaAtacAATCCGgcCtacCggGGTtcgTTTCaAgCgCCgGcAgAgCGTcttcAAAGC +acCgCGaccActTCCAAtAgCgGGctaacctATaTCggCtccTtACCtgctCTTAtGCgctagAcAggtAGCGgcTAtAc +GGGTtTCGGTTtCAAATCccaAcgtttcATgTGtCTtGTCcGcAgaatgGCgCgtACACAAcAtaTgATtTCGCggagcC +aaCagTcgCcGGGcAcgcGtaACAAggcGAcGCgAacaTcgaatAgaGCgtCtCTGTGaagAGTGccCCGAtccGgGgCg +aTTTgaCTcGGCACgCccTggACTtaAGCGActTactAaTctcacTAGgccATgTTTGAAgGCaCataCAtaCTGatGtt +cAtgTGgaAttaCaatGGGAGGAgcgcCaGgCATtTCaGtaGtatAAAgCaAtcCTgTatacgGGctaaTGctTtaGgaC +gTcGGCCccctaGgCTCTTgCgTGgTcgCGtTTcTGCtCaaGaAGtTTGtCccAcTatTTCGGGagagCCtGATaCttaT +TtGtaCaaGAtaTaCcTtccGGTcCaaCCaTtAcACCacGTAtaGCTGGGtcagATCttCGAATTccaaCCgtccTaCGa +gCcCtCaCcGaGcgtgGACGtCaGTcGGGTgGtaatcCTgGggGcGCgCCGgTcgTGgTGtGgGATgaAgtAactAtCTt +aCaGTCGGGaaaGcacAATaCtcCgCCAgcaAcTAtggGtcTGATGTtaTttgCtcGtgCAtacCaGAGACTggaaATCc +CCgaaGaTAgggTCCcGTatcatcAgACcTttGCTacaaAtTATCTCtgCagCcgtcGaCtGgtTCCgttCTCtcGTAtg +ACgTcGaATacgctCgtAtcgCTaAaGaAtgaAtGGagGtCGTAACtgGAgaCcAGtCcGActCagTcgtaCgAtcgGtG +aaAGGgaACcaCTgCgTCaGtTACTGcAtTTCGCcaTcCAATCACgAaCCcGcCagAAcaAAaTCcCCatCtGCtcCCct +gGggTGGAAAgCtgTAGTCGcCgcAcacCcCtCcaaCTCGaTGtgCATCTGtaGcTagActaTtAtGGcgAgAGgGTgGg +CCCaGccAAgTgAacgGtGttCaAgaTccaaCttAgcTgTcGtGctTctATAcgcACtAaActaAACTaaaCgcCgggaT +ACgGATTTgttcATaAtGcGGgaCGCAAgaAAGcgcgCAcAtgttGgCtgctcACTTAATactgAacgacAACtaAaaCG +CAcAaAAaGcCGtAggaAtTaGgTgGgcggtGgATTCgActGGCCGTtaCTatTgcaAgaACgGagaCGAtCgaGtAGtt +aatGATagTGtgGTCGAtcGCGcTTtatGcGAacaTAaagGTttAtAaCtaTCaaTcGttcaAatgagatCTgcttgGCC +GTtACcCAAaAacGAgTtAGAtcccAGAcgAAgGcaacTTggGcaGCGgTtATAgTTtttaaTgGagAcaAaTgtTggCc +tTgaTGgTACgcGGCcgCtTAcAtGtcCCTggcAACGTAgtGcgtCGgTaAAgcCccGTCTTCCaCAGcAgGtTGtagcC +cacagCTTggGCtaAAAtCTcacaGagTTAtaggAaTgACcaCGcgtCgcTgGCatagaAgGGAttGcTCAtATCTCCtA +TtTGaAAtgcCAcGGgacCTTgccATgAtCCgttCGTgCTGGcgGCTTaaaAAAgAtgAACtGTtTaGgTcAgAaCgTGG +TGGgAGaAgAcaactCcgCcGAcATCCGtCGGtgAGGAgGctCtGcACtAGccGaTCCAaGTTTgtaCtgGtCactTttg +TaAGaaAtCgcATaggttAcCACcAcCGcaAACcGTTTtaTtatGGAgAtgCatCtGTcAgcAtAacGTgCagcCgAata +GtGGaGtaaaGCtGagCagtGgcacGTgctTGGaagACctgGATCcTGTtTTtCaaCAcTAtAGTaTCtGGgTGCcGgcA +ACtaCGGtGtAcAggcGgAggGGcTtgAAaCTGtAtaTAAAgCcAGAGcCagAgTcTGtgcctACgCgaAAGcTgatTGc +GcAGccAttCGTcGGctgagtAgCGagaTCTaAaACAgACtgATGGcggAgGagctACGcGgctggcCttAgAGtgCggG +aGGatATGGcaAAtCATtGgCgcCcagAgaAatCCtagaccAcatgGGcCTcCAgagCAAaATCCCaCCcTGccaTGGCT +TgGgcaggAgCAaTgGGaCcaGtGAtgaGTcgGGgtaAcTaACctagCgAgGTCCGGCgGATTAaGggTaTAAGaCgTaA +CatAGccCAccctAaCGTtaAatTcgacAcAgTCcCGTTAcCAAAtTTCtCgccGTactTaAtGCaGCGgtTTatgcggC +cGaGTATAGtcGGgaTTgCgAtTAGtttatActtatCCTcACAGGAcCcgACTtGgcgagCTAaCTGTGGcaaGGCtCat +CCCAtCacATatCgcAgagTgCAGcACCAcctGcaCGTGCactaGtctTCaAcgGGaAacGAGCCCatgtgGggCCgAAC +cCtccTtcGtCTAttCCaAaGgTAaAttagcAaataGgAcGGcgggCTgatCggGcTATGCtGcGCtATGCcGTcgtgCg +aCCAGtaaTcCTCCGCaAcTCACaCCACCgagATaTAcaagccACAgCtgccCGATGAACGtaATgtgCGTggtaCtTAg +gAgtCGtTccCgtaacGCAAGgtTCcccTGcaCGTgcGTTggAcTgGaTTCtTTaAaaacAatcGtcgGTTCGccgTTcA +CttgTacTACttaGGgTtCtttcTTtAaTTctgAAAtAGcTTCcTaGGTcTTACTAcgTTgtACtAaTTaACGGgAtTAA +taAcctGcTAAtGTACCGtaTcaCAgtCaACGgTTTggCTTaCAtggTTcgTTgcatCtCATgGCCGACtGATACgGAcT +GagtCcATGttCGCgGgcaGGCTaCtGcCCgAtaaaAcCcTCATgGTTtcatCTAGacCCgGGAACtCtTTCgTTTacgg +GTcaGatGGActCtAcTCGCgcCTTGTTAGGAccCggcGGCTtTaGGgaaTcGaaaaTTTtAgagACgtCCCCGaaaAGg +TtCgTgtCgcgGAtgTCCAGGActgctAggGgTTaAatCaTcGTTCtgcaAcgTcCGAGcTaCGaggtcCAAttCgAGTG +GTTcaatACcgcaAttCAaaacgagTgAAAAtCtAGcGgAAAtagcAaAtTcaCcAgGaGCgAttgtAcCGaGcatAcAG +cttgctcAaaTTtaAtATGAGAaAATgActCGCtacctaGCgTgTGcaaGGtGaagcTAGccaGGcCTgtaccGtCcCTA +TAGgGGggAtcaATgGaCAcCTGccaTgcgcCttgTCctACgCCTCGCctTcTTCtCcCAAaTGAGGgTcAagCAAgCCG +taAAaatgGcTagtcgGGTcACCtTcTAtgAtCcCtaAcaGgCGcCaCCAgcGTTtgTgCaGttcaGGacCAaaTCaGgt +AaGTTgGAtctaaTaTTCCcgcTgagAtTTgGcAcatctCctgtTctAagATtctgACcTccCCtcAggcgTATCgtCgA +aGGcgCaGGCCtCCgAatGcatcAGagAAtcaaCgtaGggGaCgCGAGcgcaCtATTgAtCTaagtaCaGTggcGTctGg +aCAaCgtgTgGctaTATgtaacctAGaAACtttAtaTGGCCcgaacattGGTaAAcaGggCtGtctATGgcaaTtGgGGG +tCacTgcActTtcaGTtCcGgtaaggTatCACttgATAaaTCCAtgGGtaaacaCCAGAtaTgACagtTGAcatttAgGC +CcGgCccCATcAGcAtgcaTCGTAtTCCAtCCTcCTAtTGgtAATaTcctTggCtctctAAaCGaCTAcAccCgCggaca +aaACcGGgTAAtCGtcCTATCGtGgTATGTtcttTcTtGtAGtcTgCTtCtatTagGgctgaCagaGCaTcgTATatGCt +TCcaGaaTtgcAtCTTcAGtcGgtagGGggcCatcTCGGCcgcacccaCgtTgCaTTggtAGaTtgaGagACgGggTCTg +taTcaAAtAtaAGCGAa +>3 +tgGcAgtGCgcATAgggctCtTCgCggCCCcatTGtTAGaAgCATgttGaAtccTgactTgagaTtggtACgTaAgTcAG +tGTTGAaaAGaTaTaaCAacAaTCTGagTagAtTCGatAcTaCCGttgCATagctgTaAataaTaTCaAaatccCTagTC +AgcTgAgtcCttgTtGaTccatCgactgCCTttAtaTgcTGaTcACaATacCTttTcAcAacATaacgaccaaaAATaTA +AGGACGactATcTTccTcAacAgATtGgTtcCggaGaGtAaGGgtgATaGGgtTCcTggttgAGtacCgAaGgAgtgtgt +atgGcATcTgGGCTcATcTGacagGCtGGAtgGCcttctTgGGtgggCtAtTGtAAatacatgaGGGgGgGCAgCGttTC +aGaacCgACTgGTgCGCtttAgACGacgCtGgaGgacgGgtGAcCAaGgAGAGtTGGccGtAaaGTAgGGCCAAgAATta +gaTtCCtgCCCtAagaCaCCGCtTcATTCaacggaCCTTGAagTGtCTGaTtAAaCgctgaCcgGTAActaGaGGttacT +gGcggcaTtgCTCGcATGCAcTgTCaGTgcCTGtTacAccaaAAgcCcCATttaGcACagTgttgtTTGtcaAaGTacgT +atGcTTCCtAcaGCcAaggGCaGgAgctGAaAAAAaatTatAAGggcGgAgAcaAGgcTgaCcgAAttaAGtGaAcCGTg +gtTgaTgAcTAtTctcgaAcggCcGcAAACcAGTaAtAttgcggTGtCGCaGcTaTccCGtGtAgAgacTTGGTgTgAAg +CATGgCTTTTACATaCaAtcgcaTCtCattcCtgCCTggTTTgcTGgCcgCtACtTtCaaCTTccCAGaTgcagTAcgtt +tATGTTgGGtaaaaTaGGaTaTaCTtgcgTgtCtCctCCtcaatgGgggaGctGAAtGcAGTTTCCGATaGTTACTATag +AAACaAAtggaCAGtAAgGgGaTaATcCCcTTtattgCAtATCTagtcCCtAAAgCCgtaagacCcaACACctcTGagaa +TACTTCgTTGgatcCAaGGCAtCcAtCtTaGTgTAGGAtaCCAGGAGTgggaTtcAActtgtGGTgCgGaCacCaCAaAA +cCgtgctGGgcCGCtCcttgggTAAggatGgggacCtgCgAacgAcTCcatCggGGAGcAGaCACTgACtAtAaTatGGC +tcGtGCTTAcgtgcAtAccTgCAGtatAGTgGGCaGtcCaggTCcCaagTcGacgGtcCaaTGAagGGtagACAATTTat +GTcAttaTTTACGCAtGttGgCaaAGaCgTCCccCAgTTgtAGGTATaAGcAcaATTataaCcttgcaCtATCatCGaCa +atgCGTAcCagacGCtcatCAaGaGAcTTaCtGCtTActGattcgTTgAcGaTGAaaCTAgcGCAcAgTcgGCtAaTTgG +aAATggCGTACggatacTtCgGtaTgggTGacTctATAtaCTcgaAAtTAGgaattccAtcaatAaaAcATTatatGggc +GtaATgTtGAAATCctTACgggTattTTAcaaggGTcACTCTAGtaCAcTCGATaacgGATGCgggCcgaTCTaGatctt +aGGgTcgaTTgAactGcTttcCtgCTtaTTtacAatTcTaAtGGcattcaCaagCgaCcGTAgCgaCgatCcgGTaTAaT +ttaGcatatGaAgTAggcGaGgaTCCAacgGaaAgCCggCgCcgCcGTtCAAacTcgaGGtaAtaCTgcaTTgAAGccGt +AggCTtcgcGAtGcCcaTaAATCgTtTaGTcAGgcGTaCacaaCTAaGatctttctTTCTGtcaatCtGCaaTgaAGCGg +CacTAGgCgTtaaCgGCTgTgagGGATGGTCATgTcGCTAagGcCgCgttGtAtacTgttAaAtcGCTCtTcacGgcCgt +TTgTcatgTgctcaTTtTtAtgcTaAgcGgTgcTgCATGGggACaAAGAtTAaCAgcTAaTcCTTtgCgGcgTaTaaCgt +AtGGgTtAGtgCttgCTtTggcGGcgCaaatctTcttTCagtGaTaGCcTTCtAgAtCatgTgAGaACgaCagCaaTCaa +GTAgagatGctAggCatCggGgGTACCcaTAcgCgtaccTTcatTTgGGCtCAAaatAacaTCcAACcTCgGtCTGgCTt +GTCagCtAtcacGcCtttcaTggGctCgggtgCcgAGGAAGTtTtggAGGctCTCgCcCAtACgTaTCGcAtttcgcatt +CgtGCAtcAtttGATgtgAAcGcAatGcATcgATtttTctCCTTGtGAAGGttTaGTtAgTGgtTGgcGActgGCaCaTg +aCACtCTgCTaaaCcatgAtTCGGTGtttTGGtcGCcAtgCcAcGgAAgTagagcTATcaTttcgTAaaATatGctcggC +tGTgCAcGcGAtGcAGTgAaGtGttatgaTAcccGtGgaGGGtTtCgtTactGcgAagACCCgaAAggCctAaaGAaCat +tAcATtTcCGctTTCAcgcCCtgccAGcagGAaAGAAtggCaACTgGGCTAcGCGGGtcgtAAtggtGCaaGtATGCgCC +gtCagtgtCgtGccCacCGtgaaGaaAAaaGAcAagTGcGtGGaTAatGtTccGTaTTCGaGgCTGAaTcTTTAcACGcg +TTgaCTCcgTAAcTaGAgtGcccAtcGGtTTAttCtTAccgAaTACGagactGAacgcCCTgcTtGAcaACtaTaAAGcC +GggTTGatgctaatcgCagaAGGGAaGgTtTGaGCGaGattAAaGAgaAaCgAAGAAAgcGatGggttTcgAgCCCaCcT +cATAGAGCGCCacaagGaaaGCtGagGttTAcCGGaTatcTtCGGaTaagTGcCTtgATcctCcGctatcggGCaaAatA +TTTaGgATGAtggccCGccaacacAtAcAgaTGCAaCGtAcGcgaaaaTTCatTTatTagGAcGAtATgtGTAATtaTCc +tGcgaggggCTcGCcAcGCcGttGcaGACgAtaaATATAacaTAGTggacAtCaCaCaaATCTTtAAAccTCgGCaGcgA +TtTCGtggTAAtctAaatTcAcaCCaTtAcaCcACACGaAATAaCgGgtGtaTTtttCgGGttgTACgCtTctGTggCCA +CtgggtTTtGTactccATATcCAGgtAcTATggTaTccGtctGccAGCGAAagTAaAAgtAtgggATAgATgTcCCCgag +gAttgTcTtAcCGaCGaCTCCGTatCgGCcgccTaCcGtATGAcCAaGTCCaggCTcAgtacgAcCCcaTGAGTGGattC +TaGTtGagGcTtttggacTgGCgTgaTCTTCcGTgACcAGcgGCtCTcGCAGatagcGCAtgTTgGAgcCcgGCcaAGGc +GTGGTaAaGTCGGgtAtaAAggaACggTTCACaGgGgTccGTACttggcaAGTTgcttGtAATTgCGAcgCAAtcCGgaA +ccCCgGCGgtCgcCttAcGtgCgGcTCtgTgATATtgtAgTtACCccTtgGCAgacCgaCCaacagGTcgttaAGaAgcA +ccTtGtcTGcAcgtAtGcTaGcctccTaGttCGgagactaCCATGaccCGGGcCgtTTTaGcgTAGgaTCCcTtgtaTaG +cGCaTCatGgcCTgAgaaATtctAGTcgaTCAaTGcAcGactTctccaaTGGGGCgtcCaaGCcGaatcgTgaATTAggg +CtaAAaTgtATccGaCcTgCgTgtGcGggTtTaTgGCaactCTgCtcGTTggATAtGTtcTTtgAtCaCTaCgcAtCgcT +gTATgCCCcTaggttCtTaGagAtActaaCATTCtcatcTGCcCtagAAtaTaCtCGaTtgaccCtatAtGgCgcaaGca +ATcgGCttTGACTgTcGTATaTaGgGggatAaGtagaACattCcGAAaACCCAcTtagAAtgcCgtACtCactTtcacGG +cCgCCcCGCTagCcAcGcAaCgtCgAGTgAtgCcttGccAtCtAgGcgAaGCtcCtGAcagGaCgcCTttAtCagcggGg +GtCtGATCacaCCGgTCgcgacCTGAAGgCAtAtTTtGGTTCcTGaCgtCctaaGAacAaTaTTcaAgAAGGaTTCagTt +CACgAcTTAGggAgtAatTTGaAGacATCTaaTAaTtaagatGAtCgCGTtaAcAatCcGgtgAcAaGgaTtAtcCaTtc +CTcatTgTAcctcGgcaccTaGGagaGCGAaatTgTACaAaAGtcAgaaTGaTGGgACCTTcgCgTtTGagcgtAgGtTt +gCtaGcGggaGttatCTgatggCCgcgAACTctCaTCAaCCAGtaAAcCgaatAActtgCggTCtcGGAAtcCTaCaGCa +AggGcctCTtTgTggCAGGggcgtTTgatatGgTGactACcCGAgGAGAAACGTtgcgAaaAcATtttGactaCagAgCA +gtGctTCGTattaGtCTCTgAcGCGcaaaCGGTCgcaaTAgTAAaaTCGgCtGTTgcAacTaTgTtGGgaacCTaTGgTa +GgcTGCtTtTgcaGCaCTgCgGAaCCCCcctTtGgcGtGcgCaagCcttATGTCGCAattaAGGccgACGgccAgttagg +CcTGtGGaAttTCCGGGAgtCtgCgAAGtTaTgcTCGcCGTAgTtGCagtGAGgtaGgCTgggaTcTTaAGgtaGTcgAc +CTTaAgGagaAcCcaGACGGaTTtacGTGagAgAAagccatgGcAtTaTgATAatGACaACGacTgccggaTCGACacct +gcTtgTcACatCAcgTATAgTCtCCgAgCCccCCgatGttcCgCGgatGaagcccatgtcTGAtatCACacGCCaCcacT +GCcTtCaATtaAtaAgacGtcgATCGaaggCgTaacGgtaaTtaaGtAgAGcgaacaggTcCTGCtTGTGCCgGtCtCCa +TTacAcaGcaAgaGcgccTtacCtTTacgtgACCACCCAcTGTGAGtagACaAAGaAgtgCaTtaactGTgAGgagTcaG +ttcCcgACgGgagaAcaaTTAAgggAgTtaaAaCaCcgGtTTcGTAtcCactCccTCaTAactgTAACCtTcTaGGAGaG +AGCAtCcAAagGAgCcAtGCcaaTcGttAcTtTTgGTCCATcGAtggctaggGCATtAcacGgTTCGtgGACaggctcTC +AGcGgaCAcGaaaaccACCTTggCAAGggACTCTGggACgGCaAcCAaatatgGggCCgtacTagaATcCgCtTTgCttc +tCctTtAATCgaAaaccCAGggtagcaaatcccCAtTgaGagTcaTCTGTgGtTtAAaAtggGGgTtccAAATacAaGGg +TAaTaTatGaGgAacAgAatcgACTTTGCAGtatataAtGGcagttatgctgaTTaAtGcaCAGgaCagAagAGcaAaTc +gaAAaAtaaaATTCtgTaaggGcTgcagTaTGtcCcTATACGCTACcaTTAaTCagTtgcTtGaCgGtGacaCAaaCtca +aCGCgCaCAttAaGAgaTTAGcGCaCgTacTAGAGttaTAccatTAAGCTGtACgGGttaTTTCTTagTTcCAcCCGttA +agaAGcgtcagTTCGtatGGggATgcaAgTCACtcgatgcTtTaCgaTgCTCGAGcTcctaCaaAcTGAACaAATccaTt +TcTTGccatTCccAcCAAtAtGTGtctcCCCaAagtgaCGTTgatgtGTCtAAaCAACcATGGggCTgCgtGGCgtTGTg +aggatttTgCatcacgtcTggGAAaTgcAAATcgatcCAACTGCTGCAtaTCAtCCAaaGCTGAAtTAgcCTTTtCgcaT +gGggcTaATCcttgTgtTaTAAtTGCgagTcAgTacgtaaCGGGcCGaAcCCCgtaGCatTatGtGacttTgTgcCCAtg +CaCTgtGATAGTgtgTcTCGTaGggAAGaGCaggtCaTGGaaAtTGtCTAatTtAGGCAgAtaCtGaCaCCTcgtcTcTA +gaCTGcCaaGCTtcttTAaAGtgttTGtcgGgCgCacgAtgGTCcAAtcGtAaGAacCATCCGagtTaaTtGAgcgCAGA +CActagGgGCTtTGgGCTAtaGtGCgtCATCAagtaaTatGAAaCcgCgtTGaggCCttAAAGacAcgTCGGGtAtAtTA +tcAGAGttcCGTAcTaaTCcAaGaatCAgatCaCcccTTcTCGcTGTCcCcACGGGCGttAtgctTGTgaagAaCgTCAG +CcCAaAgTAACggCcgCgCTTctcccCCaTCTctggttgTCgcgaGCccgtGaCcCCtatTCaCcCgcAGTcAGtCAGgG +ACagaCGCaGtTGcGATgaCtCGcAGagCTAAatCctAcAtGcacttaGgCGcaaTCgGgaaCTaaaAgtgaaGtTtTaG +CATTAGaCaagTcgCAaataGTTcCGaTgctaCctCtccGcGTgGTaGcCGTgtgTTCCAtCTAagGctCgggtgTcgGC +CaAaCTTAAaTgGgtAagTggtGgCgtcTaCaCCtgTTTgcCacgccAccGCGttTataaCCAGCcgCCGcGagaggaCc +GaTcTcGtaTccaAtCaCAtACTaCTATGGATtgTtCAccCCaCAttTtTcGaTCGtCgGgTttcGaTgCtgGgAGTtcC +GcGgCCTggaACTTaaCcGAAAcCctTGGCcaAaGaATGCGtATgGtGtAGAAcaGcCGcACgCAAtccgaCaAcaTCat +gcAccaCaTttaTCtCatCCcaTTCgtTGtAaGacCGGAccGACGgAccTACatagTGGcAggGtCAcgCtGtgtgGAtC +aCAtcctAGaGGcCaAGAACCgctGtGccCgcCAGTtCTaGCTCcggagtggGgTgTtcccGtCacGcCTgGgCaAtTgT +ttcgTATGtAGCcgatttCCTggAGTctCggGaattCaAaTCcacAGgaCTcCtaCAccgcCtGAtacGgagacCCcatt +GGGGCaaGGCGCtcGGaAgtACgGaCtCGCgAgAaAtaTatAAcaatcacGgGcAccTGGAaTTcgCcAcATggCtAaCc +CCggtGcaCGggCtTaaaGgtTgactATCaCcGAGcggcATaGTCTCccgATcagTgtaaGTgCGGcgCCGgCCATtCct +tCgggTGcttaaGGtaGagTaCcGTCGtTTaGtgTccgGtGgACGCaGatataCAtAagGGaTctaAATAagAaAcGgta +ACTcGggtcaTAgGaaTATgatGaTATacTATGAaatCacCAtgCGagaaGCATgCaGtGTCTGAACGAACcgAtTCGCT +aCGccGTgCgtAaaTGGagTgGTaCactAGAggTGCgGatGcagGAgccAgTCGAGaATTGTaacAgcACcgAtaTACcA +tcacGCGAcacAGTcGCGCtgACAaCtatcGgatCGAgcTgcCgCcaAgaAAaTtAgcTgGAAGcTATGtCgGCTgAGAg +gCCaCcTCTACGAGcAccagaaAtGcTAatTGCcaACGcCTgccCGTgaCCgtCttaCaaaaAAAtgTcGtCCGggCgGg +aAaActCTgtTAAagATGgCAcGcctTgGGaaaCaGGCcttCCCaCTCgcaGCTccaccaTAGCCcgTGGCCgtAAGCGg +GAGGtaCagCgGCAaAggcGcCAGtgtcCCActTCtTTataGTAGCAACcttaGAcCtaGgtTTaaGaGGACTGGATttC +gtTtATgGaGaAtggcGaTcgCccGcgtCCCaaaCaCAGtcAtAcaGcaacgTctTTctAaAACtCtgcaAAGttCcGCC +GgTTaggcTaaCCtGacTTGTTttccAGACCtggaaggtACGGtgTcaactGcGACccgCCcATAcCtCCcGaTTAtcCT +TTCAtCtCCCgaAGGtgACTactccCgagCaGCGtGTTgatAaAgtGatCgttActAaGtCtTttcGcaTATCCcAaggG +TCTTCGGAcatcCgaagaaCATgaAACGgagGCcAtAtcgCtcAACAACtttctccgGcTcGTctCAaacATacaCaccg +TAactaccgtAcCTTcgTCtacCtGaTtGTtAatGAAGcaCcAtCCAaAAGGGgcTtTTatAATtTGCaCTtAtcATGGg +tcTaGaaTGAtTGcCggTtagAtGtaTAaaTaAcAAgaCacgCGttAatgAAtcCgTCcagTctgttaAacGGTAtaGAT +aGCGaaaTAcaAcGaGaAGgacGTCTGGTgtAcGGggACcagtGtcAatgGATGaaTTATgAtaagtcATacCCgTGaTa +agacATcCGACGTtCccAgcaGttgtaAgtATTTcACATAAaagGTgcGTtAGCccGtataTtgAggcTaAgCAcgttcc +gtACaTtCggAaTTCcaTaatgACgTaCcTCgaCgtACCcTTAcgGAgaaAGtAATtatcaTTgaCGAaGTAaacaGatC +TGGagTTaTcttcATgAaaTCatgTaAaCAaaGgaCTAtcgtgTcTctaCCctACgtgTATAtAaAtGagGgaGccttTg +GaccgcagAtagagtGcacattTATaTCTcTtgGAcGaACAtgaaagaGGgtcATcacgACcAAgagAgGGaTCgTTCgg +CAgTGCCCACctAAGATccgagATcCtgTGtTTaGgGaAAGgGcATACcCAGAAGAAAcTgtGAaaGAtAgtcTCTTGcG +catgcCtgGgAgATaGGCtccctTCggCAAagAGgCggTattgTtTcGtTGacCcgggtGCaTTgGtCggttGAATtgGc +CtcCctgTtgcTG +>4 +CGgtATaTacgcACAAaAacGcAaAgTagAcTTTTggCgGtaaGaCCtaTgAGgcCtagTctttgGaGGCcTtgtAAaCa +gAGtcgaCgGgCTGccGcAtGGATCATTGggGCCCGGgCTcTTTtTAGcGaAataTgAcgaGtttCTGaAttGtcTACCA +GaGGCgTGacGTaGgtGCaGGTAtAatatAcacCTGgAgAcgCCCGtAgActaTATcCcGGGgCatcCcgaccctacTGG +TtGCgcGTggATGaAattagaGcACtTTTacCaGGTtACTaATcACACgACGatATTTGcaAGTaGtCcgGcGCgatGtC +AtATTCcccagACAaTGTaggcggactTCgtATaCgGCTcTCcAcgGtttgtgAtcgcTCcTgGaGaCagcAGacAcgCc +CatTtAGAcTCacGGGCCtTcgtCCcacctctTcacACccAaAtaccacTtaTActAtAttGtaACaCgTgtcGaACtTG +ccaTgatAaGCACaCGatgtctTCAtaAaTgGCcACACAGcCGCtTAcCTTaGaaCatAaatgGGTgATAgggtcgacGC +gaaCCTaTctctTCgtTaTttatGaTcAgtACaTggTcgTcaGcCGccaAgTtGtGTaTtAtGCttTaaGgAATTaACAc +gAtAGTAgTCgagtttcacGAcAAGcTCCccGGTCCtgctaaccTAcTCgGcgttAtaACcccCTtcTCgctACGCTAga +AAGTctCTCgAgtaAtCacttTTcCCAttcGaGCctGgcgAtATcCGtTTTcTagaGcTTAgacCCCccTAggcCtAgat +CtctCCTACgtTGgcctcTCGtgAcgAccAaAggTAtCACCTaACagaGtcAAGaCGtaCTTCAcatGtcatgAcTcCaT +agGcattgTACaccGcgcAGgcACtaTGaGCGtggGggAtAgaTgcgcCTGAaCttGAGtccaCAGAgGAACGAAcTaAC +ggAaatTGcATgCGAcCtcCAGgtgtCcActATATTatGgACCaaGgTccGGTagtgTTCTAcTATcATCacaCTgACcc +cGcgcgAgGCCTttATAgcAtcCCtaaCctgcAAaCgATgcctGTCGgTcatGtcCcAAggGCCcTgATAccgaGAgatT +tAtgcctGCgCACttCtTcAagCggaCAGCggtCcgTGaacGgtATCaccatGATctcatAcACaCtcCtAGtAcGcATG +TaAGCAgcgctgagtAGgCgGgGctTagCTGAaCGcggAGGaAtcGtAtAagAGgcCTcTccCagagtCATtCCaActAa +ccACCTggCAaaGGCCctCatCaCGtGGcGgTgcTTgCAcTcTaAcGAAGTtGcctcaccgagGAcTgAaCGGCGcagtg +cCgGcCActccCAgcCAGgAGagGGttTcatAaTCTttCGgTAGctttAAATGTaaccttActtTgCAtGTTTgAgaGtC +CaAAGcAtGAtacGGTCGggGcaCccAGGtTGTACGgcGCACgTcTttTCCAcggTtGgagaaACcACgccaCGTcGTTc +TtGCcAGACAGTcATTTATCaccttCACTCtCctTcgtAcaCcaAaGtgTTgCTAcAccGGATtGTAtcTccGcTttcTa +tAacCGagtgTtCtTgggacCGGcccgctcCgCGGaAcAtCcttttTgctcgATGTcTcCgatgcCGTcAgcacActGCg +CGCaAGagTGtgAACGtAaGaaACCAttaccTAtTATcTgtAaAagcTtgaatgaatctTtcTCTAGGGGTgtGgacagc +TCTcGaccTccTatgTTCttcTCcGTtCTTgatTATtTACCaCcggag +>5 +GGtTaacCtcatcAcaaTGcAaGgTaCAgtcgACatAtcgTACTgaaaCTTtCttCccGcgCaGttggTGCgAGaCTCcg +ccaAgTTGgtAgTTTCAGaTaTAccTcaggTtACGggTgCGGaACACCAGGtgGCatggcCggCGtgGGtGcagTtGTgC +TcaaCATcTgAgCaAgAccaTtaGAcGaGtTGGCgTccACggtAATtttGggGCtgAtCcgggGaGtcGAgtTtTGGGgg +TGcaaAcTGgCgTGaaATtCtcAgaGaaCaAGgTtCaatAaGattgAagaggtTCacTgACTCCTgTTGCAACCCtCCGA +cctGTcAGccGACTgTaAtttTcggCAAGgGAtAttCaCtCTAcCgcctctAatGggaATAGCGCcccCCttCGaCCgat +AtAcCACcaattCggaCtGtCGCCCtAtttAataGCTgctgTcGcgGCtAcGcccTGTTagcGagcGAttCgtAAgtaCG +CCacAGTAActATAaGGCGCcTCtcaaAGAGAGccTtCtATcAtagTTtTtCtgTaTGTAtcGGGcCacCgggTTcTATg +TTcTGAgCCgacCcAtgGTTAttTCgaTGgCTgcTcagAAAcTGaGgCGATTTCgcattGtCAcagttACcGTTAGTtTA +AGGTGTtCtAAcGACgtGTCTAtAGaGGCactGgaCaCctgGagCCgCttaATCtgTCATTtTgagCAaGGCaAcaAGCc +CtCaCAtcacCcatGTtTGCcCAagCAtCaTagGCTGCcaCGcCGGacATGCaaAggGGCCtGAGAtCCtCtgCaAAggG +agtgcacgcatgGtGtGatgTggTcCaACcGCAgGGgTAAcGCtgaTTTGCcAtctTactgtTcTttaCcCCCTACgTta +TGACaCAAtcGCtTttgccACagAACGggAtTCAAagcCAtAtATAcccgTcaAtGGACtTAcctCcgacAgTCcCTCGg +GgCCtgtTttTAgGGCgTTGcaCCTtAtTGgAtaactaaCCaAaggtaGgGgcaCtCtgcagGAgCcTcCaCaACGTgTa +GcgCAAcCtcGcTTCtTGGGAtTtTcaGCCAGaAtACaaAggGcCgGtAtCTATaTccAacaTCtatTACgTcgGCGTgG +gAgcTaggATTcaCgatggTTaaacTcTTAGaagAcaCcTCCtaATCcATTTacgcAcAGcTGTtccGTgTagAgAaTAT +AtgATAGaAAgggACCTTgAtaAGgtaAAgcCaTAAgACcCGGGcCaGgtCACaTAGTttATaTccCAcCGcCCtACgat +CccCGaccatgcaAAgAAcctcgCTtGaGaCaTACtcTgtAgtCGATATaAtGcCCttaaTAtttAcTCCACGaggaTAG +TTccgTtcgCtGcTGTcCATgGcGaCcTcGaAcCgTCCgGccaACgtTGaAgtcgCCAGcgAAcCcTGgCgGccaTttaT +AGGaatcacGgGgcGGctAaaccgaTCcgTcgCtTgagcAcGGTATGGAttCgTtTAGtTAcCccaAAACGATAtCtGAt +AttGcaCtAAcGGAAtCTaGCAGTaTggCGcAATaGtACagaccCCAAtAcGGaTcTactGGTaTCGcCacTtaGggcGt +tTcGACCGgaaTAtcaGTCAcTcCCagGTGCAcggTAgtacttaCgCAgctaaatAGGgAtcAaacaGcCtaCccGTGag +agAcaGcTAGTaaAGgtACgACGacGtctTagCgTaTtTCccCTCTCTTTTacGaaCGACGCCaaTGgTGtTGGCgATAC +AtAtGGCTCgAgTaCgCatGTccccACAaCCCAaaAggGtataCAcAAAtATAGctGgACcggGGCatGAGttGtTcTGg +GcaaGcaatTCTCAgtGcCCaTCtGtGcccacTTcAgcCgCtaAGCAGgTAatCcacgAacCGgcgcCGTaatGcaGacg +gCGGcgctCcAAaTGaGCtAcTcaagcgGTGaTAagCTCtCCcaaacAAaaGTatatttaGTtacaaGAtTCaCgGtTTA +TCACACcgccCCTCCcCgGGTTtcTTCATGgGGTAttGaGTGTGACAGacccgtAGcGAAGaGGgATAtgTAtcaagCGA +GcgCtAGTCATccgtatTtACCAcAGAtaCGAcaTAcGTAGaCaAtccCCGAccCAtctTTgGCcCGAaaataCGaTCCT +AaCttCAtggAgCTTCcaTGGTAgGCcGatcgTCaATTGAcAAGCgcTGgCCCtCtGaCGCGCAatcCTTAcACTgaGTg +ttcATCCAaaacAgatCaccaCtCgTTCTGaaGTgTCGGaGaGtatGcaAaagTgcatAagGgcgtCTGgGGtcGCcAAA +acTaGGaTaTataGTatTAGTaGCTCacCgCGCtTGGgtcGTGTgCCTttGAGcGggAGtTTgaCGcgccTcATTagtga +TGaGcCgcagCcgcaCcaTccaaggaAtcCaAaaGaGtGGTTCcgcacTTCGACcCGcaGatgGgGgaTgTgcCGacgCC +CCAatTCccGGtAgcacTGCacaTataGGTtGCagATtgcccCAGcggcgtgATTtTTgCCGaaagTcTtcCagTTaTTg +caTTCCGcgGcatacAgCTggccgTcgGaCGAGgaatcaggcagGGgGaggGGgAtggGtAtctatctACTTGgAggCcG +cTaAGACctTCtcggacCatattgcaAGGAGTTaTaccTcAAccCAAGTCTCgacCCTCAGccTaGCggCattGaTCGcc +tggGgCACtAaactGCctGggtgaCGAgAtaCgAaGcAgGTAcaCgcaGgATgtcGCTAtGgGaAACAacCacCTgcAGg +tATAtaACGAGAatAgGagTATTatGATgCcgCCGCggTaTATaCActTaaTGcaacgtTggTgcTaaAagaATGgCTTT +cgATgCCTgtaCagGGtaATAAGCgTcATCCaaCAttggtGCGgtgTCTTaTggccTacCAaGaTcggcgTGcTTttcGG +cGCCacTgtGccgTggaTTACtcACCaagAtAtTAgCgGGATcATctcgCtGAccCCGcCGGaCGcTcTtTaAGCCtaAT +CtTcTcctCacTtGtgGCtTgAtTTcTAGAAGgGGgcGTgAGcGtGcAAcgTcCTtAAaaactTGtttCGcctGagTCgC +AacGCacTTAGacCtaacCTcACTgGccGtgGGtTcTgAgatcgcAcAAAaCCagGAaCAtgtAaagAtccgGaCTaTAT +gGCaAagCgcaatAgCtcTcTTTGAGcgTCACACgtGACggcggTGtCCCgcgcCGtGcGTcGtcGGtcGcaagGTTcCg +AaGCtaGgCgccagCgTctaGcaCtcTtaTtgggtAATTTGGcGGAcacGgaGCagacTTGGtgaaGTGCAcgTtAAGcG +cgggCgaGTtATtAtTCAttgtTTTtcaGTcAgtTtATccATtgaCCAAa diff --git a/t/data/test3.fa.fai b/t/data/test3.fa.fai new file mode 100644 index 0000000..4eae6e0 --- /dev/null +++ b/t/data/test3.fa.fai @@ -0,0 +1,5 @@ +1 5869 3 80 81 +2 8417 5949 80 81 +3 8653 14475 80 81 +4 1808 23240 80 81 +5 3410 25074 80 81 diff --git a/t/data/test_analysis1122.yaml b/t/data/test_analysis1122.yaml new file mode 100644 index 0000000..4d11193 --- /dev/null +++ b/t/data/test_analysis1122.yaml @@ -0,0 +1,43 @@ +name: zmp_ph1 +chunk_total: 3 +read1_length: 30 +read2_length: 54 +mismatch_threshold: 2 +bin_size: 100 +peak_buffer_width: 100 +hmm_sig_level: 0.001 +hmm_binary: bin/quince_chiphmmnew +r_binary: R +deseq_script: script/run_deseq.R +output_sig_level: 0.05 +ref_fasta: t/data/test12.fa +ensembl_species: danio_rerio +samples: + - + name: zmp_ph1_1m + description: ZMP phenotype 1.1 mutant + condition: mutant + group: 1 + tag: NNNNBGAGGC + bam_file: t/data/test1.bam + - + name: zmp_ph1_1s + description: ZMP phenotype 1.1 sibling + condition: sibling + group: 1 + tag: NNNNBAGAAG + bam_file: t/data/test1.bam + - + name: zmp_ph1_2m + description: ZMP phenotype 1.2 mutant + condition: mutant + group: 2 + tag: NNNNBCAGAG + bam_file: t/data/test2.bam + - + name: zmp_ph1_2s + description: ZMP phenotype 1.2 sibling + condition: sibling + group: 2 + tag: NNNNBGCACG + bam_file: t/data/test2.bam diff --git a/t/data/test_analysis12.yaml b/t/data/test_analysis12.yaml new file mode 100644 index 0000000..d0adcba --- /dev/null +++ b/t/data/test_analysis12.yaml @@ -0,0 +1,28 @@ +name: zmp_ph1 +chunk_total: 3 +read1_length: 30 +read2_length: 54 +mismatch_threshold: 2 +bin_size: 100 +peak_buffer_width: 100 +hmm_sig_level: 0.001 +hmm_binary: bin/quince_chiphmmnew +r_binary: R +deseq_script: script/run_deseq.R +output_sig_level: 0.05 +ref_fasta: t/data/test12.fa +samples: + - + name: zmp_ph1_1m + description: ZMP phenotype 1.1 mutant + condition: mutant + group: 1 + tag: NNNNBGAGGC + bam_file: t/data/test1.bam + - + name: zmp_ph1_1s + description: ZMP phenotype 1.1 sibling + condition: sibling + group: 1 + tag: NNNNBCAGAG + bam_file: t/data/test2.bam diff --git a/t/data/test_analysis13.yaml b/t/data/test_analysis13.yaml new file mode 100644 index 0000000..78028f6 --- /dev/null +++ b/t/data/test_analysis13.yaml @@ -0,0 +1,28 @@ +name: zmp_ph1 +chunk_total: 3 +read1_length: 30 +read2_length: 54 +mismatch_threshold: 2 +bin_size: 100 +peak_buffer_width: 100 +hmm_sig_level: 0.001 +hmm_binary: bin/quince_chiphmmnew +r_binary: R +deseq_script: script/run_deseq.R +output_sig_level: 0.05 +ref_fasta: t/data/test12.fa +samples: + - + name: zmp_ph1_1m + description: ZMP phenotype 1.1 mutant + condition: mutant + group: 1 + tag: NNNNBGAGGC + bam_file: t/data/test1.bam + - + name: zmp_ph1_1s + description: ZMP phenotype 1.1 sibling + condition: sibling + group: 1 + tag: NNNNBCGCAA + bam_file: t/data/test3.bam diff --git a/t/data/test_de.yaml b/t/data/test_de.yaml new file mode 100644 index 0000000..4a47940 --- /dev/null +++ b/t/data/test_de.yaml @@ -0,0 +1,75 @@ +- + name: count_tags + default_memory: 3000 +- + name: bin_reads + default_memory: 3000 +- + name: get_read_peaks + default_memory: 3000 +- + name: merge_read_peaks + default_memory: 50 + prerequisites: + - get_read_peaks +- + name: summarise_read_peaks + default_memory: 200 + prerequisites: + - merge_read_peaks +- + name: run_peak_hmm + default_memory: 300 + prerequisites: + - bin_reads + - summarise_read_peaks +- + name: join_hmm_bins + default_memory: 50 + prerequisites: + - run_peak_hmm +- + name: get_three_prime_ends + default_memory: 1000 + prerequisites: + - join_hmm_bins +- + name: merge_three_prime_ends + default_memory: 50 + prerequisites: + - get_three_prime_ends +- + name: filter_three_prime_ends + default_memory: 50 + prerequisites: + - merge_three_prime_ends +- + name: choose_three_prime_end + default_memory: 50 + prerequisites: + - filter_three_prime_ends +- + name: count_reads + default_memory: 300 + prerequisites: + - choose_three_prime_end +- + name: merge_read_counts + default_memory: 50 + prerequisites: + - count_reads +- + name: run_deseq + default_memory: 2000 + prerequisites: + - merge_read_counts +- + name: add_gene_annotation + default_memory: 3000 + prerequisites: + - run_deseq +- + name: dump_as_table + default_memory: 3000 + prerequisites: + - add_gene_annotation diff --git a/t/gene.t b/t/gene.t new file mode 100644 index 0000000..57f1d56 --- /dev/null +++ b/t/gene.t @@ -0,0 +1,119 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 54; + +use DETCT::Gene; + +my $gene = DETCT::Gene->new( + { + genebuild_version => 'e61', + stable_id => 'ENSDARG00000095747', + biotype => 'protein_coding', + seq_name => '5', + start => 40352744, + end => 40354399, + strand => 1, + } +); + +isa_ok( $gene, 'DETCT::Gene' ); + +# Test genebuild version attribute +is( $gene->genebuild_version, 'e61', 'Get genebuild version' ); +is( $gene->set_genebuild_version('e62'), undef, 'Set genebuild version' ); +is( $gene->genebuild_version, 'e62', 'Get new genebuild version' ); +throws_ok { $gene->set_genebuild_version() } +qr/No genebuild version specified/ms, 'No genebuild version'; +throws_ok { $gene->set_genebuild_version('#invalid#') } +qr/Invalid genebuild version/ms, 'Invalid genebuild version'; + +# Test stable id attribute +is( $gene->stable_id, 'ENSDARG00000095747', 'Get stable id' ); +is( $gene->set_stable_id('ENSDARG00000024771'), undef, 'Set stable id' ); +is( $gene->stable_id, 'ENSDARG00000024771', 'Get new stable id' ); +throws_ok { $gene->set_stable_id() } qr/No stable id specified/ms, + 'No stable id'; +throws_ok { $gene->set_stable_id('#invalid#') } qr/Invalid stable id/ms, + 'Invalid stable id'; + +# Test name attribute +is( $gene->name, undef, 'Get name' ); +is( $gene->set_name('cxc64'), undef, 'Set name' ); +is( $gene->name, 'cxc64', 'Get new name' ); +is( $gene->set_name(), undef, 'Set undef name' ); +is( $gene->name, undef, 'Get undef name' ); +my $long_name = 'X' x ( $DETCT::Gene::MAX_NAME_LENGTH + 1 ); +throws_ok { $gene->set_name('') } qr/Name is empty/ms, 'Empty name'; +throws_ok { $gene->set_name($long_name) } qr/longer than \d+ characters/ms, + 'Invalid name'; + +# Test description attribute +is( $gene->description, undef, 'Get description' ); +is( $gene->set_description('CXC chemokine 64'), undef, 'Set description' ); +is( $gene->description, 'CXC chemokine 64', 'Get new description' ); +is( $gene->set_description(), undef, 'Set undef description' ); +is( $gene->description, undef, 'Get undef description' ); + +# Test biotype attribute +is( $gene->biotype, 'protein_coding', 'Get biotype' ); +is( $gene->set_biotype('nonsense_mediated_decay'), undef, 'Set biotype' ); +is( $gene->biotype, 'nonsense_mediated_decay', 'Get new biotype' ); +throws_ok { $gene->set_biotype() } qr/No biotype specified/ms, 'No biotype'; +throws_ok { $gene->set_biotype('#invalid#') } qr/Invalid biotype/ms, + 'Invalid biotype'; + +# Test sequence name attribute +is( $gene->seq_name, '5', 'Get sequence name' ); +is( $gene->set_seq_name('6'), undef, 'Set sequence name' ); +is( $gene->seq_name, '6', 'Get new sequence name' ); +throws_ok { $gene->set_seq_name() } qr/No sequence name specified/ms, + 'No sequence name'; +throws_ok { $gene->set_seq_name('#invalid#') } qr/Invalid sequence name/ms, + 'Invalid sequence name'; + +# Test start attribute +is( $gene->start, 40352744, 'Get start' ); +is( $gene->set_start(30352744), undef, 'Set start' ); +is( $gene->start, 30352744, 'Get new start' ); +throws_ok { $gene->set_start() } qr/No start specified/ms, 'No start'; +throws_ok { $gene->set_start(-1) } qr/Invalid start/ms, 'Invalid start'; + +# Test end attribute +is( $gene->end, 40354399, 'Get end' ); +is( $gene->set_end(30354399), undef, 'Set end' ); +is( $gene->end, 30354399, 'Get new end' ); +throws_ok { $gene->set_end() } qr/No end specified/ms, 'No end'; +throws_ok { $gene->set_end(-2) } qr/Invalid end/ms, 'Invalid end'; + +# Test strand attribute +is( $gene->strand, 1, 'Get strand' ); +is( $gene->set_strand(-1), undef, 'Set strand' ); +is( $gene->strand, -1, 'Get new strand' ); +throws_ok { $gene->set_strand() } qr/No strand specified/ms, 'No strand'; +throws_ok { $gene->set_strand(0) } qr/Invalid strand/ms, 'Invalid strand'; + +# Mock transcript objects +my $transcript1 = Test::MockObject->new(); +$transcript1->set_isa('DETCT::Transcript'); +my $transcript2 = Test::MockObject->new(); +$transcript2->set_isa('DETCT::Transcript'); + +# Test adding and retrieving transcripts +my $transcripts; +$transcripts = $gene->get_all_transcripts(); +is( scalar @{$transcripts}, 0, 'No transcripts' ); +is( $gene->add_transcript($transcript1), undef, 'Add transcript' ); +$transcripts = $gene->get_all_transcripts(); +is( scalar @{$transcripts}, 1, 'Get one transcript' ); +$gene->add_transcript($transcript2); +is( scalar @{$transcripts}, 2, 'Get two transcripts' ); +throws_ok { $gene->add_transcript() } qr/No transcript specified/ms, + 'No transcript specified'; +throws_ok { $gene->add_transcript('invalid') } qr/Class of transcript/ms, + 'Invalid transcript'; + diff --git a/t/genefinder.t b/t/genefinder.t new file mode 100644 index 0000000..ccd7a86 --- /dev/null +++ b/t/genefinder.t @@ -0,0 +1,311 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 93; + +use DETCT::GeneFinder; + +# Mock genes +my @genes; +my @transcript_three_prime_ends = + ( [ 100, 1 ], [ 200, 1 ], [ 300, -1 ], [ 400, -1 ], ); +foreach my $transcript_three_prime_end (@transcript_three_prime_ends) { + my ( $pos, $strand ) = @{$transcript_three_prime_end}; + + # Construct start and end so $pos is always 3' end + my $start = $strand == 1 ? $pos - 50 : $pos; + my $end = $strand == 1 ? $pos : $pos + 50; + + # Create genes named by 3' end position + my $gene = Test::MockObject->new(); + $gene->set_always( 'stable_id', 'ENSDARG00000095747' ); + $gene->set_always( 'external_name', q{g} . $pos . q{:} . $strand ); + $gene->set_always( 'description', undef ); + $gene->set_always( 'biotype', 'protein_coding' ); + $gene->set_always( 'seq_region_start', $start ); + $gene->set_always( 'seq_region_end', $end ); + $gene->set_always( 'seq_region_strand', $strand ); + my $transcript = Test::MockObject->new(); + $transcript->set_always( 'stable_id', 'ENSDART00000133571' ); + $transcript->set_always( 'external_name', q{t} . $pos . q{:} . $strand ); + $transcript->set_always( 'description', undef ); + $transcript->set_always( 'biotype', 'protein_coding' ); + $transcript->set_always( 'seq_region_start', $start ); + $transcript->set_always( 'seq_region_end', $end ); + $transcript->set_always( 'seq_region_strand', $strand ); + my $transcript_far = Test::MockObject->new(); + $transcript_far->set_always( 'stable_id', 'ENSDART00000133572' ); + $transcript_far->set_always( 'external_name', 'cxc64-001' ); + $transcript_far->set_always( 'description', undef ); + $transcript_far->set_always( 'biotype', 'protein_coding' ); + $transcript_far->set_always( 'seq_region_start', 100_000 ); + $transcript_far->set_always( 'seq_region_end', 100_100 ); + $transcript_far->set_always( 'seq_region_strand', $strand ); + $gene->set_always( 'get_all_Transcripts', + [ $transcript, $transcript_far ] ); + push @genes, $gene; +} + +# Mock slice +my $slice = Test::MockObject->new(); +$slice->set_always( 'get_all_Genes', \@genes ); + +# Mock slice adaptor +my $slice_adaptor = Test::MockObject->new(); +$slice_adaptor->set_isa('Bio::EnsEMBL::DBSQL::SliceAdaptor'); +$slice_adaptor->set_always( 'fetch_by_region', $slice ); + +my $gene_finder = + DETCT::GeneFinder->new( { slice_adaptor => $slice_adaptor, } ); + +isa_ok( $gene_finder, 'DETCT::GeneFinder' ); + +# Test Ensembl slice adaptor attribute +isa_ok( $gene_finder->slice_adaptor, 'Bio::EnsEMBL::DBSQL::SliceAdaptor' ); +throws_ok { $gene_finder->set_slice_adaptor() } +qr/No Ensembl slice adaptor specified/ms, 'No Ensembl slice adaptor'; +throws_ok { $gene_finder->set_slice_adaptor('invalid') } +qr/Class of Ensembl slice adaptor/ms, 'Invalid Ensembl slice adaptor'; + +my $genes; +my $transcripts; +my $distance; + +# Near to one gene on forward strand +( $genes, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_genes( '1', 110, 1 ); +is( $genes->[0]->name, 'g100:1', + q{Gene with 3' end at 100 bp on forward strand} ); +is( $distance, 10, q{3' end is 10 bp downstream} ); +is( $nearest_end_pos, 100, q{3' end at 100 bp} ); +( $genes, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_genes( '1', 90, 1 ); +is( $genes->[0]->name, 'g100:1', + q{Gene with 3' end at 100 bp on forward strand} ); +is( $distance, -10, q{3' end is 10 bp upstream} ); +is( $nearest_end_pos, 100, q{3' end at 100 bp} ); + +# Near to one gene on reverse strand +( $genes, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_genes( '1', 290, -1 ); +is( $genes->[0]->name, 'g300:-1', + q{Gene with 3' end at 300 bp on reverse strand} ); +is( $distance, 10, q{3' end is 10 bp downstream} ); +is( $nearest_end_pos, 300, q{3' end at 300 bp} ); +( $genes, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_genes( '1', 310, -1 ); +is( $genes->[0]->name, 'g300:-1', + q{Gene with 3' end at 300 bp on reverse strand} ); +is( $distance, -10, q{3' end is 10 bp upstream} ); +is( $nearest_end_pos, 300, q{3' end at 300 bp} ); + +# Between two genes on forward strand +( $genes, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_genes( '1', 150, 1 ); +is( $genes->[0]->name, 'g100:1', + q{Gene with 3' end at 100 bp on forward strand} ); +is( $distance, 50, q{3' end is 50 bp upstream} ); +is( $nearest_end_pos, 100, q{3' end at 100 bp} ); + +# Between two genes on reverse strand +( $genes, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_genes( '1', 350, -1 ); +is( $genes->[0]->name, 'g400:-1', + q{Gene with 3' end at 400 bp on reverse strand} ); +is( $distance, 50, q{3' end is 50 bp upstream} ); +is( $nearest_end_pos, 400, q{3' end at 400 bp} ); + +# Near to one transcript on forward strand +( $transcripts, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_transcripts( '1', 110, 1 ); +is( $transcripts->[0]->name, + 't100:1', q{Transcript with 3' end at 100 bp on forward strand} ); +is( $distance, 10, q{3' end is 10 bp downstream} ); +is( $nearest_end_pos, 100, q{3' end at 100 bp} ); +( $transcripts, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_transcripts( '1', 90, 1 ); +is( $transcripts->[0]->name, + 't100:1', q{Transcript with 3' end at 100 bp on forward strand} ); +is( $distance, -10, q{3' end is 10 bp upstream} ); +is( $nearest_end_pos, 100, q{3' end at 100 bp} ); + +# Near to one transcript on reverse strand +( $transcripts, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_transcripts( '1', 290, -1 ); +is( $transcripts->[0]->name, + 't300:-1', q{Transcript with 3' end at 300 bp on reverse strand} ); +is( $distance, 10, q{3' end is 10 bp downstream} ); +is( $nearest_end_pos, 300, q{3' end at 300 bp} ); +( $transcripts, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_transcripts( '1', 310, -1 ); +is( $transcripts->[0]->name, + 't300:-1', q{Transcript with 3' end at 300 bp on reverse strand} ); +is( $distance, -10, q{3' end is 10 bp upstream} ); +is( $nearest_end_pos, 300, q{3' end at 300 bp} ); + +# Between two transcripts on forward strand +( $transcripts, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_transcripts( '1', 150, 1 ); +is( $transcripts->[0]->name, + 't100:1', q{Transcript with 3' end at 100 bp on forward strand} ); +is( $distance, 50, q{3' end is 50 bp upstream} ); +is( $nearest_end_pos, 100, q{3' end at 100 bp} ); + +# Between two transcripts on reverse strand +( $transcripts, $distance, $nearest_end_pos ) = + $gene_finder->get_nearest_transcripts( '1', 350, -1 ); +is( $transcripts->[0]->name, + 't400:-1', q{Transcript with 3' end at 400 bp on reverse strand} ); +is( $distance, 50, q{3' end is 50 bp upstream} ); +is( $nearest_end_pos, 400, q{3' end at 400 bp} ); + +# Check adding gene annotation required parameters +throws_ok { $gene_finder->add_gene_annotation() } qr/No regions specified/ms, + 'No regions'; + +my $regions; + +# Adding gene annotation +$regions = [ + [ '1', 1, 1000, 10, -10, '1', 110, 1, 10, [], undef, undef, [], [] ], + [ '1', 1, 1000, 10, -10, '1', 290, -1, 10, [], undef, undef, [], [] ], + [ '1', 1, 1000, 10, -10, '1', 100, 1, 10, [], undef, undef, [], [] ], + [ '1', 1, 1000, 10, -10, '1', 300, -1, 10, [], undef, undef, [], [] ], +]; +my $annotated_regions = $gene_finder->add_gene_annotation($regions); +my ($gv) = keys %{ $annotated_regions->[0]->[-1] }; # Genebuild version varies +is( scalar keys %{ $annotated_regions->[0]->[-1] }, 1, '1 genebuild' ); +is( scalar @{ $annotated_regions->[0]->[-1]->{$gv} }, 1, '1 gene' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[0], + 'ENSDARG00000095747', 'Stable id' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[1], + 'g100:1', q{3' end as name} ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[2], undef, 'Description' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[3], + 'protein_coding', 'Biotype' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[4], 10, 'Distance downstream' ); +is( scalar @{ $annotated_regions->[0]->[-1]->{$gv}->[0]->[5] }, + 1, '1 transcript' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[5]->[0]->[0], + 'ENSDART00000133571', 'Transcript stable id' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[5]->[0]->[1], + 'protein_coding', 'Transcript biotype' ); +is( scalar keys %{ $annotated_regions->[1]->[-1] }, 1, '1 genebuild' ); +is( scalar @{ $annotated_regions->[1]->[-1]->{$gv} }, 1, '1 gene' ); +is( $annotated_regions->[1]->[-1]->{$gv}->[0]->[0], + 'ENSDARG00000095747', 'Stable id' ); +is( $annotated_regions->[1]->[-1]->{$gv}->[0]->[1], + 'g300:-1', q{3' end as name} ); +is( $annotated_regions->[1]->[-1]->{$gv}->[0]->[2], undef, 'Description' ); +is( $annotated_regions->[1]->[-1]->{$gv}->[0]->[3], + 'protein_coding', 'Biotype' ); +is( $annotated_regions->[1]->[-1]->{$gv}->[0]->[4], 10, 'Distance downstream' ); +is( scalar @{ $annotated_regions->[1]->[-1]->{$gv}->[0]->[5] }, + 1, '1 transcript' ); +is( $annotated_regions->[1]->[-1]->{$gv}->[0]->[5]->[0]->[0], + 'ENSDART00000133571', 'Transcript stable id' ); +is( $annotated_regions->[1]->[-1]->{$gv}->[0]->[5]->[0]->[1], + 'protein_coding', 'Transcript biotype' ); +is( scalar keys %{ $annotated_regions->[2]->[-1] }, 1, '1 genebuild' ); +is( scalar @{ $annotated_regions->[2]->[-1]->{$gv} }, 1, '1 gene' ); +is( $annotated_regions->[2]->[-1]->{$gv}->[0]->[0], + 'ENSDARG00000095747', 'Stable id' ); +is( $annotated_regions->[2]->[-1]->{$gv}->[0]->[1], + 'g100:1', q{3' end as name} ); +is( $annotated_regions->[2]->[-1]->{$gv}->[0]->[2], undef, 'Description' ); +is( $annotated_regions->[2]->[-1]->{$gv}->[0]->[3], + 'protein_coding', 'Biotype' ); +is( $annotated_regions->[2]->[-1]->{$gv}->[0]->[4], 0, 'Distance downstream' ); +is( scalar @{ $annotated_regions->[2]->[-1]->{$gv}->[0]->[5] }, + 1, '1 transcript' ); +is( $annotated_regions->[2]->[-1]->{$gv}->[0]->[5]->[0]->[0], + 'ENSDART00000133571', 'Transcript stable id' ); +is( $annotated_regions->[2]->[-1]->{$gv}->[0]->[5]->[0]->[1], + 'protein_coding', 'Transcript biotype' ); +is( scalar keys %{ $annotated_regions->[3]->[-1] }, 1, '1 genebuild' ); +is( scalar @{ $annotated_regions->[3]->[-1]->{$gv} }, 1, '1 gene' ); +is( $annotated_regions->[3]->[-1]->{$gv}->[0]->[0], + 'ENSDARG00000095747', 'Stable id' ); +is( $annotated_regions->[3]->[-1]->{$gv}->[0]->[1], + 'g300:-1', q{3' end as name} ); +is( $annotated_regions->[3]->[-1]->{$gv}->[0]->[2], undef, 'Description' ); +is( $annotated_regions->[3]->[-1]->{$gv}->[0]->[3], + 'protein_coding', 'Biotype' ); +is( $annotated_regions->[3]->[-1]->{$gv}->[0]->[4], 0, 'Distance downstream' ); +is( scalar @{ $annotated_regions->[3]->[-1]->{$gv}->[0]->[5] }, + 1, '1 transcript' ); +is( $annotated_regions->[3]->[-1]->{$gv}->[0]->[5]->[0]->[0], + 'ENSDART00000133571', 'Transcript stable id' ); +is( $annotated_regions->[3]->[-1]->{$gv}->[0]->[5]->[0]->[1], + 'protein_coding', 'Transcript biotype' ); + +# Mock genes all on one strand +@genes = (); +@transcript_three_prime_ends = + ( [ 100, 1 ], [ 200, 1 ], [ 300, 1 ], [ 400, 1 ], ); +foreach my $transcript_three_prime_end (@transcript_three_prime_ends) { + my ( $pos, $strand ) = @{$transcript_three_prime_end}; + + # Create genes named by 3' end position + my $gene = Test::MockObject->new(); + $gene->set_always( 'stable_id', 'ENSDARG00000095747' ); + $gene->set_always( 'external_name', q{g} . $pos . q{:} . $strand ); + $gene->set_always( 'description', undef ); + $gene->set_always( 'biotype', 'protein_coding' ); + $gene->set_always( 'seq_region_start', 1 ); + $gene->set_always( 'seq_region_end', $pos ); + $gene->set_always( 'seq_region_strand', $strand ); + my $transcript = Test::MockObject->new(); + $transcript->set_always( 'stable_id', 'ENSDART00000133571' ); + $transcript->set_always( 'external_name', 'cxc64-001' ); + $transcript->set_always( 'description', undef ); + $transcript->set_always( 'biotype', 'protein_coding' ); + $transcript->set_always( 'seq_region_start', $pos - 50 ); + $transcript->set_always( 'seq_region_end', $pos ); + $transcript->set_always( 'seq_region_strand', $strand ); + $gene->set_always( 'get_all_Transcripts', [$transcript] ); + push @genes, $gene; +} + +# Mock slice +$slice = Test::MockObject->new(); +$slice->set_always( 'get_all_Genes', \@genes ); + +# Mock slice adaptor +$slice_adaptor = Test::MockObject->new(); +$slice_adaptor->set_isa('Bio::EnsEMBL::DBSQL::SliceAdaptor'); +$slice_adaptor->set_always( 'fetch_by_region', $slice ); + +$gene_finder = DETCT::GeneFinder->new( { slice_adaptor => $slice_adaptor, } ); + +isa_ok( $gene_finder, 'DETCT::GeneFinder' ); + +# Adding gene annotation with genes only on one strand +$regions = [ + [ '1', 1, 1000, 10, -10, '1', 110, 1, 10, [], undef, undef, [], [] ], + [ '1', 1, 1000, 10, -10, '1', 290, -1, 10, [], undef, undef, [], [] ], +]; +my $annotated_regions = $gene_finder->add_gene_annotation($regions); +my ($gv) = keys %{ $annotated_regions->[0]->[-1] }; # Genebuild version varies +is( scalar keys %{ $annotated_regions->[0]->[-1] }, 1, '1 genebuild' ); +is( scalar @{ $annotated_regions->[0]->[-1]->{$gv} }, 1, '1 gene' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[0], + 'ENSDARG00000095747', 'Stable id' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[1], + 'g100:1', q{3' end as name} ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[2], undef, 'Description' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[3], + 'protein_coding', 'Biotype' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[4], 10, 'Distance downstream' ); +is( scalar @{ $annotated_regions->[0]->[-1]->{$gv}->[0]->[5] }, + 1, '1 transcript' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[5]->[0]->[0], + 'ENSDART00000133571', 'Transcript stable id' ); +is( $annotated_regions->[0]->[-1]->{$gv}->[0]->[5]->[0]->[1], + 'protein_coding', 'Transcript biotype' ); +is( scalar keys %{ $annotated_regions->[1]->[-1] }, + 0, 'No genes on reverse strand' ); diff --git a/t/misc-bam.t b/t/misc-bam.t new file mode 100644 index 0000000..86f41bf --- /dev/null +++ b/t/misc-bam.t @@ -0,0 +1,1652 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 323; + +use DETCT::Misc::BAM qw( + get_reference_sequence_lengths + get_sequence + count_tags + bin_reads + get_read_peaks + get_three_prime_ends + merge_three_prime_ends + filter_three_prime_ends + choose_three_prime_end + count_reads + merge_read_counts +); + +=for comment + +Test random BAM files can be regenerated using: + +perl script/make_test_sam.pl --seed 10 --seq_region_count 5 \ +--seq_region_max_length 10_000 --read_pair_count 100 \ +--read_tags NNNNBGAGGC NNNNBAGAAG | samtools view -bS - | samtools sort - test1 +perl script/make_test_sam.pl --seed 10 --seq_region_count 5 \ +--seq_region_max_length 10_000 --read_pair_count 100 \ +--read_tags NNNNBCAGAG NNNNBGCACG | samtools view -bS - | samtools sort - test2 +perl script/make_test_sam.pl --seed 20 --seq_region_count 5 \ +--seq_region_max_length 10_000 --read_pair_count 100 \ +--read_tags NNNNBCGCAA NNNNBCAAGA | samtools view -bS - | samtools sort - test3 +ls *.bam | xargs -n1 samtools index +mv test* t/data/ + +Some numbers in tests below will then need updating. Code to generate numbers +(using independent methods) is given before each test. + +Test random FASTA files can be regenerated using: + +perl script/make_test_fasta.pl --seed 10 --seq_region_count 5 \ +--seq_region_max_length 10_000 > test12.fa +perl script/make_test_fasta.pl --seed 20 --seq_region_count 5 \ +--seq_region_max_length 10_000 > test3.fa +ls *.fa | xargs -n1 samtools faidx +mv test* t/data/ + +=cut + +# Check reference sequence length returned by test BAM file +throws_ok { get_reference_sequence_lengths() } qr/No BAM file specified/ms, + 'No BAM file'; +my %bam_length = get_reference_sequence_lengths('t/data/test1.bam'); +is( $bam_length{1}, 8789, 'Chr 1 length' ); +is( $bam_length{2}, 7958, 'Chr 2 length' ); +is( $bam_length{3}, 4808, 'Chr 3 length' ); + +# Check getting sequence from test FASTA file +# First 10 bp of chromosome 1 should be CCAGGCGCGG according to: + +=for comment +head -2 t/data/test12.fa +=cut + +throws_ok { + get_sequence( + { + seq_name => '1', + start => 1, + end => 10, + strand => 1, + } + ); +} +qr/No FASTA index or FASTA file specified/ms, 'No FASTA index or file'; +throws_ok { + get_sequence( + { + ref_fasta => 't/data/test12.fa', + start => 1, + end => 10, + strand => 1, + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + get_sequence( + { + ref_fasta => 't/data/test12.fa', + seq_name => '1', + end => 10, + strand => 1, + } + ); +} +qr/No sequence start specified/ms, 'No sequence start'; +throws_ok { + get_sequence( + { + ref_fasta => 't/data/test12.fa', + seq_name => '1', + start => 1, + strand => 1, + } + ); +} +qr/No sequence end specified/ms, 'No sequence end'; +throws_ok { + get_sequence( + { + ref_fasta => 't/data/test12.fa', + seq_name => '1', + start => 1, + end => 10, + } + ); +} +qr/No sequence strand specified/ms, 'No sequence strand'; +my $seq; +$seq = get_sequence( + { + ref_fasta => 't/data/test12.fa', + seq_name => '1', + start => 1, + end => 10, + strand => 1, + } +); +is( length $seq, 10, 'Subsequence length' ); +is( $seq, 'CCAGGCGCGG', 'Subsequence' ); +$seq = get_sequence( + { + ref_fasta => 't/data/test12.fa', + seq_name => '1', + start => 1, + end => 10, + strand => -1, + } +); +is( length $seq, 10, 'Reverse complement subsequence length' ); +is( $seq, 'CCGCGCCTGG', 'Reverse complement subsequence' ); + +# Check counting tags required parameters +throws_ok { + count_tags( + { + mismatch_threshold => 2, + seq_name => '1', + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No BAM file specified/ms, 'No BAM file'; +throws_ok { + count_tags( + { + bam_file => 't/data/test1.bam', + seq_name => '1', + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No mismatch threshold specified/ms, 'No mismatch threshold'; +throws_ok { + count_tags( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 2, + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + count_tags( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 2, + seq_name => '1', + } + ); +} +qr/No tags specified/ms, 'No tags'; + +my $count; + +# Check tag counts returned by chromosome 1 of test BAM file +# Should be 50 random tags according to: + +=for comment +samtools view -f 128 -F 1028 t/data/test1.bam 1 | awk '{ print $1 }' \ +| sed -e 's/.*#//' | grep GAGGC$ | sort -u | wc -l +=cut + +$count = count_tags( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 100, + seq_name => '1', + tags => ['NNNNBGAGGC'], + } +); +is( scalar keys %{$count}, 1, '1 tag' ); +is( scalar keys %{ $count->{NNNNBGAGGC} }, 50, '50 random tags' ); + +# Check tag counts returned in 1000 bp onwards of chromosome 1 of test BAM file +# Should be 45 random tags according to: + +=for comment +samtools view -f 128 -F 1028 t/data/test1.bam 1:1000 | awk '{ print $1 }' \ +| sed -e 's/.*#//' | grep GAGGC$ | sort -u | wc -l +=cut + +$count = count_tags( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 100, + seq_name => '1', + start => 1000, + tags => ['NNNNBGAGGC'], + } +); +is( scalar keys %{$count}, 1, '1 tag' ); +is( scalar keys %{ $count->{NNNNBGAGGC} }, 45, '45 random tags' ); + +# Check tag counts returned in first 1000 bp of chromosome 1 of test BAM file +# Should be 6 random tags according to: + +=for comment +samtools view -f 128 -F 1028 t/data/test1.bam 1:1-1000 | awk '{ print $1 }' \ +| sed -e 's/.*#//' | grep GAGGC$ | sort -u | wc -l +=cut + +$count = count_tags( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 100, + seq_name => '1', + start => 1, + end => 1000, + tags => ['NNNNBGAGGC'], + } +); +is( scalar keys %{$count}, 1, '1 tag' ); +is( scalar keys %{ $count->{NNNNBGAGGC} }, 6, '6 random tags' ); + +# Check tag counts returned with low mismatch threshold +# Should be 13 random tags according to: + +=for comment +samtools view -f 128 -F 1028 t/data/test1.bam 1 | grep NM:i:0 \ +| awk '{ if ($6 == "54M") print $1 }' \ +| sed -e 's/.*#//' | grep GAGGC$ | sort -u | wc -l +=cut + +$count = count_tags( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + seq_name => '1', + tags => ['NNNNBGAGGC'], + } +); +is( scalar keys %{$count}, 1, '1 tag' ); +is( scalar keys %{ $count->{NNNNBGAGGC} }, 13, '13 random tags' ); + +# Check binning reads required parameters +throws_ok { + bin_reads( + { + mismatch_threshold => 2, + bin_size => 100, + seq_name => '1', + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No BAM file specified/ms, 'No BAM file'; +throws_ok { + bin_reads( + { + bam_file => 't/data/test1.bam', + bin_size => 100, + seq_name => '1', + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No mismatch threshold specified/ms, 'No mismatch threshold'; +throws_ok { + bin_reads( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 2, + seq_name => '1', + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No bin size specified/ms, 'No bin size'; +throws_ok { + bin_reads( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 2, + bin_size => 100, + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + bin_reads( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 2, + bin_size => 100, + seq_name => '1', + } + ); +} +qr/No tags specified/ms, 'No tags'; + +# Check read bins returned by test BAM file +# Should be 35 bins according to: + +=for comment +(samtools view -f 16 -F 1028 t/data/test1.bam 2 | grep 54M | grep NM:i:0 \ +| awk '{ print ($4) / 100 "\t" ($4 + 53 - 1) / 100 }'; \ +samtools view -f 32 -F 1028 t/data/test1.bam 2 | grep 54M | grep NM:i:0 \ +| awk '{ print ($4) / 100 "\t" ($4 + 53 - 1) / 100 }') \ +| sed -e 's/\.[0-9]*//g' \ +| awk '{ if ($1 == $2) print $1; else print $1 "\n" $2 }' \ +| sort | uniq -c | wc -l +=cut + +$count = bin_reads( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + bin_size => 100, + seq_name => '2', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } +); +is( scalar keys %{$count}, 1, '1 sequence' ); +is( scalar keys %{ $count->{'2'} }, 35, '35 bins' ); + +# Check read bins returned with non-existent tag +$count = bin_reads( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + bin_size => 100, + seq_name => '1', + tags => ['NNNNTTTTTT'], + } +); +is( scalar keys %{$count}, 1, '1 sequence' ); +is( scalar keys %{ $count->{'1'} }, 0, '0 bins' ); + +# Check getting read peaks required parameters +throws_ok { + get_read_peaks( + { + mismatch_threshold => 0, + peak_buffer_width => 100, + seq_name => '1', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } + ); +} +qr/No BAM file specified/ms, 'No BAM file'; +throws_ok { + get_read_peaks( + { + bam_file => 't/data/test1.bam', + peak_buffer_width => 100, + seq_name => '1', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } + ); +} +qr/No mismatch threshold specified/ms, 'No mismatch threshold'; +throws_ok { + get_read_peaks( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + seq_name => '1', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } + ); +} +qr/No peak buffer width specified/ms, 'No peak buffer width'; +throws_ok { + get_read_peaks( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + peak_buffer_width => 100, + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + get_read_peaks( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + peak_buffer_width => 100, + seq_name => '1', + } + ); +} +qr/No tags specified/ms, 'No tags'; + +my $peaks; + +# Check read peaks returned by test BAM file +# First peak should be 262 - 350 (2 reads) according to: + +=for comment +samtools view -f 128 -F 1028 t/data/test1.bam 2 | grep 54M | grep NM:i:0 \ +| awk '{ print $4 "\t" $4 + 53 }' | head -4 +=cut + +# Last peak should be 7399 - 7452 (1 read) according to: + +=for comment +samtools view -f 128 -F 1028 t/data/test1.bam 2 | grep 54M | grep NM:i:0 \ +| awk '{ print $4 "\t" $4 + 53 }' | tail -4 +=cut + +$peaks = get_read_peaks( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + peak_buffer_width => 100, + seq_name => '2', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } +); +is( scalar keys %{$peaks}, 1, '1 sequence' ); +is( $peaks->{'2'}->[0]->[0], 262, 'Start of first peak' ); +is( $peaks->{'2'}->[0]->[1], 350, 'End of first peak' ); +is( $peaks->{'2'}->[0]->[2], 2, 'First peak read count' ); +is( $peaks->{'2'}->[-1]->[0], 7399, 'Start of last peak' ); +is( $peaks->{'2'}->[-1]->[1], 7452, 'End of last peak' ); +is( $peaks->{'2'}->[-1]->[2], 1, 'Last peak read count' ); + +# Check read peaks returned by test BAM file +# First peak should be 78 - 131 (1 read) according to: + +=for comment +samtools view -f 128 -F 1028 t/data/test2.bam 1 | grep 54M | grep NM:i:0 \ +| awk '{ print $4 "\t" $4 + 53 }' | head -4 +=cut + +# Last peak should be 8666 - 8719 (1 read) according to: + +=for comment +samtools view -f 128 -F 1028 t/data/test2.bam 1 | grep 54M | grep NM:i:0 \ +| awk '{ print $4 "\t" $4 + 53 }' | tail -4 +=cut + +$peaks = get_read_peaks( + { + bam_file => 't/data/test2.bam', + mismatch_threshold => 0, + peak_buffer_width => 100, + seq_name => '1', + tags => [ 'NNNNBCAGAG', 'NNNNBGCACG' ], + } +); +is( scalar keys %{$peaks}, 1, '1 sequence' ); +is( $peaks->{'1'}->[0]->[0], 78, 'Start of first peak' ); +is( $peaks->{'1'}->[0]->[1], 131, 'End of first peak' ); +is( $peaks->{'1'}->[0]->[2], 1, 'First peak read count' ); +is( $peaks->{'1'}->[-1]->[0], 8666, 'Start of last peak' ); +is( $peaks->{'1'}->[-1]->[1], 8719, 'End of last peak' ); +is( $peaks->{'1'}->[-1]->[2], 1, 'Last peak read count' ); + +# Check read peaks returned with non-existent tag +$peaks = get_read_peaks( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + peak_buffer_width => 100, + seq_name => '1', + tags => ['NNNNTTTTTT'], + } +); +is( scalar keys %{$peaks}, 1, '1 sequence' ); +is( scalar @{ $peaks->{'1'} }, 0, '0 peaks' ); + +# Check getting 3' ends required parameters +throws_ok { + get_three_prime_ends( + { + mismatch_threshold => 0, + seq_name => '1', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + regions => [ [ 1, 1000, 10, -10 ] ], + } + ); +} +qr/No BAM file specified/ms, 'No BAM file'; +throws_ok { + get_three_prime_ends( + { + bam_file => 't/data/test1.bam', + seq_name => '1', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + regions => [ [ 1, 1000, 10, -10 ] ], + } + ); +} +qr/No mismatch threshold specified/ms, 'No mismatch threshold'; +throws_ok { + get_three_prime_ends( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + regions => [ [ 1, 1000, 10, -10 ] ], + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + get_three_prime_ends( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + seq_name => '1', + regions => [ [ 1, 1000, 10, -10 ] ], + } + ); +} +qr/No tags specified/ms, 'No tags'; +throws_ok { + get_three_prime_ends( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + seq_name => '1', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } + ); +} +qr/No regions specified/ms, 'No regions'; + +my $three_prime_ends; + +# Check 3' ends returned in first 2000 bp of chromosome 1 of test BAM file +# Should be 9 3' ends according to: + +=for comment +(samtools view -f 160 -F 1036 t/data/test1.bam 1:1-2000 \ +| grep NM:i:0 | grep 54M | awk '{ print "1:" $8 + 29 ":1" }'; \ +samtools view -f 128 -F 1068 t/data/test1.bam 1:1-2000 \ +| grep NM:i:0 | grep 54M | awk '{ print "1:" $8 ":-1" }') \ +| wc -l +=cut + +# One forward strand 3' end should be 1:2642:1 with 1 read according to: + +=for comment +samtools view -f 160 -F 1036 t/data/test1.bam 1:1-2000 \ +| grep NM:i:0 | grep 54M | awk '{ print "1:" $8 + 29 ":1" }' | sort | uniq -c +=cut + +# One reverse strand 3' end should be 1:632:-1 with 1 read according to: + +=for comment +samtools view -f 128 -F 1068 t/data/test1.bam 1:1-2000 \ +| grep NM:i:0 | grep 54M | awk '{ print "1:" $8 ":-1" }' | sort | uniq -c +=cut + +$three_prime_ends = get_three_prime_ends( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + seq_name => '1', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + regions => [ [ 1, 2000, 10, -10 ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( scalar @{ $three_prime_ends->{'1'}->[0]->[4] }, 9, q{9 3' ends} ); +my $got_forward = 0; +my $got_reverse = 0; +foreach my $three_prime_end ( @{ $three_prime_ends->{'1'}->[0]->[4] } ) { + my ( $seq, $pos, $strand, $read_count ) = @{$three_prime_end}; + my $string_form = join q{:}, $seq, $pos, $strand; + if ( $string_form eq '1:2642:1' ) { + $got_forward = 1; + } + if ( $string_form eq '1:632:-1' ) { + $got_reverse = 1; + } +} +ok( $got_forward, q{1 forward strand 3' end} ); +ok( $got_reverse, q{1 reverse strand 3' end} ); + +# Get 3' ends returned with non-existent tag +$three_prime_ends = get_three_prime_ends( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + seq_name => '1', + tags => ['NNNNTTTTTT'], + regions => [ [ 1, 2000, 10, -10 ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( scalar @{ $three_prime_ends->{'1'}->[0]->[4] }, 0, q{0 3' ends} ); + +# Get 3' ends for sequence name with a peak +$three_prime_ends = get_three_prime_ends( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + seq_name => '3', + tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + regions => [ [ 1, 10000, 10, -10 ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'3'} }, 1, '1 region' ); +my $max_read_count = 0; +foreach my $three_prime_end ( @{ $three_prime_ends->{'3'}->[0]->[4] } ) { + my ( $seq, $pos, $strand, $read_count ) = @{$three_prime_end}; + if ( $read_count > $max_read_count ) { + $max_read_count = $read_count; + } +} +ok( $max_read_count > 1, q{Read count for 3' end of peak} ); + +# Check merging 3' ends required parameters +throws_ok { + merge_three_prime_ends( { regions => [ [ [ 1, 1000, 10, -10, [] ] ] ], } ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + merge_three_prime_ends( { seq_name => '1', } ); +} +qr/No regions specified/ms, 'No regions'; + +# Test lists with different number of regions +throws_ok { + merge_three_prime_ends( + { + seq_name => '1', + regions => [ + [ [ 1, 1000, 10, -10, [] ], ], + [ [ 1, 1000, 10, -10, [] ], [ 2000, 3000, 10, -10, [] ], ], + ], + } + ); +} +qr/Number of regions does not match in all lists/ms, + 'Different number of regions'; + +# Test lists with different regions +throws_ok { + merge_three_prime_ends( + { + seq_name => '1', + regions => [ + [ [ 1, 1000, 10, -10, [] ], [ 2000, 4000, 10, -10, [] ], ], + [ [ 1, 1000, 10, -10, [] ], [ 3000, 4000, 10, -10, [] ], ], + ], + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + 'Different region start'; +throws_ok { + merge_three_prime_ends( + { + seq_name => '1', + regions => [ + [ [ 1, 1000, 10, -10, [] ], [ 2000, 4000, 10, -10, [] ], ], + [ [ 1, 1000, 10, -10, [] ], [ 2000, 5000, 10, -10, [] ], ], + ], + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + 'Different region end'; +throws_ok { + merge_three_prime_ends( + { + seq_name => '1', + regions => [ + [ [ 1, 1000, 10, -10, [] ], [ 2000, 4000, 10, -10, [] ], ], + [ [ 1, 1000, 10, -10, [] ], [ 2000, 4000, 20, -10, [] ], ], + ], + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + 'Different region maximum read count'; +throws_ok { + merge_three_prime_ends( + { + seq_name => '1', + regions => [ + [ [ 1, 1000, 10, -10, [] ], [ 2000, 4000, 10, -10, [] ], ], + [ [ 1, 1000, 10, -10, [] ], [ 2000, 4000, 10, -20, [] ], ], + ], + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + 'Different region log probability sum'; + +# Test one list of regions +$three_prime_ends = merge_three_prime_ends( + { + seq_name => '1', + regions => [ [ [ 1, 1000, 10, -10, [] ] ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( @{ $three_prime_ends->{'1'}->[0]->[4] }, 0, q{No 3' ends} ); + +# Test two lists of regions +$three_prime_ends = merge_three_prime_ends( + { + seq_name => '1', + regions => [ + [ + [ + 1, 1000, 10, -10, + [ [ '1', 2000, 1, 10 ], [ '1', 3000, 1, 10 ], ] + ] + ], + [ [ 1, 1000, 10, -10, [ [ '1', 3000, 1, 10 ], ] ] ], + ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( @{ $three_prime_ends->{'1'}->[0]->[4] }, 2, q{2 3' ends} ); +is( $three_prime_ends->{'1'}->[0]->[4]->[0]->[0], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[4]->[0]->[1], 3000, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[4]->[0]->[2], 1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[4]->[0]->[3], 20, q{3' end read count} ); + +# Test different strands +$three_prime_ends = merge_three_prime_ends( + { + seq_name => '1', + regions => [ + [ [ 1, 1000, 10, -10, [ [ '1', 2000, 1, 10 ], ] ] ], + [ [ 1, 1000, 10, -10, [ [ '1', 2000, -1, 10 ], ] ] ], + ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( @{ $three_prime_ends->{'1'}->[0]->[4] }, 2, q{2 3' ends} ); + +my $analysis; + +# Mock analysis object returning non-polyA +$analysis = Test::MockObject->new(); +$analysis->set_isa('DETCT::Analysis'); +$analysis->set_always( 'get_subsequence', 'TTTTTTTTTT' ); + +# Check filtering 3' ends required parameters +throws_ok { + filter_three_prime_ends( + { + seq_name => '1', + regions => [ [ [ 1, 1000, 10, -10, [] ] ] ], + } + ); +} +qr/No analysis specified/ms, 'No analysis'; +throws_ok { + filter_three_prime_ends( + { + analysis => $analysis, + regions => [ [ [ 1, 1000, 10, -10, [] ] ] ], + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + filter_three_prime_ends( + { + analysis => $analysis, + seq_name => '1', + } + ); +} +qr/No regions specified/ms, 'No regions'; + +# Test filtering 3' ends +$three_prime_ends = filter_three_prime_ends( + { + analysis => $analysis, + seq_name => '1', + regions => [ + [ + 1, 1000, 10, -10, + [ + [ '1', 1000, 1, 20 ], + [ '1', 2000, -1, 10 ], + [ '1', 3000, 1, 1 ], + [ '1', 4000, 1, 3 ], + ] + ] + ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( @{ $three_prime_ends->{'1'}->[0]->[4] }, 2, q{2 3' ends} ); +is( $three_prime_ends->{'1'}->[0]->[4]->[0]->[0], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[4]->[0]->[1], 1000, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[4]->[0]->[2], 1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[4]->[0]->[3], 20, q{3' end read count} ); + +# Mock analysis object returning polyA +$analysis = Test::MockObject->new(); +$analysis->set_isa('DETCT::Analysis'); +$analysis->set_always( 'get_subsequence', 'AAAATTTTTT' ); + +# Test filtering 3' ends +$three_prime_ends = filter_three_prime_ends( + { + analysis => $analysis, + seq_name => '1', + regions => [ + [ + 1, 1000, 10, -10, + [ + [ '1', 1000, 1, 20 ], + [ '1', 2000, -1, 10 ], + [ '1', 3000, 1, 1 ], + [ '1', 4000, 1, 3 ], + ] + ] + ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( @{ $three_prime_ends->{'1'}->[0]->[4] }, 0, q{0 3' ends} ); + +# Check choosing 3' end required parameters +throws_ok { + choose_three_prime_end( { regions => [ [ [ 1, 1000, 10, -10, [] ] ] ], } ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + choose_three_prime_end( { seq_name => '1', } ); +} +qr/No regions specified/ms, 'No regions'; + +# Test choosing 3' end +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ + [ + 1, 1000, 10, -10, + [ + [ '1', 1000, 1, 20 ], + [ '1', 2000, -1, 10 ], + [ '1', 3000, 1, 1 ], + [ '1', 4000, 1, 3 ], + ] + ] + ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 1000, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], 1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); + +# Test choosing 3' end with no 3' ends +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ [ 1, 1000, 10, -10, [] ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], undef, q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], undef, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], undef, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], undef, q{3' end read count} ); + +# Test choosing 3' end with reduced region end +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ [ 1, 1000, 10, -10, [ [ '1', 900, 1, 20 ], ] ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 900, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 900, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], 1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); + +# Test choosing 3' end with reduced region start +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ [ 1, 1000, 10, -10, [ [ '1', 100, -1, 20 ], ] ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 100, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 100, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], -1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); + +# Test choosing 3' end with different sequence name +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ [ 1, 1000, 10, -10, [ [ '2', 100, -1, 20 ], ] ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '2', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 100, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], -1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); + +# Test choosing 3' end beyond region start +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ [ 1000, 2000, 10, -10, [ [ '1', 900, 1, 20 ], ] ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1000, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 2000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 900, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], 1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ [ 1000, 2000, 10, -10, [ [ '1', 900, -1, 20 ], ] ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1000, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 2000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 900, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], -1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); + +# Test choosing 3' end beyond region end +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ [ 1000, 2000, 10, -10, [ [ '1', 2100, -1, 20 ], ] ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1000, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 2000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 2100, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], -1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ [ 1000, 2000, 10, -10, [ [ '1', 2100, 1, 20 ], ] ] ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1000, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 2000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 2100, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], 1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); + +# Test choosing 3' end with same read count +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ + [ + 1000, 2000, 10, -10, + [ [ '1', 900, -1, 20 ], [ '1', 2200, -1, 20 ], ] + ] + ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1000, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 2000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 900, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], -1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ + [ + 1000, 2000, 10, -10, + [ [ '1', 900, -1, 20 ], [ '1', 2100, -1, 20 ], ] + ] + ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1000, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 2000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[6], -1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); +$three_prime_ends = choose_three_prime_end( + { + seq_name => '1', + regions => [ + [ + 1000, 2000, 10, -10, + [ [ '2', 900, -1, 20 ], [ '2', 2100, -1, 20 ], ] + ] + ], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1000, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 2000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '2', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[6], -1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 20, q{3' end read count} ); + +# Test checking for polyA +is( DETCT::Misc::BAM::is_polya('TTTTTTTTTT'), 0, 'PolyT' ); +is( DETCT::Misc::BAM::is_polya('AAAATTTTTT'), 1, '>3 As at start' ); +is( DETCT::Misc::BAM::is_polya('TTTTTTAAAA'), 0, '>3 As at end' ); +is( DETCT::Misc::BAM::is_polya('TAAAATAAAT'), 1, '>6 As' ); +is( DETCT::Misc::BAM::is_polya('AAATAAATTT'), 1, 'AAA.AAA... regexp' ); +is( DETCT::Misc::BAM::is_polya('AAATAATATT'), 1, 'AAA.AA.A.. regexp' ); +is( DETCT::Misc::BAM::is_polya('AAATATAATT'), 1, 'AAA.A.AA.. regexp' ); +is( DETCT::Misc::BAM::is_polya('AATAAAATTT'), 1, 'AA.AAAA... regexp' ); +is( DETCT::Misc::BAM::is_polya('AATAAATATT'), 1, 'AA.AAA.A.. regexp' ); +is( DETCT::Misc::BAM::is_polya('AATATAAATT'), 1, 'AA.A.AAA.. regexp' ); +is( DETCT::Misc::BAM::is_polya('ATAAAAATTT'), 1, 'A.AAAAA... regexp' ); +is( DETCT::Misc::BAM::is_polya('ATAAAATATT'), 1, 'A.AAAA.A.. regexp' ); +is( DETCT::Misc::BAM::is_polya('ATAAATAATT'), 1, 'A.AAA.AA.. regexp' ); +is( DETCT::Misc::BAM::is_polya('ATAATAAATT'), 1, 'A.AA.AAA.. regexp' ); +is( DETCT::Misc::BAM::is_polya('ATATAAAATT'), 1, 'A.A.AAAA.. regexp' ); +is( DETCT::Misc::BAM::is_polya('AATAATAATT'), 1, 'AA.AA.AA.. regexp' ); +is( DETCT::Misc::BAM::is_polya('TATAATAATA'), 0, '6 As' ); + +# Check counting reads required parameters +throws_ok { + count_reads( + { + mismatch_threshold => 2, + seq_name => '1', + regions => [ [ 1000, 2000, 10, -10, '1', 2000, 1, 10 ], ], + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No BAM file specified/ms, 'No BAM file'; +throws_ok { + count_reads( + { + bam_file => 't/data/test1.bam', + seq_name => '1', + regions => [ [ 1000, 2000, 10, -10, '1', 2000, 1, 10 ], ], + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No mismatch threshold specified/ms, 'No mismatch threshold'; +throws_ok { + count_reads( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 2, + regions => [ [ 1000, 2000, 10, -10, '1', 2000, 1, 10 ], ], + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + count_reads( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 2, + seq_name => '1', + tags => ['NNNNBGAGGC'], + } + ); +} +qr/No regions specified/ms, 'No regions'; +throws_ok { + count_reads( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + seq_name => '1', + regions => [ [ 1, 2000, 10, -10, '1', 2000, 1, 10 ], ], + } + ); +} +qr/No tags specified/ms, 'No tags'; + +# Check read counts returned by test BAM file +# Should be 11 reads according to: + +=for comment +samtools view -f 128 -F 1028 t/data/test1.bam 1:1-2000 \ +| grep 54M | grep NM:i:0 | awk '{ print $1 }' \ +| sed -e 's/.*#//' | grep GAGGC$ | wc -l +=cut + +$three_prime_ends = count_reads( + { + bam_file => 't/data/test1.bam', + mismatch_threshold => 0, + seq_name => '1', + regions => [ [ 1, 2000, 10, -10, '1', 2000, 1, 10 ], ], + tags => ['NNNNBGAGGC'], + } +); +is( scalar keys %{$three_prime_ends}, 1, '1 sequence' ); +is( scalar @{ $three_prime_ends->{'1'} }, 1, '1 region' ); +is( $three_prime_ends->{'1'}->[0]->[0], 1, 'Region start' ); +is( $three_prime_ends->{'1'}->[0]->[1], 2000, 'Region end' ); +is( $three_prime_ends->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $three_prime_ends->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $three_prime_ends->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $three_prime_ends->{'1'}->[0]->[5], 2000, q{3' end position} ); +is( $three_prime_ends->{'1'}->[0]->[6], 1, q{3' end strand} ); +is( $three_prime_ends->{'1'}->[0]->[7], 10, q{3' end read count} ); +is( scalar keys %{ $three_prime_ends->{'1'}->[0]->[8] }, 1, '1 tag' ); +is( $three_prime_ends->{'1'}->[0]->[8]->{NNNNBGAGGC}, 4, '4 reads' ); + +# Mock sample objects +my $sample1 = Test::MockObject->new(); +$sample1->set_isa('DETCT::Sample'); +$sample1->set_always( 'bam_file', '1.bam' ); +$sample1->set_always( 'tag', 'AA' ); +my $sample2 = Test::MockObject->new(); +$sample2->set_isa('DETCT::Sample'); +$sample2->set_always( 'bam_file', '2.bam' ); +$sample2->set_always( 'tag', 'TT' ); +my $samples = [ $sample1, $sample2 ]; + +# Check merging read counts required parameters +throws_ok { + merge_read_counts( + { + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + merge_read_counts( + { + seq_name => '1', + samples => $samples, + } + ); +} +qr/No regions specified/ms, 'No regions'; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + }, + } + ); +} +qr/No samples specified/ms, 'No samples'; + +# Test lists with different number of regions +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => [ + [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ], + [ 3000, 4000, 10, -10, '1', 5000, 1, 10, { AA => 10 } ], + ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Number of regions does not match in all lists/ms, + 'Different number of regions'; + +# Test lists with different regions +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 2, 1000, 10, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + 'Different region start'; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1001, 10, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + 'Different region end'; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 11, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + 'Different region maximum read count'; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -11, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + 'Different region log probability sum'; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '2', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{Different 3' end sequence}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2001, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{Different 3' end position}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, -1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{Different 3' end strand}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 11, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{Different 3' end read count}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, undef, 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{3' end sequence undefined}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, undef, 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{Other 3' end sequence undefined}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', undef, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{3' end position undefined}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', undef, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{Other 3' end position undefined}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, undef, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{3' end strand undefined}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, undef, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{Other 3' end strand undefined}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, undef, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{3' end read count undefined}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, undef, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Regions not in the same order or not the same in each list/ms, + q{Other 3' end read count undefined}; + +# Test unknown BAM file and/or tag +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '3.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Unknown BAM file/ms, q{BAM file not in samples}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { CC => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Unknown BAM file/ms, q{Tag not in samples}; +throws_ok { + merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { TT => 10 } ] ], + '2.bam' => + [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + }, + samples => $samples, + } + ); +} +qr/Unknown BAM file/ms, q{Combination of BAM file and tag not in samples}; + +my $read_counts; + +$read_counts = merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { AA => 10 } ] ], + '2.bam' => [ [ 1, 1000, 10, -10, '1', 2000, 1, 10, { TT => 20 } ] ], + }, + samples => $samples, + } +); +is( scalar keys %{$read_counts}, 1, '1 sequence' ); +is( scalar @{ $read_counts->{'1'} }, 1, '1 region' ); +is( $read_counts->{'1'}->[0]->[0], 1, 'Region start' ); +is( $read_counts->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $read_counts->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $read_counts->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $read_counts->{'1'}->[0]->[4], '1', q{3' end sequence} ); +is( $read_counts->{'1'}->[0]->[5], 2000, q{3' end position} ); +is( $read_counts->{'1'}->[0]->[6], 1, q{3' end strand} ); +is( $read_counts->{'1'}->[0]->[7], 10, q{3' end read count} ); +is( scalar @{ $read_counts->{'1'}->[0]->[8] }, 2, '2 samples' ); +is( $read_counts->{'1'}->[0]->[8]->[0], 10, '10 reads' ); +is( $read_counts->{'1'}->[0]->[8]->[1], 20, '20 reads' ); + +$read_counts = merge_read_counts( + { + seq_name => '1', + regions => { + '1.bam' => [ + [ 1, 1000, 10, -10, undef, undef, undef, undef, { AA => 10 } ] + ], + '2.bam' => [ + [ 1, 1000, 10, -10, undef, undef, undef, undef, { TT => 20 } ] + ], + }, + samples => $samples, + } +); +is( scalar keys %{$read_counts}, 1, '1 sequence' ); +is( scalar @{ $read_counts->{'1'} }, 1, '1 region' ); +is( $read_counts->{'1'}->[0]->[0], 1, 'Region start' ); +is( $read_counts->{'1'}->[0]->[1], 1000, 'Region end' ); +is( $read_counts->{'1'}->[0]->[2], 10, 'Region maximum read count' ); +is( $read_counts->{'1'}->[0]->[3], -10, 'Region log probability sum' ); +is( $read_counts->{'1'}->[0]->[4], undef, q{3' end sequence} ); +is( $read_counts->{'1'}->[0]->[5], undef, q{3' end position} ); +is( $read_counts->{'1'}->[0]->[6], undef, q{3' end strand} ); +is( $read_counts->{'1'}->[0]->[7], undef, q{3' end read count} ); +is( scalar @{ $read_counts->{'1'}->[0]->[8] }, 2, '2 samples' ); +is( $read_counts->{'1'}->[0]->[8]->[0], 10, '10 reads' ); +is( $read_counts->{'1'}->[0]->[8]->[1], 20, '20 reads' ); diff --git a/t/misc-output.t b/t/misc-output.t new file mode 100644 index 0000000..555bfd8 --- /dev/null +++ b/t/misc-output.t @@ -0,0 +1,98 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 13; + +use DETCT::Misc::Output qw( + dump_as_table +); + +use File::Temp qw( tempdir ); +use File::Spec; + +my $tmp_dir = tempdir( CLEANUP => 1 ); + +# Mock sample objects +my $sample1 = Test::MockObject->new(); +$sample1->set_isa('DETCT::Sample'); +$sample1->set_always( 'name', 'wt1' ); +$sample1->set_always( 'condition', 'sibling' ); +$sample1->set_always( 'group', '1' ); +my $sample2 = Test::MockObject->new(); +$sample2->set_isa('DETCT::Sample'); +$sample2->set_always( 'name', 'wt2' ); +$sample2->set_always( 'condition', 'sibling' ); +$sample2->set_always( 'group', '2' ); +my $sample3 = Test::MockObject->new(); +$sample3->set_isa('DETCT::Sample'); +$sample3->set_always( 'name', 'mut1' ); +$sample3->set_always( 'condition', 'mutant' ); +$sample3->set_always( 'group', '1' ); +my $sample4 = Test::MockObject->new(); +$sample4->set_isa('DETCT::Sample'); +$sample4->set_always( 'name', 'mut2' ); +$sample4->set_always( 'condition', 'mutant' ); +$sample4->set_always( 'group', '2' ); +my $samples = [ $sample1, $sample2, $sample3, $sample4 ]; + +# Mock analysis object +my $analysis = Test::MockObject->new(); +$analysis->set_isa('DETCT::Analysis'); +$analysis->set_always( 'get_all_samples', $samples ); +$analysis->set_always( 'ensembl_species', 'danio_rerio' ); + +my $regions = [ + [ + '1', 1, 110, 10, -10, '1', 110, + 1, 10, + [ 4, 1, 2, 7 ], + [ 4.6, 1.1, 2.1, 4.6 ], + undef, undef, + [ 1.18, 0.233 ], + [ [ 0.46, -1.13 ], [ 4.18, 2.06 ] ], + { + e61 => [ + [ + 'ENSDARG00000095747', + 'cxc64', + 'CXC chemokine 64', + 'protein_coding', + 5, + [ [ 'ENSDART00000133571', 'protein_coding' ] ] + ] + ] + } + ], + [ + '1', 1, + 1000, 10, + -10, undef, + undef, undef, + undef, [ 4, 1, 2, 7 ], + [ 4.6, 1.1, 2.1, 4.6 ], undef, + undef, [ 1.18, 0.233 ], + [ [ 0.46, -1.13 ], [ 4.18, 2.06 ] ], {} + ], +]; + +is( + dump_as_table( + { analysis => $analysis, dir => $tmp_dir, regions => $regions, } + ), + undef, 'Dump' +); + +foreach my $format ( 'csv', 'tsv', 'html' ) { + foreach my $level ( 'all', 'sig' ) { + my $file = $level . q{.} . $format; + my $filepath = File::Spec->catfile( $tmp_dir, $file ); + ok( -e $filepath, $file . ' exists' ); + ok( !-z $filepath, $file . ' is not empty' ); + } +} + +# TODO: Actually test output diff --git a/t/misc-peakhmm.t b/t/misc-peakhmm.t new file mode 100644 index 0000000..3764343 --- /dev/null +++ b/t/misc-peakhmm.t @@ -0,0 +1,572 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 103; + +use DETCT::Misc::PeakHMM qw( + merge_read_peaks + summarise_read_peaks + run_peak_hmm + join_hmm_bins +); + +use File::Temp qw( tempdir ); +use File::Path qw( make_path ); +use POSIX qw( WIFEXITED); + +# Compile quince_chiphmmnew if necessary +if ( !-r 'bin/quince_chiphmmnew' ) { + make_path('bin'); + my $cmd = 'g++ -o bin/quince_chiphmmnew src/quince_chiphmmnew.cpp'; + WIFEXITED( system $cmd) or confess "Couldn't run $cmd"; +} + +my $input_peaks; +my $output_peaks; + +# Check merging read peaks required parameters +throws_ok { + merge_read_peaks( + { + seq_name => 1, + peaks => [ [ 1, 2, 1 ] ], + } + ); +} +qr/No peak buffer width specified/ms, 'No peak buffer width'; +throws_ok { + merge_read_peaks( + { + peak_buffer_width => 100, + peaks => [ [ 1, 2, 1 ] ], + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + merge_read_peaks( + { + peak_buffer_width => 100, + seq_name => 1, + } + ); +} +qr/No peaks specified/ms, 'No peaks'; + +# Two peaks but no merging +$input_peaks = [ [ 100, 200, 1 ], [ 500, 600, 1 ], ]; +$output_peaks = merge_read_peaks( + { + peak_buffer_width => 100, + seq_name => 1, + peaks => $input_peaks, + } +); +is( scalar keys %{$output_peaks}, 1, '1 sequence' ); +is( scalar @{ $output_peaks->{'1'} }, 2, '2 peaks' ); +is( $output_peaks->{'1'}->[0]->[0], 100, 'Start of first peak' ); +is( $output_peaks->{'1'}->[0]->[1], 200, 'End of first peak' ); +is( $output_peaks->{'1'}->[0]->[2], 1, 'First peak read count' ); +is( $output_peaks->{'1'}->[-1]->[0], 500, 'Start of last peak' ); +is( $output_peaks->{'1'}->[-1]->[1], 600, 'End of last peak' ); +is( $output_peaks->{'1'}->[-1]->[2], 1, 'Last peak read count' ); + +# Two peaks merged into one +$input_peaks = [ [ 100, 200, 1 ], [ 250, 350, 1 ], ]; +$output_peaks = merge_read_peaks( + { + peak_buffer_width => 100, + seq_name => 1, + peaks => $input_peaks, + } +); +is( scalar keys %{$output_peaks}, 1, '1 sequence' ); +is( scalar @{ $output_peaks->{'1'} }, 1, '1 peak' ); +is( $output_peaks->{'1'}->[0]->[0], 100, 'Start of first peak' ); +is( $output_peaks->{'1'}->[0]->[1], 350, 'End of first peak' ); +is( $output_peaks->{'1'}->[0]->[2], 2, 'First peak read count' ); + +# Three peaks with first two merged +$input_peaks = [ [ 100, 200, 1 ], [ 250, 350, 1 ], [ 500, 600, 1 ], ]; +$output_peaks = merge_read_peaks( + { + peak_buffer_width => 100, + seq_name => 1, + peaks => $input_peaks, + } +); +is( scalar keys %{$output_peaks}, 1, '1 sequence' ); +is( scalar @{ $output_peaks->{'1'} }, 2, '2 peaks' ); +is( $output_peaks->{'1'}->[0]->[0], 100, 'Start of first peak' ); +is( $output_peaks->{'1'}->[0]->[1], 350, 'End of first peak' ); +is( $output_peaks->{'1'}->[0]->[2], 2, 'First peak read count' ); +is( $output_peaks->{'1'}->[-1]->[0], 500, 'Start of last peak' ); +is( $output_peaks->{'1'}->[-1]->[1], 600, 'End of last peak' ); +is( $output_peaks->{'1'}->[-1]->[2], 1, 'Last peak read count' ); + +# Three peaks with second two merged +$input_peaks = [ [ 100, 200, 1 ], [ 500, 600, 1 ], [ 550, 650, 1 ], ]; +$output_peaks = merge_read_peaks( + { + peak_buffer_width => 100, + seq_name => 1, + peaks => $input_peaks, + } +); +is( scalar keys %{$output_peaks}, 1, '1 sequence' ); +is( scalar @{ $output_peaks->{'1'} }, 2, '2 peaks' ); +is( $output_peaks->{'1'}->[0]->[0], 100, 'Start of first peak' ); +is( $output_peaks->{'1'}->[0]->[1], 200, 'End of first peak' ); +is( $output_peaks->{'1'}->[0]->[2], 1, 'First peak read count' ); +is( $output_peaks->{'1'}->[-1]->[0], 500, 'Start of last peak' ); +is( $output_peaks->{'1'}->[-1]->[1], 650, 'End of last peak' ); +is( $output_peaks->{'1'}->[-1]->[2], 2, 'Last peak read count' ); + +# Two peaks separated by buffer width +$input_peaks = [ [ 100, 200, 1 ], [ 300, 400, 1 ], ]; +$output_peaks = merge_read_peaks( + { + peak_buffer_width => 100, + seq_name => 1, + peaks => $input_peaks, + } +); +is( scalar keys %{$output_peaks}, 1, '1 sequence' ); +is( scalar @{ $output_peaks->{'1'} }, 2, '2 peaks' ); +is( $output_peaks->{'1'}->[0]->[0], 100, 'Start of first peak' ); +is( $output_peaks->{'1'}->[0]->[1], 200, 'End of first peak' ); +is( $output_peaks->{'1'}->[0]->[2], 1, 'First peak read count' ); +is( $output_peaks->{'1'}->[-1]->[0], 300, 'Start of last peak' ); +is( $output_peaks->{'1'}->[-1]->[1], 400, 'End of last peak' ); +is( $output_peaks->{'1'}->[-1]->[2], 1, 'Last peak read count' ); + +# Two peaks separated by just under buffer width +$input_peaks = [ [ 100, 200, 1 ], [ 299, 400, 1 ], ]; +$output_peaks = merge_read_peaks( + { + peak_buffer_width => 100, + seq_name => 1, + peaks => $input_peaks, + } +); +is( scalar keys %{$output_peaks}, 1, '1 sequence' ); +is( scalar @{ $output_peaks->{'1'} }, 1, '1 peak' ); +is( $output_peaks->{'1'}->[0]->[0], 100, 'Start of first peak' ); +is( $output_peaks->{'1'}->[0]->[1], 400, 'End of first peak' ); +is( $output_peaks->{'1'}->[0]->[2], 2, 'First peak read count' ); + +# Two peaks with same start +$input_peaks = [ [ 100, 200, 1 ], [ 100, 300, 1 ], ]; +$output_peaks = merge_read_peaks( + { + peak_buffer_width => 100, + seq_name => 1, + peaks => $input_peaks, + } +); +is( scalar keys %{$output_peaks}, 1, '1 sequence' ); +is( scalar @{ $output_peaks->{'1'} }, 1, '1 peak' ); +is( $output_peaks->{'1'}->[0]->[0], 100, 'Start of first peak' ); +is( $output_peaks->{'1'}->[0]->[1], 300, 'End of first peak' ); +is( $output_peaks->{'1'}->[0]->[2], 2, 'First peak read count' ); + +# No peaks +$input_peaks = []; +$output_peaks = merge_read_peaks( + { + peak_buffer_width => 100, + seq_name => 1, + peaks => $input_peaks, + } +); +is( scalar keys %{$output_peaks}, 1, '1 sequence' ); +is( scalar @{ $output_peaks->{'1'} }, 0, '0 peaks' ); + +my $summary; + +# Check summarising read peaks required parameters +throws_ok { + summarise_read_peaks( + { + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_name => '1', + seq_bp => 1000, + read_length => 54, + peaks => [ [ 1, 2, 1 ] ], + } + ); +} +qr/No bin size specified/ms, 'No bin size'; +throws_ok { + summarise_read_peaks( + { + bin_size => 100, + hmm_sig_level => 0.001, + seq_name => '1', + seq_bp => 1000, + read_length => 54, + peaks => [ [ 1, 2, 1 ] ], + } + ); +} +qr/No peak buffer width specified/ms, 'No peak buffer width'; +throws_ok { + summarise_read_peaks( + { + bin_size => 100, + peak_buffer_width => 100, + seq_name => '1', + seq_bp => 1000, + read_length => 54, + peaks => [ [ 1, 2, 1 ] ], + } + ); +} +qr/No HMM significance level specified/ms, 'No HMM significance level'; +throws_ok { + summarise_read_peaks( + { + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_bp => 1000, + read_length => 54, + peaks => [ [ 1, 2, 1 ] ], + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + summarise_read_peaks( + { + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_name => '1', + read_length => 54, + peaks => [ [ 1, 2, 1 ] ], + } + ); +} +qr/No sequence bp specified/ms, 'No sequence bp'; +throws_ok { + summarise_read_peaks( + { + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_name => '1', + seq_bp => 1000, + peaks => [ [ 1, 2, 1 ] ], + } + ); +} +qr/No read length specified/ms, 'No read length'; +throws_ok { + summarise_read_peaks( + { + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_name => '1', + seq_bp => 1000, + read_length => 54, + } + ); +} +qr/No peaks specified/ms, 'No peaks'; + +# Two peaks, one significant +$input_peaks = [ [ 100, 199, 5 ], [ 300, 399, 1 ], ]; +$summary = summarise_read_peaks( + { + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_name => '1', + seq_bp => 1000, + read_length => 54, + peaks => $input_peaks, + } +); +is( scalar keys %{$summary}, 1, '1 sequence' ); +is( scalar keys %{ $summary->{'1'} }, 9, '9 keys' ); +is( + $summary->{'1'}->{total_read_count_per_mb}, + 6 / 1_000_000, + 'Total read count per Mb' +); +is( + $summary->{'1'}->{total_sig_read_count_per_mb}, + 5 / 1_000_000, + 'Total significant read count per Mb' +); +is( + $summary->{'1'}->{total_sig_peak_width_in_mb}, + 100 / 1_000_000, + 'Total significant peak width in Mb' +); +is( $summary->{'1'}->{median_sig_peak_width}, + 100, 'Median significant peak width' ); +is( $summary->{'1'}->{total_sig_peaks}, 1, 'Total significant peaks' ); +is( $summary->{'1'}->{peak_buffer_width}, 100, 'Peak buffer width' ); +ok( $summary->{'1'}->{read_threshold} < 5, 'Read threshold' ); +is( $summary->{'1'}->{bin_size}, 100, 'Bin size' ); +is( $summary->{'1'}->{num_bins}, 10, 'Number of bins' ); + +# No significant peaks +$input_peaks = [ [ 300, 399, 1 ], ]; +$summary = summarise_read_peaks( + { + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_name => '1', + seq_bp => 1000, + read_length => 54, + peaks => $input_peaks, + } +); +is( $summary->{'1'}->{median_sig_peak_width}, + 0, 'Median significant peak width' ); + +# Three significant peaks +$input_peaks = [ [ 100, 149, 500 ], [ 300, 399, 500 ], [ 600, 759, 500 ], ]; +$summary = summarise_read_peaks( + { + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_name => '1', + seq_bp => 1000, + read_length => 54, + peaks => $input_peaks, + } +); +is( $summary->{'1'}->{median_sig_peak_width}, + 100, 'Median significant peak width' ); + +# No peaks +$summary = summarise_read_peaks( + { + bin_size => 100, + peak_buffer_width => 100, + hmm_sig_level => 0.001, + seq_name => '1', + seq_bp => 1000, + read_length => 54, + peaks => [], + } +); +is( scalar keys %{ $summary->{'1'} }, 0, 'No summary' ); + +my $tmp_dir = tempdir( CLEANUP => 1 ); + +# Check running peak HMM required parameters +my $read_bins = { + 1 => 500, + 3 => 1, +}; +$summary = { + total_read_count_per_mb => 501 / 1_000_000, + total_sig_read_count_per_mb => 500 / 1_000_000, + total_sig_peak_width_in_mb => 100 / 1_000_000, + median_sig_peak_width => 100, + total_sig_peaks => 1, + peak_buffer_width => 100, + read_threshold => 3, + bin_size => 100, + num_bins => 10, +}; +throws_ok { + run_peak_hmm( + { + hmm_sig_level => 0.001, + seq_name => '1', + read_bins => $read_bins, + summary => $summary, + hmm_binary => 'bin/quince_chiphmmnew', + } + ); +} +qr/No directory specified/ms, 'No directory'; +throws_ok { + run_peak_hmm( + { + dir => $tmp_dir, + seq_name => '1', + read_bins => $read_bins, + summary => $summary, + hmm_binary => 'bin/quince_chiphmmnew', + } + ); +} +qr/No HMM significance level specified/ms, 'No HMM significance level'; +throws_ok { + run_peak_hmm( + { + dir => $tmp_dir, + hmm_sig_level => 0.001, + read_bins => $read_bins, + summary => $summary, + hmm_binary => 'bin/quince_chiphmmnew', + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + run_peak_hmm( + { + dir => $tmp_dir, + hmm_sig_level => 0.001, + seq_name => '1', + summary => $summary, + hmm_binary => 'bin/quince_chiphmmnew', + } + ); +} +qr/No read bins specified/ms, 'No read bins'; +throws_ok { + run_peak_hmm( + { + dir => $tmp_dir, + hmm_sig_level => 0.001, + seq_name => '1', + read_bins => $read_bins, + hmm_binary => 'bin/quince_chiphmmnew', + } + ); +} +qr/No summary specified/ms, 'No summary'; +throws_ok { + run_peak_hmm( + { + dir => $tmp_dir, + hmm_sig_level => 0.001, + seq_name => '1', + read_bins => $read_bins, + summary => $summary, + } + ); +} +qr/No HMM binary specified/ms, 'No HMM binary'; + +my $hmm; + +# Run peak HMM +$hmm = run_peak_hmm( + { + dir => $tmp_dir, + hmm_sig_level => 0.001, + seq_name => '1', + read_bins => $read_bins, + summary => $summary, + hmm_binary => 'bin/quince_chiphmmnew', + } +); +is( scalar keys %{$hmm}, 1, '1 sequence' ); +is( scalar @{ $hmm->{'1'} }, 1, '1 peak' ); +is( $hmm->{'1'}->[0]->[0], 1, 'Bin 1' ); +is( $hmm->{'1'}->[0]->[1], 500, '500 reads' ); +ok( $hmm->{'1'}->[0]->[2] < 0, 'Log probability negative' ); + +# Run peak HMM with no summary +$hmm = run_peak_hmm( + { + dir => $tmp_dir, + hmm_sig_level => 0.001, + seq_name => '1', + read_bins => $read_bins, + summary => {}, + hmm_binary => 'bin/quince_chiphmmnew', + } +); +is( scalar keys %{$hmm}, 1, '1 sequence' ); +is( scalar @{ $hmm->{'1'} }, 0, '0 peaks' ); + +# Run peak HMM with non-existent working directory +$hmm = run_peak_hmm( + { + dir => $tmp_dir . '/test', + hmm_sig_level => 0.001, + seq_name => '1', + read_bins => $read_bins, + summary => $summary, + hmm_binary => 'bin/quince_chiphmmnew', + } +); +is( scalar keys %{$hmm}, 1, '1 sequence' ); +is( scalar @{ $hmm->{'1'} }, 1, '1 peak' ); + +# Check running peak HMM required parameters +my $hmm_bins = [ + [ 1, 10, -2.3 ], + [ 2, 20, -2.3 ], + [ 4, 10, -2.3 ], + [ 5, 30, -2.3 ], + [ 6, 20, -2.3 ], +]; +throws_ok { + join_hmm_bins( + { + seq_name => '1', + hmm_bins => $hmm_bins, + } + ); +} +qr/No bin size specified/ms, 'No bin size'; +throws_ok { + join_hmm_bins( + { + bin_size => 100, + hmm_bins => $hmm_bins, + } + ); +} +qr/No sequence name specified/ms, 'No sequence name'; +throws_ok { + join_hmm_bins( + { + bin_size => 100, + seq_name => '1', + } + ); +} +qr/No HMM bins specified/ms, 'No HMM bins'; + +my $regions; + +# Five peaks joined to two regions +$regions = join_hmm_bins( + { + bin_size => 100, + seq_name => '1', + hmm_bins => $hmm_bins, + } +); +is( scalar keys %{$regions}, 1, '1 sequence' ); +is( scalar @{ $regions->{'1'} }, 2, '2 peaks' ); +is( $regions->{'1'}->[0]->[0], 101, 'Region 1 start' ); +is( $regions->{'1'}->[0]->[1], 300, 'Region 1 end' ); +is( $regions->{'1'}->[0]->[2], 20, 'Region 1 max read count' ); +is( $regions->{'1'}->[0]->[3], -4.6, 'Region 1 log probability sum' ); +is( $regions->{'1'}->[1]->[0], 401, 'Region 2 start' ); +is( $regions->{'1'}->[1]->[1], 700, 'Region 2 end' ); +is( $regions->{'1'}->[1]->[2], 30, 'Region 2 max read count' ); +is( $regions->{'1'}->[1]->[3], -6.9, 'Region 2 log probability sum' ); + +# No peaks +$regions = join_hmm_bins( + { + bin_size => 100, + seq_name => '1', + hmm_bins => [], + } +); +is( scalar keys %{$regions}, 1, '1 sequence' ); +is( scalar @{ $regions->{'1'} }, 0, '0 peaks' ); diff --git a/t/misc-tag.t b/t/misc-tag.t new file mode 100644 index 0000000..2cf6a8a --- /dev/null +++ b/t/misc-tag.t @@ -0,0 +1,260 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 104; + +use DETCT::Misc::Tag qw( + detag_trim_fastq + convert_tag_to_regexp +); + +use File::Temp qw( tempdir ); +use File::Slurp; + +=for comment + +Test random FASTQ files can be regenerated using: + +perl script/make_test_fastq.pl --seed 1 --output_prefix test1 \ +--read_tags NNNNBGAGGC NNNNBAGAAG +perl script/make_test_fastq.pl --seed 2 --output_prefix test2 \ +--read_tags NNNNBGAGGC NNNNBAGAAG --read_length 54 +mv test* t/data/ + +Some numbers in tests below will then need updating. + +test1 NNNNBGAGGC: 25 +test1 NNNNBAGAAG: 24 +test1 XXXXXXXXXX: 51 + +test2 NNNNBGAGGC: 24 +test2 NNNNBAGAAG: 35 +test2 XXXXXXXXXX: 41 + +=cut + +my $tmp_dir = tempdir( CLEANUP => 1 ); + +# Check detagging and trimming FASTQ files +is( + detag_trim_fastq( + { + fastq_read1_input => 't/data/test1_1.fastq', + fastq_read2_input => 't/data/test1_2.fastq', + fastq_output_prefix => $tmp_dir . '/test1', + pre_detag_trim_length => 54, + polyt_trim_length => 14, + polyt_min_length => 10, + read_tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } + ), + undef, + 'Detag and trim FASTQ' +); + +my @fastq; +@fastq = read_file( $tmp_dir . '/test1_NNNNBGAGGC_1.fastq' ); +is( scalar @fastq / 4, 25, '25 read 1s' ); +@fastq = read_file( $tmp_dir . '/test1_NNNNBGAGGC_2.fastq' ); +is( scalar @fastq / 4, 25, '25 read 2s' ); +@fastq = read_file( $tmp_dir . '/test1_NNNNBAGAAG_1.fastq' ); +is( scalar @fastq / 4, 24, '24 read 1s' ); +@fastq = read_file( $tmp_dir . '/test1_NNNNBAGAAG_2.fastq' ); +is( scalar @fastq / 4, 24, '24 read 2s' ); +@fastq = read_file( $tmp_dir . '/test1_XXXXXXXXXX_1.fastq' ); +is( scalar @fastq / 4, 51, '51 read 1s' ); +@fastq = read_file( $tmp_dir . '/test1_XXXXXXXXXX_2.fastq' ); +is( scalar @fastq / 4, 51, '51 read 2s' ); + +@fastq = read_file( $tmp_dir . '/test1_NNNNBGAGGC_1.fastq' ); +my $read_name = $fastq[0]; +chomp $read_name; +is( substr( $read_name, -7 ), 'GAGGC/1', 'Tag added to read name' ); +my $read_seq = $fastq[1]; +chomp $read_seq; +is( length $read_seq, 30, 'Sequence trimmed to 30 bp' ); +my $read_qual = $fastq[3]; +chomp $read_qual; +is( length $read_qual, 30, 'Quality trimmed to 30 bp' ); + +@fastq = read_file( $tmp_dir . '/test1_NNNNBGAGGC_2.fastq' ); +my $read_name = $fastq[0]; +chomp $read_name; +is( substr( $read_name, -7 ), 'GAGGC/2', 'Tag added to read name' ); +my $read_seq = $fastq[1]; +chomp $read_seq; +is( length $read_seq, 54, 'Sequence trimmed to 54 bp' ); +my $read_qual = $fastq[3]; +chomp $read_qual; +is( length $read_qual, 54, 'Quality trimmed to 54 bp' ); + +@fastq = read_file( $tmp_dir . '/test1_XXXXXXXXXX_1.fastq' ); +my $read_name = $fastq[0]; +chomp $read_name; +is( substr( $read_name, -13 ), '#XXXXXXXXXX/1', 'Tag added to read name' ); +my $read_seq = $fastq[1]; +chomp $read_seq; +is( length $read_seq, 54, 'Sequence trimmed to 54 bp' ); +my $read_qual = $fastq[3]; +chomp $read_qual; +is( length $read_qual, 54, 'Quality trimmed to 54 bp' ); + +@fastq = read_file( $tmp_dir . '/test1_XXXXXXXXXX_2.fastq' ); +my $read_name = $fastq[0]; +chomp $read_name; +is( substr( $read_name, -13 ), '#XXXXXXXXXX/2', 'Tag added to read name' ); +my $read_seq = $fastq[1]; +chomp $read_seq; +is( length $read_seq, 54, 'Sequence trimmed to 54 bp' ); +my $read_qual = $fastq[3]; +chomp $read_qual; +is( length $read_qual, 54, 'Quality trimmed to 54 bp' ); + +# Check detagging and trimming FASTQ files +is( + detag_trim_fastq( + { + fastq_read1_input => 't/data/test2_1.fastq', + fastq_read2_input => 't/data/test2_2.fastq', + fastq_output_prefix => $tmp_dir . '/test2', + pre_detag_trim_length => 54, + polyt_trim_length => 14, + polyt_min_length => 10, + read_tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } + ), + undef, + 'Detag and trim FASTQ' +); + +my @fastq; +@fastq = read_file( $tmp_dir . '/test2_NNNNBGAGGC_1.fastq' ); +is( scalar @fastq / 4, 24, '30 read 1s' ); +@fastq = read_file( $tmp_dir . '/test2_NNNNBGAGGC_2.fastq' ); +is( scalar @fastq / 4, 24, '30 read 2s' ); +@fastq = read_file( $tmp_dir . '/test2_NNNNBAGAAG_1.fastq' ); +is( scalar @fastq / 4, 35, '24 read 1s' ); +@fastq = read_file( $tmp_dir . '/test2_NNNNBAGAAG_2.fastq' ); +is( scalar @fastq / 4, 35, '24 read 2s' ); +@fastq = read_file( $tmp_dir . '/test2_XXXXXXXXXX_1.fastq' ); +is( scalar @fastq / 4, 41, '46 read 1s' ); +@fastq = read_file( $tmp_dir . '/test2_XXXXXXXXXX_2.fastq' ); +is( scalar @fastq / 4, 41, '46 read 2s' ); + +throws_ok { + detag_trim_fastq( + { + fastq_read1_input => 't/data/test1_1.fastq', + fastq_read2_input => 't/data/test2_2.fastq', + fastq_output_prefix => $tmp_dir . '/test', + pre_detag_trim_length => 54, + polyt_trim_length => 14, + polyt_min_length => 10, + read_tags => [ 'NNNNBGAGGC', 'NNNNBAGAAG' ], + } + ); +} +qr/Read order does not match in input/ms, 'FASTQ files not matched'; + +# Check converting tags to regular expressions + +my @tags = qw( N B D H V R Y K M S W A G C T AA ); + +%re_for = convert_tag_to_regexp(@tags); + +ok( q{A} =~ $re_for{A}->[0], 'A matches A' ); +ok( q{G} !~ $re_for{A}->[0], 'A does not match G' ); +ok( q{C} !~ $re_for{A}->[0], 'A does not match C' ); +ok( q{T} !~ $re_for{A}->[0], 'A does not match T' ); +ok( q{N} !~ $re_for{A}->[0], 'A does not match N' ); + +ok( q{A} !~ $re_for{G}->[0], 'G does not match A' ); +ok( q{G} =~ $re_for{G}->[0], 'G matches G' ); +ok( q{C} !~ $re_for{G}->[0], 'G does not match C' ); +ok( q{T} !~ $re_for{G}->[0], 'G does not match T' ); +ok( q{N} !~ $re_for{G}->[0], 'G does not match N' ); + +ok( q{A} !~ $re_for{C}->[0], 'C does not match A' ); +ok( q{G} !~ $re_for{C}->[0], 'C does not match G' ); +ok( q{C} =~ $re_for{C}->[0], 'C matches C' ); +ok( q{T} !~ $re_for{C}->[0], 'C does not match T' ); +ok( q{N} !~ $re_for{C}->[0], 'C does not match N' ); + +ok( q{A} !~ $re_for{T}->[0], 'T does not match A' ); +ok( q{G} !~ $re_for{T}->[0], 'T does not match G' ); +ok( q{C} !~ $re_for{T}->[0], 'T does not match C' ); +ok( q{T} =~ $re_for{T}->[0], 'T matches T' ); +ok( q{N} !~ $re_for{T}->[0], 'T does not match N' ); + +ok( q{A} =~ $re_for{R}->[0], 'R matches A' ); +ok( q{G} =~ $re_for{R}->[0], 'R matches G' ); +ok( q{C} !~ $re_for{R}->[0], 'R does not match C' ); +ok( q{T} !~ $re_for{R}->[0], 'R does not match T' ); +ok( q{N} !~ $re_for{R}->[0], 'R does not match N' ); + +ok( q{A} !~ $re_for{Y}->[0], 'Y does not match A' ); +ok( q{G} !~ $re_for{Y}->[0], 'Y does not match G' ); +ok( q{C} =~ $re_for{Y}->[0], 'Y matches C' ); +ok( q{T} =~ $re_for{Y}->[0], 'Y matches T' ); +ok( q{N} !~ $re_for{Y}->[0], 'Y does not match N' ); + +ok( q{A} !~ $re_for{S}->[0], 'S does not match A' ); +ok( q{G} =~ $re_for{S}->[0], 'S matches G' ); +ok( q{C} =~ $re_for{S}->[0], 'S matches C' ); +ok( q{T} !~ $re_for{S}->[0], 'S does not match T' ); +ok( q{N} !~ $re_for{S}->[0], 'S does not match N' ); + +ok( q{A} =~ $re_for{W}->[0], 'W matches A' ); +ok( q{G} !~ $re_for{W}->[0], 'W does not match G' ); +ok( q{C} !~ $re_for{W}->[0], 'W does not match C' ); +ok( q{T} =~ $re_for{W}->[0], 'W matches T' ); +ok( q{N} !~ $re_for{W}->[0], 'W does not match N' ); + +ok( q{A} !~ $re_for{K}->[0], 'K does not match A' ); +ok( q{G} =~ $re_for{K}->[0], 'K matches G' ); +ok( q{C} !~ $re_for{K}->[0], 'K does not match C' ); +ok( q{T} =~ $re_for{K}->[0], 'K matches T' ); +ok( q{N} !~ $re_for{K}->[0], 'K does not match N' ); + +ok( q{A} =~ $re_for{M}->[0], 'M matches A' ); +ok( q{G} !~ $re_for{M}->[0], 'M does not match G' ); +ok( q{C} =~ $re_for{M}->[0], 'M matches C' ); +ok( q{T} !~ $re_for{M}->[0], 'M does not match T' ); +ok( q{N} !~ $re_for{M}->[0], 'M does not match N' ); + +ok( q{A} !~ $re_for{B}->[0], 'B does not match A' ); +ok( q{G} =~ $re_for{B}->[0], 'B matches G' ); +ok( q{C} =~ $re_for{B}->[0], 'B matches C' ); +ok( q{T} =~ $re_for{B}->[0], 'B matches T' ); +ok( q{N} !~ $re_for{B}->[0], 'B does not match N' ); + +ok( q{A} =~ $re_for{D}->[0], 'D matches A' ); +ok( q{G} =~ $re_for{D}->[0], 'D matches G' ); +ok( q{C} !~ $re_for{D}->[0], 'D does not match C' ); +ok( q{T} =~ $re_for{D}->[0], 'D matches T' ); +ok( q{N} !~ $re_for{D}->[0], 'D does not match N' ); + +ok( q{A} =~ $re_for{H}->[0], 'M matches A' ); +ok( q{G} !~ $re_for{H}->[0], 'M does not match G' ); +ok( q{C} =~ $re_for{H}->[0], 'M matches C' ); +ok( q{T} =~ $re_for{H}->[0], 'M matches T' ); +ok( q{N} !~ $re_for{H}->[0], 'M does not match N' ); + +ok( q{A} =~ $re_for{V}->[0], 'V matches A' ); +ok( q{G} =~ $re_for{V}->[0], 'V matches G' ); +ok( q{C} =~ $re_for{V}->[0], 'V matches C' ); +ok( q{T} !~ $re_for{V}->[0], 'V does not match T' ); +ok( q{N} !~ $re_for{V}->[0], 'V does not match N' ); + +ok( q{A} =~ $re_for{N}->[0], 'N matches A' ); +ok( q{G} =~ $re_for{N}->[0], 'N matches G' ); +ok( q{C} =~ $re_for{N}->[0], 'N matches C' ); +ok( q{T} =~ $re_for{N}->[0], 'N matches T' ); +ok( q{N} =~ $re_for{N}->[0], 'N matches N' ); + +ok( q{A} !~ $re_for{AA}->[0], 'AA does not match A' ); +ok( q{AA} =~ $re_for{AA}->[0], 'AA matches AA' ); diff --git a/t/pipeline-job.t b/t/pipeline-job.t new file mode 100644 index 0000000..3458a56 --- /dev/null +++ b/t/pipeline-job.t @@ -0,0 +1,102 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 48; + +use DETCT::Pipeline::Job; + +# Mock stage objects with different names +my $stage1 = Test::MockObject->new(); +$stage1->set_isa('DETCT::Pipeline::Stage'); +$stage1->set_always( 'name', 'get_read_peaks' ); +my $stage2 = Test::MockObject->new(); +$stage2->set_isa('DETCT::Pipeline::Stage'); +$stage2->set_always( 'name', 'merge_read_peaks' ); + +my $job = DETCT::Pipeline::Job->new( + { + stage => $stage1, + component => 2, + scheduler => 'local', + base_filename => './run_deseq/1', + } +); + +isa_ok( $job, 'DETCT::Pipeline::Job' ); + +# Test stage attribute +is( $job->stage->name, 'get_read_peaks', 'Get stage' ); +is( $job->set_stage($stage2), undef, 'Set stage' ); +is( $job->stage->name, 'merge_read_peaks', 'Get new stage' ); +throws_ok { $job->set_stage() } qr/No stage specified/ms, 'No stage'; +throws_ok { $job->set_stage('invalid') } qr/Class of stage/ms, 'Invalid stage'; + +# Test component attribute +is( $job->component, 2, 'Get component' ); +is( $job->set_component(3), undef, 'Set component' ); +is( $job->component, 3, 'Get new component' ); +throws_ok { $job->set_component() } qr/No component specified/ms, + 'No component'; +throws_ok { $job->set_component(-1) } qr/Invalid component/ms, + 'Invalid component'; + +# Test scheduler attribute +is( $job->scheduler, 'local', 'Get scheduler' ); +is( $job->set_scheduler('lsf'), undef, 'Set scheduler' ); +is( $job->scheduler, 'lsf', 'Get new scheduler' ); +throws_ok { $job->set_scheduler() } qr/Invalid scheduler specified/ms, + 'No scheduler'; +throws_ok { $job->set_scheduler('invalid') } qr/Invalid scheduler specified/ms, + 'Invalid scheduler'; + +# Test base_filename attribute +is( $job->base_filename, './run_deseq/1', 'Get base filename' ); +is( $job->set_base_filename('./count_reads/2'), undef, 'Set base filename' ); +is( $job->base_filename, './count_reads/2', 'Get new base filename' ); +throws_ok { $job->set_base_filename() } qr/No base filename specified/ms, + 'No base filename'; +throws_ok { $job->set_base_filename('') } qr/No base filename specified/ms, + 'Empty base filename'; + +# Test parameters attribute +is( $job->parameters, undef, 'Get parameters' ); +is( $job->set_parameters('test'), undef, 'Set parameters' ); +is( $job->parameters, 'test', 'Get new parameters' ); +is( $job->set_parameters(), undef, 'Set undef parameters' ); +is( $job->parameters, undef, 'Get undef parameters' ); + +# Test memory attribute +is( $job->memory, undef, 'Get memory' ); +is( $job->set_memory(1000), undef, 'Set memory' ); +is( $job->memory, 1000, 'Get new memory' ); +throws_ok { $job->set_memory(-1) } qr/Invalid memory/ms, 'Invalid memory'; + +# Test retries attribute +is( $job->retries, undef, 'Get retries' ); +is( $job->set_retries(5), undef, 'Set retries' ); +is( $job->retries, 5, 'Get new retries' ); +throws_ok { $job->set_retries(-1) } qr/Invalid retries/ms, 'Invalid retries'; + +# Test status code attribute +is( $job->status_code, 'NOT_RUN', 'Get not run status code' ); +is( $job->set_status_code('DONE'), undef, 'Set done status code' ); +is( $job->status_code, 'DONE', 'Get done status code' ); +is( $job->set_status_code('RUNNING'), undef, 'Set running status code' ); +is( $job->status_code, 'RUNNING', 'Get new running status code' ); +is( $job->set_status_code('FAILED'), undef, 'Set failed status code' ); +is( $job->status_code, 'FAILED', 'Get new failed status code' ); +throws_ok { $job->set_status_code() } qr/No status code specified/ms, + 'No status code'; +throws_ok { $job->set_status_code('invalid') } qr/Invalid status code/ms, + 'Invalid status code'; + +# Test status text attribute +is( $job->status_text, undef, 'Get status text' ); +is( $job->set_status_text('Job killed by owner'), undef, 'Set status text' ); +is( $job->status_text, 'Job killed by owner', 'Get new status text' ); +is( $job->set_status_text(), undef, 'Set undef status text' ); +is( $job->status_text, undef, 'Get undef status text' ); diff --git a/t/pipeline-stage.t b/t/pipeline-stage.t new file mode 100644 index 0000000..56dfaff --- /dev/null +++ b/t/pipeline-stage.t @@ -0,0 +1,64 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 22; + +use DETCT::Pipeline::Stage; + +my $stage = DETCT::Pipeline::Stage->new( + { + name => 'count_tags', + default_memory => 3000, + } +); + +isa_ok( $stage, 'DETCT::Pipeline::Stage' ); + +# Test name attribute +is( $stage->name, 'count_tags', 'Get name' ); +is( $stage->set_name('bin_reads'), undef, 'Set name' ); +is( $stage->name, 'bin_reads', 'Get new name' ); +throws_ok { $stage->set_name() } qr/No name specified/ms, 'No name'; +throws_ok { $stage->set_name('/') } qr/Invalid name/ms, 'Invalid name'; + +# Test default memory attribute +is( $stage->default_memory, 3000, 'Get default memory' ); +is( $stage->set_default_memory(2000), undef, 'Set default memory' ); +is( $stage->default_memory, 2000, 'Get new default memory' ); +throws_ok { $stage->set_default_memory() } qr/No default memory specified/ms, + 'No default memory'; +throws_ok { $stage->set_default_memory(-1) } qr/Invalid default memory/ms, + 'Invalid default memory'; + +# Test all jobs run attribute +is( $stage->all_jobs_run, 0, 'Get all jobs run' ); +is( $stage->set_all_jobs_run(10), undef, 'Set all jobs run to true' ); +is( $stage->all_jobs_run, 1, 'Get new true all jobs run' ); +is( $stage->set_all_jobs_run(), undef, 'Set all jobs run to false' ); +is( $stage->all_jobs_run, 0, 'Get new false all jobs run' ); + +# Mock stage object with different name +my $stage1 = Test::MockObject->new(); +$stage1->set_isa('DETCT::Pipeline::Stage'); +$stage1->set_always( 'name', 'get_read_peaks' ); +my $stage2 = Test::MockObject->new(); +$stage2->set_isa('DETCT::Pipeline::Stage'); +$stage2->set_always( 'name', 'merge_read_peaks' ); + +# Test adding and retrieving prerequisites +my $prerequisites; +$prerequisites = $stage->get_all_prerequisites(); +is( scalar @{$prerequisites}, 0, 'No prerequisites' ); +is( $stage->add_prerequisite($stage1), undef, 'Add prerequisite' ); +$prerequisites = $stage->get_all_prerequisites(); +is( scalar @{$prerequisites}, 1, 'Get one prerequisite' ); +$stage->add_prerequisite($stage2); +is( scalar @{$prerequisites}, 2, 'Get two prerequisites' ); +throws_ok { $stage->add_prerequisite() } qr/No prerequisite specified/ms, + 'No prerequisite specified'; +throws_ok { $stage->add_prerequisite('invalid') } qr/Class of prerequisite/ms, + 'Invalid prerequisite'; diff --git a/t/sample.t b/t/sample.t new file mode 100644 index 0000000..16b3bc4 --- /dev/null +++ b/t/sample.t @@ -0,0 +1,81 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 36; + +use DETCT::Sample; + +my $sample = DETCT::Sample->new( + { + name => 'zmp_ph1_1m', + condition => 'mutant', + group => '1', + tag => 'NNNNBGAGGC', + bam_file => 't/data/test1.bam', + } +); + +isa_ok( $sample, 'DETCT::Sample' ); + +# Test name attribute +is( $sample->name, 'zmp_ph1_1m', 'Get name' ); +is( $sample->set_name('zmp_ph1_1s'), undef, 'Set name' ); +is( $sample->name, 'zmp_ph1_1s', 'Get new name' ); +throws_ok { $sample->set_name() } qr/No name specified/ms, 'No name'; +my $long_name = 'X' x ( $DETCT::Sample::MAX_NAME_LENGTH + 1 ); +throws_ok { $sample->set_name(' ') } qr/Invalid name specified/ms, + 'Invalid name'; +throws_ok { $sample->set_name('') } qr/Empty name specified/ms, 'Empty name'; +throws_ok { $sample->set_name($long_name) } qr/longer than \d+ characters/ms, + 'Long name'; + +# Test description attribute +is( $sample->description, undef, 'Get description' ); +is( $sample->set_description('ZMP phenotype 1.1 mutant'), + undef, 'Set description' ); +is( $sample->description, 'ZMP phenotype 1.1 mutant', 'Get new description' ); +is( $sample->set_description(), undef, 'Set undef description' ); +is( $sample->description, undef, 'Get undef description' ); + +# Test condition attribute +is( $sample->condition, 'mutant', 'Get condition' ); +is( $sample->set_condition('sibling'), undef, 'Set condition' ); +is( $sample->condition, 'sibling', 'Get new condition' ); +throws_ok { $sample->set_condition() } qr/No condition specified/ms, + 'No condition'; +my $long_condition = 'X' x ( $DETCT::Sample::MAX_CONDITION_LENGTH + 1 ); +throws_ok { $sample->set_condition('') } qr/Empty condition specified/ms, + 'Empty condition'; +throws_ok { $sample->set_condition($long_condition) } +qr/longer than \d+ characters/ms, 'Long condition'; + +# Test group attribute +is( $sample->group, '1', 'Get group' ); +is( $sample->set_group('2'), undef, 'Set group' ); +is( $sample->group, '2', 'Get new group' ); +is( $sample->set_group(), undef, 'Set undefined group' ); +is( $sample->group, undef, 'Get undefined group' ); +my $long_group = 'X' x ( $DETCT::Sample::MAX_GROUP_LENGTH + 1 ); +throws_ok { $sample->set_group('') } qr/Empty group specified/ms, 'Empty group'; +throws_ok { $sample->set_group($long_group) } qr/longer than \d+ characters/ms, + 'Long group'; + +# Test tag attribute +is( $sample->tag, 'NNNNBGAGGC', 'Get tag' ); +is( $sample->set_tag('NNNNBCAGAG'), undef, 'Set tag' ); +is( $sample->tag, 'NNNNBCAGAG', 'Get new tag' ); +throws_ok { $sample->set_tag() } qr/No tag specified/ms, 'No tag'; +throws_ok { $sample->set_tag('NNNNBCAGAN') } qr/Invalid tag/ms, 'Invalid tag'; + +# Test bam file attribute +is( $sample->bam_file, 't/data/test1.bam', 'Get BAM file' ); +is( $sample->set_bam_file('t/data/test2.bam'), undef, 'Set BAM file' ); +is( $sample->bam_file, 't/data/test2.bam', 'Get new BAM file' ); +throws_ok { $sample->set_bam_file() } qr/No BAM file specified/ms, + 'No BAM file'; +throws_ok { $sample->set_bam_file('nonexistent.bam') } +qr/does not exist or cannot be read/ms, 'Missing BAM file'; diff --git a/t/sequence.t b/t/sequence.t new file mode 100644 index 0000000..7aa985d --- /dev/null +++ b/t/sequence.t @@ -0,0 +1,36 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 12; + +use DETCT::Sequence; + +my $sequence = DETCT::Sequence->new( + { + name => '1', + bp => 60_348_388, + } +); + +isa_ok( $sequence, 'DETCT::Sequence' ); + +# Test name attribute +is( $sequence->name, '1', 'Get name' ); +is( $sequence->set_name('2'), undef, 'Set name' ); +is( $sequence->name, '2', 'Get new name' ); +throws_ok { $sequence->set_name() } qr/No name specified/ms, 'No name'; +my $long_name = 'X' x ( $DETCT::Sequence::MAX_NAME_LENGTH + 1 ); +throws_ok { $sequence->set_name('') } qr/Empty name specified/ms, 'Empty name'; +throws_ok { $sequence->set_name($long_name) } qr/longer than \d+ characters/ms, + 'Long name'; + +# Test bp attribute +is( $sequence->bp, 60_348_388, 'Get bp' ); +is( $sequence->set_bp(60_300_536), undef, 'Set bp' ); +is( $sequence->bp, 60_300_536, 'Get new bp' ); +throws_ok { $sequence->set_bp() } qr/No bp specified/ms, 'No bp'; +throws_ok { $sequence->set_bp(-1) } qr/Invalid bp/ms, 'Invalid bp'; diff --git a/t/transcript.t b/t/transcript.t new file mode 100644 index 0000000..87fbca3 --- /dev/null +++ b/t/transcript.t @@ -0,0 +1,102 @@ +use Test::More; +use Test::Exception; +use Test::Warn; +use Test::DatabaseRow; +use Test::MockObject; +use Carp; + +plan tests => 47; + +use DETCT::Transcript; + +my $transcript = DETCT::Transcript->new( + { + stable_id => 'ENSDART00000133571', + biotype => 'protein_coding', + seq_name => '5', + start => 40352744, + end => 40354399, + strand => 1, + } +); + +isa_ok( $transcript, 'DETCT::Transcript' ); + +# Test stable id attribute +is( $transcript->stable_id, 'ENSDART00000133571', 'Get stable id' ); +is( $transcript->set_stable_id('ENSDART00000033574'), undef, 'Set stable id' ); +is( $transcript->stable_id, 'ENSDART00000033574', 'Get new stable id' ); +throws_ok { $transcript->set_stable_id() } qr/No stable id specified/ms, + 'No stable id'; +throws_ok { $transcript->set_stable_id('#invalid#') } qr/Invalid stable id/ms, + 'Invalid stable id'; + +# Test name attribute +is( $transcript->name, undef, 'Get name' ); +is( $transcript->set_name('cxc64-001'), undef, 'Set name' ); +is( $transcript->name, 'cxc64-001', 'Get new name' ); +is( $transcript->set_name(), undef, 'Set undef name' ); +is( $transcript->name, undef, 'Get undef name' ); +my $long_name = 'X' x ( $DETCT::Transcript::MAX_NAME_LENGTH + 1 ); +throws_ok { $transcript->set_name('') } qr/Name is empty/ms, 'Empty name'; +throws_ok { $transcript->set_name($long_name) } +qr/longer than \d+ characters/ms, 'Invalid name'; + +# Test description attribute +is( $transcript->description, undef, 'Get description' ); +is( $transcript->set_description('CXC chemokine 64'), undef, + 'Set description' ); +is( $transcript->description, 'CXC chemokine 64', 'Get new description' ); +is( $transcript->set_description(), undef, 'Set undef description' ); +is( $transcript->description, undef, 'Get undef description' ); + +# Test biotype attribute +is( $transcript->biotype, 'protein_coding', 'Get biotype' ); +is( $transcript->set_biotype('nonsense_mediated_decay'), undef, 'Set biotype' ); +is( $transcript->biotype, 'nonsense_mediated_decay', 'Get new biotype' ); +throws_ok { $transcript->set_biotype() } qr/No biotype specified/ms, + 'No biotype'; +throws_ok { $transcript->set_biotype('#invalid#') } qr/Invalid biotype/ms, + 'Invalid biotype'; + +# Test sequence name attribute +is( $transcript->seq_name, '5', 'Get sequence name' ); +is( $transcript->set_seq_name('6'), undef, 'Set sequence name' ); +is( $transcript->seq_name, '6', 'Get new sequence name' ); +throws_ok { $transcript->set_seq_name() } qr/No sequence name specified/ms, + 'No sequence name'; +throws_ok { $transcript->set_seq_name('#invalid#') } +qr/Invalid sequence name/ms, 'Invalid sequence name'; + +# Test start attribute +is( $transcript->start, 40352744, 'Get start' ); +is( $transcript->set_start(30352744), undef, 'Set start' ); +is( $transcript->start, 30352744, 'Get new start' ); +throws_ok { $transcript->set_start() } qr/No start specified/ms, 'No start'; +throws_ok { $transcript->set_start(-1) } qr/Invalid start/ms, 'Invalid start'; + +# Test end attribute +is( $transcript->end, 40354399, 'Get end' ); +is( $transcript->set_end(30354399), undef, 'Set end' ); +is( $transcript->end, 30354399, 'Get new end' ); +throws_ok { $transcript->set_end() } qr/No end specified/ms, 'No end'; +throws_ok { $transcript->set_end(-2) } qr/Invalid end/ms, 'Invalid end'; + +# Test strand attribute +is( $transcript->strand, 1, 'Get strand' ); +is( $transcript->set_strand(-1), undef, 'Set strand' ); +is( $transcript->strand, -1, 'Get new strand' ); +throws_ok { $transcript->set_strand() } qr/No strand specified/ms, 'No strand'; +throws_ok { $transcript->set_strand(0) } qr/Invalid strand/ms, 'Invalid strand'; + +# Mock gene object +my $gene = Test::MockObject->new(); +$gene->set_isa('DETCT::Gene'); +$gene->set_always( 'name', 'cxc64' ); + +# Test gene attribute +is( $transcript->gene, undef, 'Get gene' ); +is( $transcript->set_gene($gene), undef, 'Set gene' ); +is( $transcript->gene->name, 'cxc64', 'Get new gene' ); +throws_ok { $transcript->set_gene('invalid') } qr/Class of gene/ms, + 'Invalid gene';

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 U0763o02=@U00000000000K#wPg#Z8m literal 0 HcmV?d00001 diff --git a/t/data/test2.bam.bai b/t/data/test2.bam.bai new file mode 100644 index 0000000000000000000000000000000000000000..e357bb6c240c1a9a2d39cab2f13b7a96d5d4db6b GIT binary patch literal 416 zcmZ>A^kigZU|?VZVoxCk21X#wz#syq-Xs_qfOuY$AoA$qb|6hazz3mF^dg%FGsn`# zq64H9W*)jY6EnmhWb1?pcOB=ZnzAc6pKbuu*o literal 0 HcmV?d00001 diff --git a/t/data/test2_1.fastq b/t/data/test2_1.fastq new file mode 100644 index 0000000..7c9e9d3 --- /dev/null +++ b/t/data/test2_1.fastq @@ -0,0 +1,400 @@ +@HS46_3182:5:2405:11074:43547/1 +CAGTCTCCNNTTTTTTTTTTTTTTTGGTGCTACCGAATATAGAAGAACACGTTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1341:11922:194601/1 +GGCATTATGCAAAAAAAAAAAAAACCTTCTGTATCGGATCATCCCAACGGAACT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:272:17688:181326/1 +GTCAGGCGNNTTTTTTTTTTTTTTGTACCTCCCTGGGGCCCATGTTCATCCGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1564:15853:100266/1 +TCCTCGAANNTTTTTTTTTTTTTTCCATAGCCTCGTATTACTACGTTCGGTCTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1752:18919:39066/1 +GTATTGTCAAAAAAAAAAAAAAAACTTCAGGATAGCAACCAGGCATGGACGCGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1408:2410:48242/1 +GGTACGAGGCTTTTTTTTTTTTTTACTAGCTTTACAGCTGAAGGGCATCGGCCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:381:4299:36056/1 +CGTAAGAGGCTTTTTTTTTTTTTTTTTCTCGAATATACTGTGCCCGCGTTTCCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1073:8428:128989/1 +CATTCGAGGCTTTTTTTTTTTTTTAGGTGGGGCTACACCTCAAAGTATAACGCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:771:4279:15727/1 +GGCTTAGATGTTTTTTTTTTTTTTAATACGTTGATCTTTCGCTACTAAGTTGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:584:1207:121853/1 +GTTCCGAGGCTTTTTTTTTTTTTTATACAAGACATAGGGTCATATTGTTGTCAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2354:16276:7208/1 +TGGCGAGAAGAAAAAAAAAAAAAACATACTAACCGGATGATGGATTCTGTAGTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:80:5305:54651/1 +GGGCCAGAAGTTTTTTTTTTTTTTTAAAGACCCGAGAGGGCCCTAGGAGATTCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:106:2551:71099/1 +TGGTGCGGNNTTTTTTTTTTTTTTGTGTTCCAGTACCGCTAGTTGTAGAGTGGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1817:7790:154523/1 +CAATGAGAAGTTTTTTTTTTTTTTTTGATTGACAATCTCAGAGACCCAAGCTCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:178:16976:72000/1 +AGGAGAGTAGTTTTTTTTTTTTTTGCTGACCATCAATGGGTACGACTGCCAGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2547:4151:12793/1 +AAACTAGAAGTTTTTTTTTTTTTTTTTCCCCGAAGACTCACGAAAATATCCCAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2291:14901:34113/1 +GCTAGAGANNTTTTTTTTTTTTTTTAGGAAGCCTGAACAAAGCTACGTTTGGTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2934:2829:31483/1 +AGTCTTGAAGTTTTTTTTTTTTTTTTGGTGTGCTTAGCGGCCAAGACTCGGAAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1249:16218:147727/1 +CTTGTAGAAGTTTTTTTTTTTTTTTGCATGTAGCTTACGTTGCTCCGCAACGAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:553:771:39903/1 +TCTTCTGAAGAAAAAAAAAAAAAAAACGGGGTATCAGTCTGAAGCACTTCAGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1223:9525:65794/1 +GGCCTAGAAGTTTTTTTTTTTTTTTCTTCATTATGATGCTTAGTGAAAATTAGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1191:17978:187481/1 +TCTAGGAGCCTTTTTTTTTTTTTTAGGAAGTAGTAGATATTCCCTTTCAAGGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:3:10917:33452/1 +TTTCTAGAAGAAAAAAAAAAAAAATCGATAACGCTGTCAACCGCGACCCCGCAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:177:10045:179524/1 +TTTTCACAAGTTTTTTTTTTTTTTGGTCGTTTGGATGCATTTACCGTGGCCACA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:946:13566:137436/1 +GCTCGGAGGCAAAAAAAAAAAAAAAAGACTAGTGGTTTACCCGGAGGTCCCCGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1148:656:9363/1 +CAGGAGAGGCTTTTTTTTTTTTTTCATCATAACCTCTCTTATAGGTGGTGTCTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1498:3307:70020/1 +CTGATGAGNNTTTTTTTTTTTTTTATCCGTCCGTAGGAAGTCGTAAATGTCGGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2248:4900:51665/1 +CAATGGGCTTAAAAAAAAAAAAAAACGAGGAGTGTGGGTCATGCGGTGCCGTAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:807:7391:123290/1 +GGTCGAGTNNTTTTTTTTTTTTTTAGCTAATTGAGCAGAGTTGGCAAACTTCAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2455:17864:5895/1 +TGCTGAGAACTTTTTTTTTTTTTTCACCGCGTGGTCTGTCCGTGGGGTGCCCGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1257:14923:50658/1 +GGCCGAGTNNTTTTTTTTTTTTTTCGGGTCTCAACCTGGAAATTTGAGCGGAGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:492:12915:12514/1 +GACATCAGGCTTTTTTTTTTTTTTAGTGCTCCTTGTGTGCGCGCTTCTCCATGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:235:7270:100350/1 +TCACCAGTAGTTTTTTTTTTTTTTATGGTACAGCAATAGGTGAGACCAGCGTCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2260:14085:3683/1 +ATAGTAAANNTTTTTTTTTTTTTTCACTACATCGACCTGTCTACTTTTGCACCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2169:9651:142664/1 +ATGGCAGTAGTTTTTTTTTTTTTTTTACATCGCGTGTAGATATATTTGTCGTTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:540:14663:48756/1 +GCTGCGAGGCTTTTTTTTTTTTTTTTTACTTATCAGTCTAAAAAGCGAACAGGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1091:11404:166341/1 +AAAGGCCTNNTTTTTTTTTTTTTTAAACCTAGGGAGCGCCGCTAGGTGCAGCTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:608:7328:131607/1 +GAGTGAGAAGAAAAAAAAAAAAAAAGTTGAATGCATTCATTCTTTTCTAGTCGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:522:3049:47753/1 +CATAGAGAAGAAAAAAAAAAAAAACCTGTTGGTCCCCGAATAGTTTGCGGGGAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1283:16887:169154/1 +GTCATGAGCCTTTTTTTTTTTTTTAGTTTTTTTGACGTACGGCGTGTGGCCCTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1095:4319:4606/1 +ACTTATGCNNTTTTTTTTTTTTTTTAAAAGGAATGCACGTATCTAATCGCGTCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:752:19585:108953/1 +TCCGGCAANNTTTTTTTTTTTTTTTATTTGGTGAAGATCTTTTCCAATCCGTGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:752:3015:99805/1 +CACTTGAGGCTTTTTTTTTTTTTTCGGTGGTACGTAGTTCAGGCTAATAATAGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:332:4846:77772/1 +TACCTCAGGCTTTTTTTTTTTTTTGCTTTTAAGTGCCACGGGGGAGGACTGGAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1140:784:66805/1 +AACGTCGGNNTTTTTTTTTTTTTTCCTACTCTTACGCCACTGTCGGGAGCCGAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:473:18338:33594/1 +TTAATAGAAGTTTTTTTTTTTTTTCTAACTCGTTTCGCCCCGGGGATCGAACGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2702:3903:96584/1 +CGAGTGAGGCTTTTTTTTTTTTTTGAAATTGCTCACTTACGGACCTTCACCACC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1797:11052:97278/1 +TCGTTGAGTAAAAAAAAAAAAAAATACGCTCATGGTCATTACATGCGCTTTAAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:820:13922:80241/1 +AAAGCGAGGGAAAAAAAAAAAAAACGCCCCAGTCAATATTTTTACTAATTGGTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:555:4732:87380/1 +CAGGTAGAAGTTTTTTTTTTTTTTCCTGACGTACTACTTGAGTCGGCGGCAGTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:659:12505:103681/1 +GCGTTGTGGCTTTTTTTTTTTTTTGCGCCTGTAGTTCAGCTCTCTAGCTGACAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:59:2864:12297/1 +TCGCTCGANNTTTTTTTTTTTTTTGTTACGAATTCAGAGGTACGCACCAGCCGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1383:4754:130496/1 +ATTCTGACGCTTTTTTTTTTTTTTCCTCTGTCCAATTGAACAGTCTGGTCCGGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2240:16766:66728/1 +CATGGAGAAGAAAAAAAAAAAAAATCGTTCTTGATGACAGTAGTAATATATTAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2100:7817:14667/1 +ACAGTAGAACTTTTTTTTTTTTTTGAATCCAAACTCTGCGCCATAAAATCATAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:895:13916:72786/1 +AGCGCAGATGTTTTTTTTTTTTTTGGAAATGGATGTAGGATTTGCCAAGAGTGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:418:13660:161873/1 +AAGAGAGAAGTTTTTTTTTTTTTTAATGATGGTCAGTCAGCAATCACCACCCAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1283:12361:37079/1 +ATTGGGAGCCTTTTTTTTTTTTTTGCCGCAGCATAGGGTTCTCAATTGCTGAAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:347:5935:115159/1 +TTAACCGCNNTTTTTTTTTTTTTTAGGAGACACGAGGATAATCGTGAGTACGAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1983:1677:46718/1 +TTGGTGAGGCTTTTTTTTTTTTTTAAGATGCCTTGCGTGGATGCGAGCCCTGTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1380:1444:154221/1 +GAAACAGCNNTTTTTTTTTTTTTTCATCAGTATCCCGCGGTCGAGAGTCAAAGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2040:5556:131112/1 +CACTATCGNNTTTTTTTTTTTTTTGATGCCATTCATCGGCATGGGTGCGTTGTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2789:7394:44852/1 +GACACAGAAGTTTTTTTTTTTTTTGATCTGCAGCTTTATGCCGTAATCGAGACT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1215:14424:11602/1 +GTTGTGAGGCTTTTTTTTTTTTTTTAAAGTGTTCCCACCCACCAGATAAGTGCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2475:14123:112282/1 +TCCATAGAAGTTTTTTTTTTTTTTTGGAGGCGATCCTATGACGAAGAGATAATC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2546:19759:182333/1 +TTTAGAGAAGTTTTTTTTTTTTTTCTGCGGCGATCGCCGGAAGGCGTTGCATAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1849:14231:162499/1 +TTAAGACAAGTTTTTTTTTTTTTTTTGTCCAAAGATAATTCTCTGAGAACAGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:662:17029:25695/1 +TCCCTGACGCTTTTTTTTTTTTTTTTGCGAGATTGGACGCGCCGTAGAAAGTGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:566:4782:154015/1 +CGACCAGAAGTTTTTTTTTTTTTTTTCCCGCCCTCGGCGAGATGAAGGCTCCAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2085:5947:93787/1 +CACCGTAGNNTTTTTTTTTTTTTTCTACCTCTTGGACCTAAACCAGTACCGATC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1281:315:57513/1 +CCAACAGAAGTTTTTTTTTTTTTTGAAACGTATTACTTCTAACGTTTGGAGGGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2225:9077:172869/1 +GCCACGAGGCTTTTTTTTTTTTTTCGTTCTAAAGTTCTCTCCCGTACTTGAGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:373:3420:105949/1 +TTAATAGAAGTTTTTTTTTTTTTTCATTTATGATCAGGCGGGATGTAACATATG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1805:9722:68857/1 +CACTGTAGNNTTTTTTTTTTTTTTTGCATCGATCACTATTCCTTGACAATATTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1350:15373:181220/1 +GTTTAGAANNTTTTTTTTTTTTTTCCTCTATTCATTGGCGTGCGGTGCAGTGCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:44:17607:19283/1 +TCCTCGAGGCTTTTTTTTTTTTTTACTCCGTCGTAACGATTCGCCGCCCGCCGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1732:2108:100386/1 +GTTCAAGTNNTTTTTTTTTTTTTTGGTATGGGCCCTCAACTTATTGCTCATTGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:250:15883:128840/1 +ACCTCGAGGCTTTTTTTTTTTTTTTCCTGAAAGTCGACGACTCTCACGAAATGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2113:7932:11007/1 +GGCAGAGAAGTTTTTTTTTTTTTTAATCAGCTGTAGTACTGACCGACCGAAAGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1891:12437:47748/1 +TTCCGAGAAGTTTTTTTTTTTTTTTACTTTCGCTGGAAACGGCCCTCGTCCATT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2369:5530:14886/1 +TGAATAGAAGTTTTTTTTTTTTTTATGCCATGTAGGACCGCCCCCAGGTGTCAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1742:17056:119120/1 +CTCGGCAGGCAAAAAAAAAAAAAATTTCCATTCCAAAGGACTCTTCCCGAACAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2687:831:113506/1 +AACCTTCCNNTTTTTTTTTTTTTTTCATTTATTTGTAGGCAACAGAATGCGCGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:451:18172:127749/1 +AAAGTGAGGCTTTTTTTTTTTTTTGTTTTATCCAGAGCTGTTAGGGTTCTTTCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1906:6140:154562/1 +ACCCGAGAAGTTTTTTTTTTTTTTTAGAATCACGATGTCAGGACGCAATCCGTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:745:10175:155937/1 +TGGCTTGAAGTTTTTTTTTTTTTTCCAACAGCCAGCCCGGCACACGAGGTACAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2679:13605:42940/1 +CACACAGAAGTTTTTTTTTTTTTTGAGTATGTTTACGATACGTGTGCTAGAGTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:525:19584:47076/1 +CGGACAGATGTTTTTTTTTTTTTTATCGATTGACTCCTGATGGGTGATTCCGTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:541:14390:18530/1 +GCAGGCGANNTTTTTTTTTTTTTTTTTCTCCCCGCGGCACAGTTTGATTCGTCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2371:19010:102788/1 +AAGACTATNNTTTTTTTTTTTTTTAAAGGATATAGAAATTATTCTGGGGCCGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1477:14980:45119/1 +CTCGGAGAAGTTTTTTTTTTTTTTGGTCGATCGCCTTCGCTCCTGGTGTACCGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2719:11588:38543/1 +AGTTGAGGNNTTTTTTTTTTTTTTGTCGTTAGTGAGGTGGCCAAGTTTAGCCGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1734:8972:136811/1 +GTACTACAAGAAAAAAAAAAAAAAGGAATGACTCCGTGAACAACGCTTCACGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2208:6046:184805/1 +GTACCGTGGCAAAAAAAAAAAAAATACCGATCGCATCGGCCGCCTCAACGATCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:173:5726:155286/1 +CGGGCAGAAGTTTTTTTTTTTTTTCGCTTCGTGGGAGGGTCCGGCTGACTGATG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1308:11903:50795/1 +AGCCGGAGGCTTTTTTTTTTTTTTGGAGTCTCATACGAGCACCACCCTTTTGGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1898:10887:153698/1 +TCATCCTTNNTTTTTTTTTTTTTTACGGGTCCTGTCGCTGCCGTAGACATGGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:772:13347:20065/1 +AATTTAGAAGTTTTTTTTTTTTTTCCATATATATACCCACGCATTGGGAGGGCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2480:4129:148337/1 +CATATGTGGCTTTTTTTTTTTTTTTCAAACAGACTATGTCATGAAAGCACCCAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:256:18917:52058/1 +GTGCGAGAAGTTTTTTTTTTTTTTGTGGCTTCTTATTTTGTGACTTTCCAGATG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/t/data/test2_2.fastq b/t/data/test2_2.fastq new file mode 100644 index 0000000..77f752e --- /dev/null +++ b/t/data/test2_2.fastq @@ -0,0 +1,400 @@ +@HS46_3182:5:2405:11074:43547/2 +CTAGGGCTGTCCAAGCAGATATACGCGCAACACAGAGTGTGAAAATAATACTCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1341:11922:194601/2 +TCCCTCCAGAGGTTAGTCATAGTCAGTCCCCGCACGACGTGGGTTAGGGGTTGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:272:17688:181326/2 +GTGGGGATGTATTGACTTCGCGGTTCTGATCCCACTGGGTAACAACCGATGGAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1564:15853:100266/2 +GTGTATCCAACGATCGCTTGATTGGTGCTGTTGCGGGGGAGTAAACATGGGCAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1752:18919:39066/2 +GCGGACCATTCAGGCAGATTGACCTTGCCTGCTTCTACTGCATGCACATATTTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1408:2410:48242/2 +ATTATATGGATGCGTCATTTGTTCTACGCACCGCCTGTCCGACCAGTAACCACT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:381:4299:36056/2 +GGGGGTAGCAGCTATATATCCCCACGGGCTAAGAGCTTAGACCCAAGAGGCGGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1073:8428:128989/2 +TTATATTCTGGAAATTGTTAAGCGTCATAGGCCGCAAAACCCGATTATCCATGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:771:4279:15727/2 +GCTTTCGATTTACAGCGTGTGATGAGTACACTCTCGCAACCAAGGTCCAGTGTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:584:1207:121853/2 +GTAAGAAGTTCTACTAAGGCAACCGGCTTAGGCTGATCAAGAATGTCGGGCGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2354:16276:7208/2 +TAGCAGGCTGTAACCCGTGAACTAACTCGGAGTTGTTTGATGGCCGACGACACT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:80:5305:54651/2 +CTATCCACTCGTAGTCTAATAACGCGAGTAGCTCAAGGCAAAGTGTTTGCTCAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:106:2551:71099/2 +TAGTGACGCTTTGAAAGGTCTTGACTTTAGTAAGCCATTTTTTGAAGCCGGCGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1817:7790:154523/2 +GGATTGTCTTTTTCAGGCGTTTGTAACCTGTGCTCTAGACTTTATTGATCGTTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:178:16976:72000/2 +GTTGGACGTGTTTTTGGGTTCGGGAGGATTCCCGAGCCACTCACAGGATTTATG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2547:4151:12793/2 +CAGGCCTCTCTACTCGCGATATTGTTAACTTGTGGGCTGTTCCATGCGGCAACC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2291:14901:34113/2 +ACGGCGGAATACGCTCAGACCGAACGTCTTAGACCTTACAAGTGCTCAGTATAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2934:2829:31483/2 +CTCTGTGACCTCGACTATTTATACATCCGCGCTGGGCTTTCTGCGCGGTACATC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1249:16218:147727/2 +AGGCGTGGGAATTTCCATGTCCTCGCCTTCGAAGGGCTAATGAGAGAGTTATAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:553:771:39903/2 +AGCAAAATCCGTCGAGGCACCAAAGTTGGTCTTTGCCCTCGCGACAAGTGGCGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1223:9525:65794/2 +CTAGAGATACACCATTGTTACCCGCGCGATAAATACGCATGGGGTGCCGCACTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1191:17978:187481/2 +ACATCGGGTTACGATCCAAAAGTTCGGAGTAAATAGACAGATACTCAACTGTCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:3:10917:33452/2 +CAATGTGAGGTACTGATCTTCTCGGATGTGGGTTGCCCGATCAGAATTCATTGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:177:10045:179524/2 +CTCCATCAAGCGATCTGGATCAGCATCACTAGCTCGCGTCGCTCCGCTCGCTAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:946:13566:137436/2 +CTTCCGTATAATCGCTTCAGCTTTGCTACAATTTCCCTCGGTTTGAACAGTCAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1148:656:9363/2 +GCTCTGCAGTACGAACAACTCCTAAATTGGCAACGCAGTGGGACCAGAGACAAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1498:3307:70020/2 +GGACATGCGAGAAATAATTAAATAGGGGAAGACGTAGGTTCCACTGCTCACAGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2248:4900:51665/2 +CGACCGTACGCGCGATTAGTGACGGACGGTTTACAGAAATAACGTAACGGAGGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:807:7391:123290/2 +GACGAGAGTGGCTAATACAAAGTCTCTCGGGTCGGCGTATGCCTAAACTTTATC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2455:17864:5895/2 +ACGACGTGTTCTCGTTACAAACGCACCTTGCCCTAGCTACGGCAGCTTGTGAGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1257:14923:50658/2 +ATGGTCCGGTTTAGGTGAGCACATCGTCACCCTCCTAAAGCGTCATAATGAACG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:492:12915:12514/2 +AACTAGTGCTTCGCGATCCCGAACTTTTCTATGATATTAGGTTTACTCTCAGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:235:7270:100350/2 +AGGTGTAATGTCACAAAGTCATGCTTCTCCACTATCGCCTTGAACTAATCGCTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2260:14085:3683/2 +AGCATTACATATGGTCACTCATGCAGGCCCGACCCAACGTTGCTCATCTGAGCC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2169:9651:142664/2 +GAGGTGTTCAAAGACTGGTTAAGGTCACTGCTGGGAGGACATCGCATCTCTATC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:540:14663:48756/2 +GAATTGTACGGGATTTACCCGAGGCATCAGCATTACATCTCATAAGCCACGGTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1091:11404:166341/2 +CCCCGGTGATTTCGTGATGATGCCTTTATAGTGCTTTCTAGCCCACGCATACCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:608:7328:131607/2 +AGTCTGATTTGCTCGCTTTGTCACGAGAGGGGTATCACTAGATACATGGGCATG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:522:3049:47753/2 +CGCAAATCTGGCGAGTTCGCATCGGTTATCATTACCCCGCTCAGGCCACTTCTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1283:16887:169154/2 +TGCTAGCGTTTGGTCTCGAGTGGGCGCAAGCGCACTTGCAAAGATACCCAGCGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1095:4319:4606/2 +TGTTCTGGTCCGATCTCCGTCTATCTTAATGATACGAGACGATTCATCCAAGTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:752:19585:108953/2 +AGTTCGCTGGTTGACCTAATTGTAAGCCAGTTGCGGGCCGTAAATGCGAGGATG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:752:3015:99805/2 +GTCAAACGGGGTTGGCAACCTCGCTCTAGCGTAATGTCTCCACCTCGGAACCTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:332:4846:77772/2 +GACAATAACACAAGCCCCGGGAAAATTTATCGTCTAGTAGATACCTTGCCATCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1140:784:66805/2 +TGGAGAGTAGTCATCGCACTATACTCACACCTGGAGAGTTTGCGGTTCCCCTAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:473:18338:33594/2 +AGACTCCGCGGACCGACCCATCGTTCTCGCACTTATGCGGATAGGTACCGATTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2702:3903:96584/2 +TGATTGGGGATTTACGGCACGATCCACAATCGGCTTTATTGCGAGTTCAATGTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1797:11052:97278/2 +TCACTCAGCAGTGCGTTCCAACTTCTCCTCGTCAGCGAACACAGCATACTGTGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:820:13922:80241/2 +TTTATAGGATGCGAGAGCGTGAAGCACGAAAATCTCGTTTTACTCCCTAAAAAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:555:4732:87380/2 +GTCCCTGGAACAGTAGCGTGAGCAGATCCAAGGCATCTGGGATGTTAGGTTTGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:659:12505:103681/2 +GTGAGGCCGTGCAATCGCTGCACGCGAGCAACATAGAAAAGGCGATTCTGCCCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:59:2864:12297/2 +CAATAATTACGCCCTTCTCGAATAGTCCCCGGTGTCTTCATTACTTTCCAAATT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1383:4754:130496/2 +GGATTCATCATGCTAGTACAGCGAGTCTCTACGGTAGGAAGTTAGATTAGCGCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2240:16766:66728/2 +CGTGAAATACCGTGCATGGAACTAAGTGCCGCGGTTGCCTCTACATATCTCATT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2100:7817:14667/2 +GTGAAATCGAGCAGTGAGTTTGCCAGCATACGAGATCACCGACTTCATGCCCAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:895:13916:72786/2 +GGCCAGACAATGGCAATTGCACGCCCCGGTTGCCATTACGTGGTTTCAATATCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:418:13660:161873/2 +TAACTCCCTTGAACGCGTGCCACGGGCGATCGAGATGCGAAAAACACGCGCAGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1283:12361:37079/2 +TAGCTCCGGGTGCGTAAAGCCATAGACTCACGACATATATGGTTTGTCTTGTTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:347:5935:115159/2 +AGCAGACTTGCTAAACCTGCTATAGCGTGGGCACCTGGATCACAGAAGAGTTTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1983:1677:46718/2 +AGCTCTTTCAAGTACTTATTGAGTCGACATAGAGATCCGCAGACTCATCCTACA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1380:1444:154221/2 +TTATAACCTGCTATCGTGATGGAGTTAACCCTTTGGTGCTAATGCATGCTTAAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2040:5556:131112/2 +GATAATACCGACGGTCAACAGCGAAGTCGTTGGCTATGGACTCGTCTGTAACTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2789:7394:44852/2 +TGTATAATCCTATGTCCTACCACCCTGGCGGAATAGATTTGCAATTACAGACAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1215:14424:11602/2 +ATAAAAGGGAAGCCATACGTGCGAGAGCACACACTAGGTGACTAGTACTTCAAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2475:14123:112282/2 +CTCTGTCAATGCCAAATAGACGCGCATTTATAAAATAATAAAGTGCACCATGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2546:19759:182333/2 +GATATCTCGAATAACGGGCGTGTTTTCCTCTCATTGAATTAACGGCGAAGCTTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1849:14231:162499/2 +TCATTCGTTAAATCCAAGGTGCTCGCCATCGGGGAATATACTTACCGATGAGAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:662:17029:25695/2 +TACCTGATGTAGTCCCTGCTAGAAAGTGAGCAAGTAAGTTTGGAGACGGAAATC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:566:4782:154015/2 +CGTTAGTCCCCCCGTGTAGATATCAATTTTACCCCGGGAGTCTAGAGGCGCCGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2085:5947:93787/2 +ATTTTAGCCAATTTATAGAGCAATTTATCACGCTGAGCTGGGATGACCAAGACG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1281:315:57513/2 +AGGGAACAAAGACTGTGTCGAGGCACCCTCCCACTCGCCATTTTGGTAGCGGTT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2225:9077:172869/2 +TTGGACGTTTTAGGAGGGACACTGCTTCGACTCGTACATGTCCGACCTACCTTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:373:3420:105949/2 +CGACATGAAGTTTCCTACATCTGACTACACGGGCCGGAGGTGATTGGTCCATAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1805:9722:68857/2 +GGAGTTTAAGCAAGTTATGACCAACCCGCAAGTAGCAAACCAACCCCTGATAAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1350:15373:181220/2 +CATTGAGCATCTTCCTGACAACAGCCTATGTTTGTTTTCCGCTTACGAAGCAGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:44:17607:19283/2 +TACAACGATCGCTTTGGGCCTTATTTCATTCAAGCAAGCTTCCGGGCGCAAAGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1732:2108:100386/2 +CCAATTGTTACCTATACTGAGAGAGCTCCAATCTTGACTCCAAAAACTTGGGGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:250:15883:128840/2 +ACCACAAATTGTGACTGTATTAAATAGAAGGTGTTTTCAGAATAGCGCTCGGAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2113:7932:11007/2 +GCAGAACTGCGGAACGGTGCTAAGGAAAGGTAGATACCTCTCGTTCGGTGTCTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1891:12437:47748/2 +TCGCGGCCGCACGAATCCATTGGATGGTCATAACTGAAGCGTTGGCTCAAGGGA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2369:5530:14886/2 +GTTCACGTATACTTGTAGTCACCACGCGCCGGCAACCCTTTTCTATAAATACTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1742:17056:119120/2 +TTCGTATGGCTTTACATAACCCTACCTAGACCTTCAGGCGCCACAGTCCTCAAC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2687:831:113506/2 +CACAATGAACCAACCGATGAAATCCCCTCGTTAGGATTCAATTGTCCGACCATA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:451:18172:127749/2 +GCGAGGAAACTCCGAAATTGGAAGACGCCCATACTGCAAACCCATATCGCTCCG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1906:6140:154562/2 +GTCGGCATCGCTCGTGACGCAAACGAGTCTTTTCTTAACACTTACAGGAGAGCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:745:10175:155937/2 +ACCCAGTTAGCAGCGGACAAGTACCAGCCTGTACATATAACTCAAAAGACTGAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2679:13605:42940/2 +GTCTAATGGTGGGTTGTCTAGGCACCATAACGACCCGTAAGCGTCATCCCGTTA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:525:19584:47076/2 +TATTAGCAGTGCGAGTGGAACGGGCGTAAGCTATTGGCGATTTTCTTTGTCCTG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:541:14390:18530/2 +CTTTACTACCATATATTTAGAAAAAACTACAGAAGCAATCCCTGGAGTACGCGG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2371:19010:102788/2 +TTAAGCCGCGTGGCAAGTTATGGTTAGTTCCTGATGCCTAGAATTTAAATCCGT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1477:14980:45119/2 +TGGCCTGTTTGGGGCACTAGGCGCTGAGTTTGCAAAACTAGAGACCACATGCGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2719:11588:38543/2 +ACCCGTAAGTTGCAACCTGGCTGCGCAGCAGGTGTAGTCGTGGACCCGGATCAG ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1734:8972:136811/2 +CGCCAAACGAAATGGTACACACTGCCGGTGTGAACGTGGAGGGGTATCTATGCA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2208:6046:184805/2 +CTGCACCTGTAACCGATGAATTGCACCCAGTTTTCGCGCGCACATTAATTGCTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:173:5726:155286/2 +TGATGGGGAGGCCAGGCACACACCTCCTCGATTAGGGGGTTTCAGTAATGGTCT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1308:11903:50795/2 +TCGATGAGGCGACTAGACGGAGTCCCGGTAGCAACACGGAGCCAAACGATAGTC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:1898:10887:153698/2 +CTTAACGGTTCCGCTCCTTAATTGATGGGTGGTAAGTACCTTTGTTCTCTCTGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:772:13347:20065/2 +GGATACCCAATGCCCTCGATCATAGTGGGACATTATAGGCAACCGATCGTTGAT ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:2480:4129:148337/2 +AAGCGAGACAAGCACATGGTTGCCAGAGCGATGTCCGAGGTGTATACTGCGCGC ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +@HS46_3182:5:256:18917:52058/2 +ACGCGTAAATGGAGGCAAACATTTGGGCCGTTTAGTCACTGCGAGTGGCTTTAA ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/t/data/test3.bam b/t/data/test3.bam new file mode 100644 index 0000000000000000000000000000000000000000..1b34227c6e9714d91e03149df909547ec273d0d3 GIT binary patch literal 97371 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 O9Uf%MxjJ! ztT-kyhL>o86(Z*zP4Ku>a`JiRZr2)^vPo7=*d0PQRwqrW&$_^9m1;Q{DY#Ip$?KUd z$L97Oqc6sYGshl%4`Lzf7pT5`$|-s9YU`?gW}oX|)zv{Z^_qn$6Xl+vmR1)St&Nh7 z-ckQ~FwRbL3{O5YdZi{zg=+;$zEP zVnuOPq5RyZ4yMwAk`2=ZvOf?OX{((#(7=_1GMMCA$}0U^$0C zzE+h5vJ#LUqNLVSZqR0qRxY)-B_kyh^kA{Oh5?5xImvvnk_z}Ck0bIGy5DcW=+hNc&oJ0T#~7c{_m;ggE=gxntW*J zDJF3|`;EC8Tk`Bf zWr_E`T#i6(++WVA#F@`qy;qMui;PO4S*2F*5(elKSC;3noH?BHE@qce`rI8kvG-^> z1DkIFF!;pYi$L>kfqTMkTtUD@O-dv;bF`-5bIGhZ#8f49pNa|p4kazH+ZCe8M92W* z!Og9{C}8ry>@2;7=zD(A8j*r7P(wfu#T#pn6^pI&yq4o$Qt63Ra%{bs3CBf9MQ#hs zb4Hb#(e~1e2>ZEjlDOVY$rR;|jMWiV>L$mUEb%6Z(CA~X9?bKqoB76E&tuo2Nwn4M z#rs%5S_#+zRo6{@dE&CjUZ#>=2*!goQpE{* zxc5Al5j&(xWWpv9o6;YbHEQk)JT){4F&gMob?BYW9IZ|gAXj!?0$PFX*+ZA?b6?;& zM6qaaCDtxJhca{joQu(;QA-BJ$jO^-EkHW%$}i7yk-Rri?VMST=I)MwVEL8`8yB_; zTNT@YmX)T4TFhZN$~{^lNo3qJx7p9WA|rW0))JUf#)6hjv`!JtIV{&|v@p$9AWY`S z%(K9pF*&QJS-LaHFO`r(3zma@pP?t#cC0^L$6|iyzuQWZ2c`zIYAp z3)g@jt^Uf(rT#r1U#-6Xf87UsdVFztc62sPB^`pSKv z1mVri|{R00w)gz3$jy#eurF`+AD1PLF|H?lV%StayPYRpD) zr|#wN6G)96NF141snVvqRQCoXnW%)ZHPoEbsT&{r1Y$f!skfS#tDCykx!0&s3LLBb z0X4Q6qeiKEXr;%T&0VHLv(F|%6eqIk&7=VY3EJ|K{|yVy0Nmk?%L8sZ$WSLdTE$;}uw zEu`3bPn3qHn9(9!s{<=D%n&_KZ`x?YkhgwuxNv&6Tz*UO<0V!2O^1WtkacT_gUYDjf-!L{B=ZO!v3bsE!Qj{ss=%%Z) z3akbaEuWc8E-Au{#K_DL9mJX;<%jHS?T;8$QOog>Vk+!s1k%SwA2mjfJbCD{S2`bxkB+kuo(W?5FOY?v;ocH6x zxi>vAXVELg6lBa+@sy?}gYDHK8G4F-2ys{Hj2kuCVdq?ci$|W-SxbRdt=N#$p5{EA zV0WeUa>^9^tRlQ}1J3H0-JJWZR`bFb_uETvk{`Nb^tZ3S^os_Bw|WzP;A>Z_zxjXe z1KAv&A6}juZ`Pxu;_&3;$#W4ya@9>SPN;{X@h&#eQaiMwT(K1se>=RFv)T*Nm=#kp zklu<>a8Y|`6~yTdvupN)G+-WYtEfrESd--fB%zXOnqn01-==*`{WB-@pq3{xQ?`09 z6?SM%*$-0cgv1g{hH4Bxj#nqSa|=^fQ{Ow%ioux*ZD^U-5lVFGOCECq-~CShevsP2 zS20@#*IaZs-tVf#etLcPdwTmps&2eODeQWr@xmRGI3RGe!$kf)#r~M|#Z+L-IASN| zs@zrMi%VPmDr!7hl9^U$ya641E{RG@OUwMDN|@uVUs}T8Ge!?%0+AO#^$7TTN7{Y!$!r)c^haxA2)%XF zJ8g$qPJ2Qck}DZ4lpx2ZEt(jrsWLAvONoWOHV2so-6LnxAY^hWPo3qycccaiW9j5O zT8?T=zPaj(Rn?_sDd(ct0JuO$zv4sS+=0C-U=<4iw7mO`mHi-X)T5eNBbg}UnAGL* zDsYME-MdovgA}Wp5*gifl{f53Km?wMy%-iyT3lZr+k_+p_wuOV!<@*skxapUEJ!2Y78Z*Waq`$v$PsDa0$4> zEZ;pvl+`H9rAT8Zl}Z^ZHGn3!!#m)6%BLW?Swf8np+$5lkm>)DIluee$vv??Rw(Z3 z0*}{jtlvAKasQ6qeInMLkOI`iW1yjz`)s$AF+%$O>!rGWmr3dq>(V*h9|}L$a8n>~pQ;+0hQKx9%zS zNtuhU9HQ$Itu_@Stfo(=`m^6V(rT>Fc!`m{G4E=q{W@m*F;8^~zjvgr1Wi09=NAQE ze26I-RZ-aCZIC_XQ{qyp_tj^kDGnU8oH|B!ORM`Z_M@tWkdkz0AW&QMW!x287*fh5 z^0+bSbtL@mOQSHhf2IbWy?3NzjuKOD$#k{43GxsLI2#F#_QV~c8OoZqrhEHxJ|DF;s>U^13sr8O>Xw`WX7jc9zN zK#T^}ILhFE!n!m+O|1bdIV7?K6`UXY=ICXYx5xLjCtXXeIW-6|cd}X<$3(gwr-p95 zcci9A#!I9ss!JAAM>P^wkMrJZ@H_nDHBsxQpsw+DOBfpX(zlqx-w0e`e?aH z^3~2%4pl;0+Ug@n;=xBT3=Ta`B=hKjlBvFy_l~r7Fj&r9b-0?l!`fKZrZw2G*OxvDGM3{+;#PD8n^zzYZ|xy^yAg)$G*HZeCGV(^z!^@Jtoi` z9j%`{d-nYKb>r66)Fu1R!kCZRa*RcNc)%ph58ediU`HSxwX2E%yEr+O`V}Dm>NOVe z2dmZLFKz)jKDxL(UmtBQFV5E&$4}(>Q$upU0km5L#1aY#h6l7wuigaYU`HUHvg^h8 zSWqxw_CO$i=lV;3?%JCl-|^-z7?2;@26B3Ic)311KRUi#Zw^mhtj+b*m$5G2JUlvC zo-M67SH{#Nf^pGKrN^Cjuvzy%5 z?LUo?y1}&&scHeQ{h{McUxVUc7Zhe9%l{ao1UA7x6N*+xe?c*h?7(W!15y0Y^*8>i zq4;fEEAnSxwfgp7y^rGf{QUBGeR6Smy51b0KY1qR$uDx@)p~d<&wS<-%}C4TDq z`@ZT^tJUw=^5O3=fBeQ@*s{d&W;pVCvoRk&KR-TxVlKSx#5CGjVk?mevx(iBq*WIj zg4k~TtaY0QTzj;BV*yanr{H(@>46>B!} zP?~az7Nb?k_--BB2)oW!|LCz=&`=~-a^jqQbv*h8d|8Ix>MitkWN1NkAGn-lL`E8j0%r%ze zV6=g9uuPpEJPG56ufO-tUYq4^qW*p>^#6W4>l_{*8SPlxO0JJLCx>TGu0|2w)(b~e z$EERBJlFWp!LL7Y{gppxFn;S+Hu+gT+h8q3hmlhkJ(H7Lr=EQ2->FH*$wX@@sCr_V` z@s0QVXs{yj0gL77HZ`{V70Gdf2^v-Ah@!-BozYWRNW|Ij_8gJiHmobsCYHdAg z2b}nH&B8wFLnz+;eDzlA9<^O?98N*$vWv{Ha$(`0x5E%L@Nv%VuYXCzjsV z=k|Y&FV;gqJ->@I0mmSg+Y9BGLL~>|z_6x|tc~Rk(t}-)hK}P|H?lP1vY?N}qPeLr zW!DY3o7DWXFDt~YO-C{P%GN!vw-(N;PFupO?z+T5YaDzpHegZbc6}}H#;E`KHOv1y zzsBJH(tW5$YZEpMzr$(fevr22M{ihc(frdl9&PJBVL1X(bNBXAL;4Q>E+4h!idcJ=+JnTQHbZy?$gl*y(pPudRz8fN<|l}Jf=7`UHmOz zx3&vE{i>_}Ui<1l_G?@5e|~21ba8rgWaQZl`(#k(J3*!Fn$Pf^UKZg zdUG};&E4>}6qKcp@kV5|viX`CBx5B;&v!KGPhO)MJH9V8{4LUNw5<78Ke2`M2IF|mVw%;U83WF^f$@9UZ5n|VwYEWU`^^E27*SP-C>L$bgiI-tBKKqBO zEh4%$A3ffjUR<7^tk2G`bmG>5K~zVYQytH3XL#uL_?KP*@`aa18=rmd!|hD-Mj&H$ z|JbWfY)~xWG?ReiO)~Fg4EJ!xJ3o4>9qec|tbvjTZ_;bbihXyh9qec|=aF+xrnY<{ zRUG#V8ImtM#Kr1fs`(GENi{$5n^&v<_L=)u8*ekLk5AWU2KMOm>NxLis$mUm5m2d; zCo>$h@epp8fV(d5Yk(f?0JO)>p>%KwN{*GQU?NGS?eO}>?a9Od_2J8$_}wp~_g~nr zudKFT-`E^o9-WWr*}^UOh7s|=@5H04gSzU9_{)YzwOaYzPm;7eIH+~ ze*af*MZ?JeaBKx{Y_&gmKFoDnc&{EN1L!sNV!Ye5%tN29d<~9+-Ef2iO&}&m+1q=HBxM_5ObLvu=2S0 z6CS$DaO20{_vqDtJ3Cmde*W+|C$PvzOoZt{zh{(#oq|A;LD}GWtro%KmUu{`^<+% zgwNIn)bj1gcxC2B5ZxqxsgZ*-v92Wa;MrGSdIiWW!5p8pPj318n}OUMY&A=wVGLPR z%lomEhEOeWjja04R$INvY6m-7O|g}rjmI4`oVr!^Hme=%YPFsufT`{jO7gK~XfB6V z`p(x}fAkvI4?ntpyfQxfUaLZ%{meFvJT&=memIKJ+1Z&f$!E`AhQ&=HY2&dG;g*s` z$J8CSd%X)cHRW#a!cFaY{NZ-*!kg>N?M8qDFm3_0W_6|&Tt_S2^wY z?At%SZMdVu%j46FVK-atk$>gc{rpR%Sw-X`%Dz2#@b(wCfIPaxX~$MFnj&R>*v4b5fmN7=`J+X*d zh>9bVrCLi&@80qC_#V*z`kK=EuYZ$8#HY71$pnfFA4s6q+}9X2ssSb_3fYIUtoC`V#j76r*!WF=#TiY{kEf zk>~({cjf>$dHHugel;(D%re_2+;$N;H;|jt#)!pgnx3=p z)QyRI2dCp}zw7Z$UcQC$m&{(@gWD(YmX}YCPV8?-$7h?9L7cV^;B9$BQ|&tL!%u0x zmq>p6`dk0r_P({e2`C>&=UU?~}um(<7@?W{^93GbZa0O?*;{8gee$ zVm8qPQ_nlU3(kXg$LTwad2wMpWunHNC_^qZF1&pd9i>+y{_(3C;$v3)`!{S!&|@Ph z7glP=`TgPg$uoKWbo-<-b(ec+u7>fl@+6SQ_qN@>{P@*vw=bBO`IlRi>Ey(UzL6!9 zG-vD0`ofUhxHd`+CW*j#hGr$<*n5&Z* zE2P5aYK72Qv3O^j-FLGmAH3>j-}>3r>W^=8%(JtL%gyoGKz+DAxBT(4JN71%nP%?^ zT*a~JLBF0EoLfK+b_4>JJz5wu{6fw6(9Mh+s{02&xJjwE{{Bae2CYAKAIRBg-dS5> zweY(*KfSnmNOX(pqL1U6u}V$VQ|t^>$Ejl9`OtG8XiIKI##=;S#VF+vdJM>MbcQSt z?U*qFQUdnORil-Pq|l(%vg^snKG4>V%qZhxheH?95})~oMT)$8Pr?4sTKE)zL{2PU z8*D1BCsUAVhs#p?Lz`0(z$$3!TgJYB@zuF5xWnLy{h>7>nV5Y{B$ORVpltFol{oca z-aKecb>=Kap+$R)>_Moz67tTC#C!2uY!o6ucilmg+y=XdRY;bY6S9<;WA)y}FuWjw zBr94`cG!2@qbDfGU=_@%k=|0e$5>2WN#iB@ZI2dO-UlvbQcJZl)G?gI67H^jaeJw} z#IE`jvL#x}1txbYCNr(%LjG-?WXomsc zd-U7ZL^ZJrCD1XFylbV<^VBOjv!IQ!V|G1T8cS?~p)vxFL10r4P|ago(yRu{u za*@zdt!c1KMUII)jJO60yLU2Ea}#ao?Bex&Qh}_?-W{XoC*Pgw@e0O3fB_%YNUna zNyd?SWGliYl#UfR^1B~J?9p#~&f@_CmEim6YxOR(L;L#@c>?zy8yH$ooGLiVW8_J) zK-=|nc0Xtx;+PfV*{7V1E2?WgIA4~SZ?lzNNs@f69zje<_6E5c?nUN=h=ogn;9KgY zXic4(S=YGD0@02M?Aqb#`IU$ZAj1!XXVTWO&d)x`@DI5C&$eN+X+!QIe=oiVxfuMwJgcin4(N-+&7FSy?ZHY?9 z`ADJG&QZuro5U1w@Kt+RBEMDcDL879wKB9g-jl4BBCo?PG% zy@sh*)Mr6k#xZ}Zm3tuPTgyUe-1lE*r^ z3lQs*5pkdBUPmP?8T~r)dnjVf;-IK++!^pFVtvJh(;ETGQ^OoGo=PQJgK@* z&D(RPGgGQ55CmIjIXVwPoJZAynVhIhJHvJzM@#ACiQ&!|?xo_J{(HA$3%# zcB2O!qwm=yyj*XNH-|@8Gj+BGU4fZ)Y>g-6Jo)C<%ZrLP zmnwhdgI8y&ec6C~atp|~a(#Tb9-~t?>-7_P`uqmR%S~@1Rbw1JSXnhH5PO$QD8=f) zI~(oso3I}2hBf!Z-BGP&Jg?QvF^vc`103F+TD`#f{{c~0EmrRT001A02m}BC00030 z1^_}s0ssys&3#>r?8$XkuXon#^?G+*m9kSI#+!3)iBymf(>>EY(*u!Gbx)W=2!|wt z5?Oo+^YK6w9uP>N2>Db_l9KZf1Q9{xjSw5#L5?GZhz|uMP#j($QIN=6B3Vj|P{6{0 zD8$B`nw_4Ho_p`VJ$>8%UfsES)!M86ZPoes{i^Dm^U=v<@`C<+?ZIRczx>DT3zMIi zEUrHL(i@Y>!Mi4t@|}x6KRr2FEY42Orpv?S+4N|3baHllG(B8Adi*#&zIc58IHk#7 zcyTfbo&z_4ilv%hTf|U!?1TRKz@L2l!grj1a5~{DR40n`8!wT5=j8oYpM9_X z_H|$S@|SNTogXjH=8M_vY&x4wPmdoxPUjaF3iCWoeqI0bba8%>u9o9Zsqu6$4XQVm)Mk>zR%(sDdS7xy_{t3D7TC8v4eY^|U?r9s8|3QL zh(ug)6qs?j+vd* z0yWwK#Rf4H4H>k~#D_cPY+inmd5;08Qo5bDYV{?!xgb)jDK zj}PubJw56e=O^mFC#TElqYK^qiyO!3WDhdGtVXby zo}SLt3Q~G}H+;rghzb_nbw|l>h~uB#!|}=M9qylf@ZD?X_)Hv^K}nsaAweK#ZBjsD z9AWk;?qJOhwp+8nlqxrkMVzv&4_~u`?bgh=fXUnFB-ZK+lhG7tt+VmrLHsSOAGyNy z(o1h8>KAV%>Ob-IlganJcP--_YG6D*Iz7^$qw%KGg>-%u7(3dZ?yIK%W@o8TK#tC6 zRDU>{gXe(;U1P|aOIV7jo9^Q(V7&Iy{pj|-cT6UK=*4vsbFw&_&6o4D>3q7J_kf`r zc@5?=x=D!ERRIaPgqV?ohf;zqdCQHgSHRwUX~mLnHu4vhHvHN;pC3IsPuD7_hqjIv0(h?+OUWQf9p2*??u54+ z95*SUfA*QLTi0Vx!_i&yTe>1Mh(YeUXOZ*mx5`Gafr(JNfP`qF*v_*wOc zZ(3thCv)A~xjMwz^zeAry+YmM$}29lqlIJ<1r#hN?}JxLEnFJ8=a2MY8>FckV@fm` ziiwKZCS6hmZ-+G8Akv?_LiKAetpw)Hn*NRVO(s9{-h0|^ake;Ks?0o@%@>bUqpTQe z2Z|O;1Y5)he*ehJlgT%~upWy#EhxpdjT&+qc}M4H2cLtAd{y7IoJ|N)L^I)Q4BQQ*#XLO4dlaDU-}1Mek;!0 z&S>xX{rbPRW@k#DmS-nNJ$nd5V;0YKG9*)6UBzLJ8 za$>hBhEA!E8bTR9 z^qK4egu1tYOHM<5sKVobNL(=~7ZT^JD+6R=S0Ic=zGPhp01*60D^a}5HF{N!&ilA& zTzk$ST2)dS1j&nK8a|xejZI)upG^c3g=r-32%TUQUvg}oXw!p*=M18vc+bv33qkBi zAlSN$x(v}&B?+6x_vZ}4N(o(cM`M+njARq3LQN@xYfdT+5V_sO39N=n1#FSRrV{o! zgUHE8UqV)r=)jwCK#*Z0Hl^$MyEF5GI3F^&YnVb0_{aKDxsx;xpKVFoZ9n+QNOWm9?b+*cxM{LOiPc(7OfNz#k4lok43WkeVVgn`=mT^ExYpvv_D#@S*&T&9c=2#gLp%nHb1uNgE zMTpUWi!gMrwX^L=kYb*sYC+>; z8j0R%oJyrN^-fmj7>m^SoUtH~@zByp+o2v|W02ywN{s_2kvjvCLzJj#EcHhbBk7J< zJ(5y9v6z)zjkOZguh@;%G)iV9%CEW8R|>!}GV@5egDE0H0!85>j&)K4-0#daYO>fk z(n^@jR+Xr@l!6&c24>||aQR9Fg*U#_ecm7hEZcrfiW1C7F6vXv#bm6E*0+(i12LP1 zg(@k}-1tS#^9JD*Ld>BRBUVNNA)3LOc+5fnP{xXw%C%#vSbdE=^d4hp+Yvmg8Ww8; zrLk;+!5NnUs-WtQkp!{2WppMezq3mHN8%H%nv_dw#VO;(k%Cp!aH$#8D%Z_GBN=9} zMVvLMR?Kzd2VKuwfw4u%CT33zG7{x)C8WSDa#lYb>q^qM+J!JH;u8 zKfBuaKKprtXq+@ea86YP&`35BL|m>+lGH@qNE|aYSAr7E7F+`uDQsIKxo|aTOlx%{ z!;G##e|t){*k(ssiA)7b?Wf8m6OLsQ#d1Yu69P>I$T%Q0PARdHY(=WYkw9?7Odd=N zS&AFUFu4{g#j0-l`yt zx5*F#S8K<5y5PCGLg1@FEjZSNy|w*Cyj6Y|(T()FNHj>etCYrikEzqb?BxsNfS?Gu8A%!~`^$lGKx7F@Gqc##&uT_8 z%oHjn4OS#TX)Hk;EY(naV^dum>&%LxttDZAMmf@^32ap?@gpysRKz6yV?g zzq{2%&v^E~D(k69VTfka?6PsOR+XlS9h#wI=7)ei*b=O;FoQSVsaDYdS135&F!-UI zGYEE7nfKc-->=O3=+{gppL=!f{r+ixzkkx-?=Ppz+0mnPp-6A42z+z3cR;{onaZZo z?+GXlwm~5!XH5asC+qnuE-vZilW%R@O^1wLX89{$T-&&(msGBb*orfTQ*kcafblEl zYwx)4b07QK$>blsu~un%x;UFI4iAscrpv|Q{L$lH*m1KO6+#nRLNesoZpuC?ld?#Xg~sH<^wqFa*Es$_Gu8ac+I+u};3S~oZ$ec@?94z>hR ztA$c)aiOHN!CUB^0Wrb2f+j>MjBPki?+i!;<7*{TTngkj?WD3Y)ZY7Hy{Am?5%E`k zZ>^~P>HQ!skLReg1O`BpCG)0+x{9~o{L1}Gx?g_pWb!kwt+h@#(G{5<9;(|ej~4UZ zJ!8F+E}`M#K!pzWSK5#OgJew+Y1>N5CxAWJ60BfjU%iWkD@3n)h&-iaB@Tq&L9idb z0+wG{6`1|C>+S8Iz59i=8pU2;wm4p#Ef({m<43x!=eL3~`rN?hEYUg|YM60tV*m6W zh9A4$-Tv9<*Rtu;G4y&)SBOJuy;n~GsYW21QpB{OH$j7IwJL=Bz$^C);eJ!M>F3r` z*le!JYj(2iMCoWboArXu^R=DzF+gbsiVBLPc@<)2%PrfX3ai0YUG*~fi?6JD8QeJT zx9PY4=ij}%swc<2i@~%z?ozXj(mGw$TV;Fy?kbD~N)~{MCOxUV>0SA*&@_sXoBm!K zn4u0HJ4d5tZ>{=g%`HG0du=@UqA}LQs=}Q&rARxo8 zDss`GPcCnISiUPXwVB>mD_a95wN2U7uF&*)R7Xy@D!1F9U*L|>v=VyP7IYRpZh987 zv&`T<8e0jKop2jFPi4%OgO@}~l-%fc6OEV5-nJmcZ+bnuv#hBp3QQx8sl|<}v37-q zjLkJ#UyC<6kL^>TU@h5Vil(MIw%tUVBIE>>6jmNPGgt{3O@Y{t<=fbqfjQ{9aJZB{ z8$XTO+1dn6nWjqe*87cXJ$Hqscxn)QY9QIUv12hhkNsH2_KJy(`)ceiYx<<30cwm@ znfl0RbfeT}I9TW%W5Q5*NHL(UA zZv29AXC5w|d{azRl^u`1HVJiM2wJn&#j$-#IjF6UD`pkqJUUA+N+#5Vqxe+KrZ28` zw>A`*FU!rcA9|0mdnVc+s~2FdF~!mOjb_TiF$8PGZC?Ggv)eMx2?DG9&AnmH$Y_!a zF@+E{L7;5>5^85zGlmRo*fCRaW1kA3cfCm>dBkWpZp*j394@sOm!;LpGBz54-UzyJ zHY_QRy;B^j;VcePPSuW$M%M!k*Gj4PN*g=gG~}9VOGW)$ImOs^V?*)85t~y79~(`8 z=o|Jro(yi|$ws?7X%i!%Ngp#j%nWS>cr{dD+=YB+e`i4gc z&Y6w7i0)3oWQ$yCu)&rx`U$OX-YcAhHkb`wZSVTUiwh=VCt*~d_C(a%ppDh>D*=0dix;?yzHD%{$o9Em+Yb9$4cYq9S5`x|Zr-}{dnc1u*NPa9k5Bt#vZJ%3rEYN_ zJachtZ~JR^$(&VMB-fj>ptT?+muj6&+rD&Pm6m<#mHP#5e@4Ilv+rE1m6NNrfQ@%NVXqmdffQT?g=Okwn0G*oUBGGPZ2jRJYP+ky&T!~<-5bN z{`#Af$tT{khT?dx08UP(hut~n$MZ*zFD`DEQ>clt*jB09!VL{DDd*QuhWf^DyltPo zUa*@0(x;I9*R{%`XH6XI3s#J=#2CX?yf2)5bjk!GpGqm$*?{AhX9 zrv;whE^wky(HPR4;0cDN1AbsNrg(+oUtgoRDYELH{is`W%9G3D>eKn`Y`HvK9zD7o zpmnQ=&KA{7+>9%cH-5nO(R(Ov%B=cl-}B$s<~KeAMUM$4o@_`t<6oML{*opznj~*e0#NNA9?kDl>N9y*gyU9`pCxR@!8_|P{Ye&ade5| zP7d{%DN|PYn0yVJ9=Lw;>Kp&zt8Y=C+d1@)eZyq(pVp)7aUZ0+m}+KH7Stz=PVV(fuU<(jAr0mHW zf~+a)qH=--m!b*b*$U z`XO;2B#Nc_K4zAY9LJ#}l|iuIe+BH(yH+yl#>W1ON~qsln`SbY5sf1^p}=ia^E zAV|G_dOVvStH3>-9d%9l_*TE0pT0WkvCaroaoBtsG_Y$lpSt?y|9iD4FW!ptKc(sJ zm)5wqIX!h9f}V7U~T=kOBoYq6j#V-dIsGrDO7Rj)O!%}%J#qg zwfp@meq9&upTBq)^2xk6^qBV^8ne^m>7(BG=KN;;dcUBSJ($jCIYg|n6tbc83)7)e zfM?AqD5i$2sXnlG#*$B23CXr69yap*=rO=X5eK9KBS$OXm0_qf3pLlFH#uW~O{uUZ zCl1KD1=N2^W_GFo1}@mf09yqefnxKj0$mE47~&!18gH-(K+RCF`7PMB9O4> zr15M2z+!;vyzLb^y=F87hEQUXVjq4qv%5I!Tuv@%RuHTtJ~vMot%0HO1Y>|reH2q` zO*oSTA|--8p&@}i3|$`7oivGwh1g;v2%+^?g|NlJ*Y0fAYa+<4K?xRPm72VR9&m@6 zHjV*SI zy3AR#t+64-hfDh1*{qG+uM|qI4MS=f2%EH4w>Yccof&9K=xt@AV^LkhQVeFuA-FwE z3L0Qdm7;!@v&(%lk3g zYn;WbHd{TGQ2O(?9DK{XhYn^!vLTgXYv_H0OQj}i$Ni1Y{(()^mmEz5>uO=m4w#vA zrSs5KqcOm`$T35cM5ap`=wNG5j~jX)ItEy;LYAVFf~D20YB6VDC7DcnsKH9{F(-5? zyOh~_%MRE`4Rxr-eGITFWTH=1EtXoW%DeznKHmcC z9`adL;kHo}$r#$X~P4<-44%?%Z!jR6*Xh8QC zUl{6ZI|kSgnM^Cf&eWtq3!(L~OU36sObQ8EDFI2={MDIZ?me!Ys)nHfG-H4*q)Z?+ zvV>@If0L-N1f zrCBtPl@}_BF`?G19*0C`50wIut<}bC*gBIXYF5BzuvOkeB9m-TT9AAn0%lUGBsFup z9LBzXU~|$n1Xky75^Q6P9Q)u#vqd%E?ves?38D-ZbnB5c>LyhT&9|Y3Y-4~mCAI3Q z^(PViI63>6YjG;X_AnYCh}vqA+^Q+glu()lEE)FknBJgkHuh=VMsZSO%f9Ji(>}gF zvOXo|1kQ*{!9cC8w+zUG4bMhd*=U{egCv@^GzcVA@d&OsJM{K@46u!=RiYgQ*qDkM zWR;1Be)hG8D=SQF#A_N*Y7o?1Q&ui+f*TsoI0jhj*_RZXv5>qFYSdB^E>1t&Kd@PO zKvWS8H35I{z|^a z8jIb>Q^ZseR~skyX=W)N6YK_jaXz!aBzTTjDWQr<__l>t!X8hH5eBa29#7UCXHJdt3xbN%Frm;L9nZKE+2Yr)t}_1 zPr`Q}OeX*K|E#qhSR5|TPN%({%kt!8I(wv0SN%yY+qp|9RnxQS>Ed+Rdu`0-^IqGY9^Y*WqHEa-M{j(N48lGKYdoTJ z+j8$!TcPiJ?S7*se0?(c50kb21c%3`XY<)?el|ayoy>Z7t;e_83VGe~AVs9~k-gqV zl?a|2#i179gX{VPpa)w3bt<(SwV(vi8Q-Gn*;or^WsF+_{qPm2PrSD34|&sS;TQA| z-?7%uXE{4Pn=Y2R|3^p1z3$ z!+zs>!){#m@8}Nvx&OV^Zf1VCI6LZH3}?r)!=uYanU8OEF+|^76=>8uxOHM2S~C`* zZrcJ(u0Z|PYpZT}H+^w_Prv!|Yd}wXXN=zY=wvyYY8-rgvBJ6Pio>a9!LB(WYlPWw z!JdHQU^^Tw^+LR=a7t`Lr#06&e(35u-~ak5g8kL&_2{2{X1zP$$?@{C#fv70quJ@{ z>7(8X>dJmPjLSwL-rEp0)v{|XZrUj5llP$f__Yo7&z8Tt-f`v`P_8dIAQ($cMXOIL z4148|s9je#G=}LJa9?=Z${lRIaxvxJuG95a>m_ZmatB+l95=vHOAqjnx#r#k*%}9J zC@_t)ayhG|S<2BDTQ;6Sv6Gb}mz=I!4w(gFY;WBJK@7C*smUtTf5YpmP=6D>KA}I~ z^S{=^tVa3y;`nTFI6FPmsL}fi-84CB8G1*^Dp@1`rcI9cRv!MwH4Hsp|IxL^P|wKC z9faiKny*nb+Osb-(laeV+QIH&raJQH1I8 zR8zCA*RlR_)*BC|blu5%)y_+`N=4&BR8H-o=ISG%vx@%VqV+8k`++NHUwnO~R&GqJ zx5WI^T9e;rw7dFW7UxDrju{~?AzY8-Wc?__t0HVm2J(JM~lPmqPJUJ z#t>CXM6{k#giU8QeC!^Kn^wO4vrm6vy-VsdU|dFuoY<#Sn$1+fWeUBvN_pQF2eOl= zt=PdCQCK!fZkKOB7_vtt8A4c{5|Nmp% zExY_Xo}L^llG&+-_ltgFa_1OA6*DHQQ3{kSgJZ94NGtzU_)#NyAsabGNc>&uhL z2iJ!xET(76#btNvINUFFw#KbwXu{AirEuL5G1Sm-AwPR7YyOL&&)|*DFz=NSK z#9wWom83P>>n{?5;%5w&))L6~b z6|b&TEa@M0L3Q8r7IjX$@p|l(J|#fnqUtoMzBeH^yG8ZbPJmJ%(>udcCt@yH=+(J> z_}tLc!ml>afZ$AFRUTPQv?#rh8mn`%MIWD?0F9c;FR#wCRn_4bpf!#~)6|{;t^KKw z?lsp|n5ZC715j1IZ_eaGX5DfT>%poYMNyzFi)`IO`Z)u|NWEFQ6Krg1q2|2nQ^mJA z1KLf%2LQ~R*|LgbwO8j&^g+TFPu6#o>kTJUMUo|-A!8rNORY-Sv#pO-#h6P_4FH4b zQxR0m_Pe}o$KyO_ptkgmh$U*=?<1HHvO^648ld(p;4{{MF?*G5y$Nk)k{Eq6X^UQH zyRp%d2sxD)ZL(1ls?RAysv&KwGM_WhmaNz0nR7D{0vIUGXvl)RMQ@(nWTs+`G2Ewi zRwy|cqbbV7lD4=B+>O_p{sN*hQ|~B6A!;;LhOc5|&%8coFp*QV)>A0v@)<wKL6JIJ z0efRPc4g0KUZ9NG#imI~dLIDqih6!jrm}Bw)EKa$yrf_KS5e;r&&ozyd-(IR-Q;>_ zdI23vv$~qO<&aWyCdd}E%68-RT`~3M?TM|^rHtO$K5}4iywz75Xo|o#^)iFd(9|m2 zn9%03MNR!~7=FtfYjM<{Kv1!&MZ~HE-KL~xH_jw|f`cX{sH8R;dUrEpl}pK<9g;Ch z5gYmgWAXhFEE=ztkjU=aiUO!X_LDJPO%-*$49({L7Gcj$^OBEQRY*;ZO5r5Mh|<1( zOtxn~lLT49!FPY6De?tU(@{r+&4nR4QWct&{z}X(r{$R-LI} zEPcc$gi66sW7wjL;ZA@`)nuv6#0I0TPbkWyq*osA+hqf~#H^~W3P8n@k~KAk;FL)1 z7f{#w$avG#f1H?xO%XcE;Px#%IhFOUk&ulVJJ~X(QeA=M_w0(%s{AlbJxYUp&B0M4 zufA1r-%gx`KpGY{C4rd4ee_AOwVJkPD%3d@OWt~oN|3ASyIixAiVpY8>y>>fRKYBW zu_AFZnmt7mz@8oC7DWkt_O#Dg1-)&M#ndci-@+4wU|hE9QC_XnsVb4EWG@2k*&&(f zUDG@k>oFQhfl^Xd8f@J+6{;Mhaibb@QJNR~Fbk)SRbtvRQc@j&@z5=`H039m96e~8k+9vL1>lthxUE+bP z6{+49Wu-)B=sEsZ8>nwp)iQ?)RG?^#vg%XSQ8jx8RHG(4ixQ(mM6Y2~j18f%Y@0&= zKcOiv80`N503VA81ONa4009360763o017S5eQT_w$yr{{Z136_->O`@TlVhQt~!M& zP%eGH;Yg{PB2y;HhRacqNC+s2lqg0)kc)^=WLLR>0wf|7gvFl(L5#r$3{e0p7i2|- z3O7?AexP`*GEKd7ii4dh0#C zUhfWm{nTrEz0d#b+xVT{Kk3cRKl|dB^?G0R$( z$z;4-p1Aw>Pw(G%?t$xl?GwFTE{=O4GyZ8+2`WTqat_QZ_tlT}zZd?({oa>9_^M&= zJvR$9`jD$P#VZz)v`J{sM36cgd1OF!&APD2RbZ7nCt|Qk^BTN3E}$+M7GkP~OCPK( z&M9emaK5nP0xEeZNoQ_j^*M0mSTIotlWaXMpe$LzoehPFY9!N%aV}NWlE>o$>P^(n zS6_vXOzT)gNENuN_s0d4%jB4{$zrojNF@m)z3??hbzDG=G)kzL40kG(B1IG>c~+Iz z#|4xdm5m8nc;_s*kX7x znK|Kf4CcsIG`O0I%raGp}a zxn37nbXCllRG5uCDKBkSJfvQ{?7 z1SaidIxe6R+g+SwktHo_P|PDYWo=A8G8^R?6d%+HI;5Dau=fE`KuBhe3~0?YUWjP0Y9|v^q9%2MsSFl?3p{2$~0_%sGg&s7L1Y zfn)PM<1sT`Oh9{Sch-+^|@V3#d&dV*Nl(f@D&98K6a_C@gj@l6~trsLueX z1<<>_ujsw;{Ih5Kz22uku?=*%94uGE@n}3>O~%8)>;xzQ^#QQ<1pdq~;t%S`eKo!& z%X5}38sq}WJI}xM-LLKS-m+e@OC+OL^m_m34;xE11e)=Dyqb?E)5VE9B^>J&`!)O# z1yXDTm$q83bIU3L!<~m==s!M&Bz2(>U0t%(n>{i7$ocpE(reF5=Wg!@FHGk{PxpGS zdJ=4UYey#2)nqnYtmcEoWPWo0!Gra70K}KVbZAqOa?sk!2+LSvK||fBq!~Ev50DEa+U-qhA?<+pNy}R@2YB*R< zN2}p{w3rM}+yjEU2D>qJCyh1AIxMCxzJ=6WnEEXM0dr+lWUhS-JhYIO+XTXrEYk)O{eU(Si;)IF zEpVZbyAB7WKoC-uP3Rm#JY74NjeCMuRxSr4qR>%h>Egg8ES%vt@40LPdFE9RT2@+n|weZg((${ zhWNyar~__|K~QzhR#_A0>5$pihai2)LA&6>0V8Xak;$6Doed9%ToDGts8ys4SzX%> zc5l^vnGjBKt&Mk1xj!@AT?Abu`wl?2vvVT z8;Lw6kk$}X3gv*tnhFn!3(i#Fk{wX#Ss1J1s#vQ-VF#oEQEDX+R9PunmjkkRErcno z>dI=!^^lx3${I>`j!B5$18$9@)`%ovtwsxVz<|aJXnaYR^Mj22vf55VYvsz^5MK;1qcSwO% zRjH-;0P?DG%Q4ce2DId@*G3f+nX?BpR-dY}F1nzZ*Xe*EUQzQN^E?H~;vrdl6tL64 zVhGykbik^j7rvOvOSFbbb-+SR^vWusY*5-saljR+(nV@W<(?T^4_Uejro@QQ8ibB* zod~xIM1!nm>#LX1wYBZsYSLrI!&U{c0DefxnIqPO$BfL&MO(Aot(LB;Vyb%P98Xpr za79wq+7~CCvRIe{O3o?`PY_taYS-EpbE_+2w91I?O2v}&0f9hBq+&$!Msj~prN@-d zSyWjga^3;Gh8OJ(OUxu+QrnfN+pUkJlnV=1nwZrg4;O>xS}UPhVeXJ3+?5gl!6eDD zk_Rl*6sNwXSrR|u{GbAhr({eKCR4lA0R@)g zMZ#WH6v@d$Dm@X4^DLzhYL(%ToK-jxkZ-uF+1UeD1FYngc~NU%LLQQj_z9S*oti?;s1;V=*!b)+#`Uo>w3+1)co*Q$(b6@4E2r{p)tqOiXud$F;68S$C$&@_c z)@Z>j!`?W$5Pe7>vgA~2joGB?4;j!>bXrQnQi3msY}>F}m}+#YsoJ*t^tZY-kpNf+ zBA|^qWQ!m)9<><>$Ae@Yfb_e%Z>|Q+%v$gwUG@d?uJbP)uwL&A8kb_;)9>}ZvT;3U zG#Cw5!_j2ETn&e_$#lNHpL2G{Vtprtfi`Ok((HmUdy9!{VDvk|P|ByIiV_|syLS&- zKL5(U$IdP*-Rb@Hi`DqZbG_boy<+>e$!IVcV@u|P31Ezevy;;Yr<;8t6e-~A6&2xQ z7O_2m+<6#^eisy3yJBlnR(YP=E^l9;*lfvdcAM{+{Oi<$$Yq40>^4H8_lODr|!PH zvKGQyVY!36Q##gt4-1r`X2%iq81H>!% z#(BJ2U%;%%1g{jc9KNa)HzFF7U5SrBzT(c71Jw(lmwrsE{birs_Txva>^hjFAWex} ztH44VxQo@g21LID2>36v0ns&Ptywz_xfKwp2G4xfT#A@&yMoxZn}2+b-Mk&~>{HF) zzCcu;4@O9Iqv;s&3R!>4D3Tx1|!c-HaIA0 z>t!r@-{RmVF8tOt5%N1hKHvF&x1oStU?m3NIm^*(IwHE|uH4GzzS8`m0BbMO>{}ST z21LIDh#ac0!e|>qbpFMXt-tuXU9$DQH}!ho{mMobo=;Yb*$lE|G#<^C@Fced!un(q zjT3>-^L@$IH6Z#OKtyM~fPB`mn6&rZi_N|BZ+P8Cpk2E2*WkAg{?_((OcyiI-obdW zn$HGvfLIS$SR3c&-nrsc8G$uHTw%#Uq*)1MJeS8GuW@H<#=^T_w>x9ux9;|Oi{EL0 zB_TN-&H;Ho91WLvp5y@4XDM;`bWU9u_q1VLqEK zV9?9am?j*!E3_!d0FJ4gl{Cc7MNw+8)*5K-$6m9CBJFoU3aM#LfXC@7jM9icIdCp) z5iQK173n4s|L523GKL-!@!LM95wEAC(P}haERgAz%h3r9`nZyaRaQw+D~0nhX_t7F z-q=)F=W#UeJqPua*eJ%!&HvLd!%sAPW;6rF;Rs2Pa?EHxTr;e#L9;e82M$j?O4(JF z466uiIzN66JoA_zK8MA{*&L3mGcevE&m!(ss1qQaQlOcS=aUJ@`-4sD*?6~3pn^fF zYIAZ%J5D=q1+%j2vc7M_WyHq2FPHTL*!z3G->}rR_+1XCu-V08usFF-&V7cKbP**= zWmQZuY9mqsB?C^!LFcROJdWxbwEeDVjnrWK+9|ldX3BBrLNwenbyK6B{3>Oc>3kC-2tpG zY=orIl-TxS0M0U;Oh+`y=D`-*4K^t{u`9tqyr@)b@tiptG2C(Cu7TR`1XW4Lf&fl$ zK=7Q*oJPBWadzJ04b)dZy}PP+pGUBIStHPo=M+Cj^AU}`nJm$Zfzz}Q&=UP*OgdZ1GnE9Zh;Zy*KU9W3WS`rvBd&$?!?d@Vtl9Ipv}p@F=Vw(IMUgS6iTsdb{{V0q!WDrFGx zHK#TQr@PKaylJFP8DqE>+-a?i^NJfzcdWGRI&yNOiNE=o4G+D{Pyx*Dv%O|Aoxr(~ z6-KM^e7YEbl@(!~knQf1mkitpry^vrI4_(}LJZ~ED2>PZn;Y`F_F1+?d@J?Zdn2H62ga8#JdN=C*h&glG(&mY66QeEN?Y`r{Ffzp)^hIJ7-OsLE09 zF~0Kd!;tj5A_>KMc$LfCc)iC}_1i;Y4VSuBorE-XkISxix56?7wq{bM0C=Zyvh}E7 zw+_qDv4}1~X)A+v*;-K2YK?9i{P+l@+aogRh_u;(Lr19}XqNpSer$T#&@YP(JnNLb zmF-3UyIVlM=NceX4ftS#$2>y$UrzaP4B|gmtE&t99*u4)&F_77x6=H@FYopK*2kMb zpiF0j0Z8a_HXv3P%$J`7u!ltJ6h)oh<*nU8G1Ny&X_3@I{q*qj9^mJ ztV#q#vBzQZwl}>|?i%sW_VIu1|1?s=XhO4XwRm{QJ7k%6Q z3XpzBAVJW%dUVuYZ}!-h?SkGn%H4wATb}Fn{?pwC?Hf&pD~O{3{Aw|o!mTfWtdG|N zGG&sEMpRU1OID5vr4Vp#>Q#{|K>Hnlst|dMIysIXnDZqTglvU?cvBA{+z?RDBb?0z z=Z!2(mEuZDNH8(2NrVMk?!Ll?RP;+#|g$MBw^)2F;jw& z<0>fD){5GSbQ4>C<(eS(&Zm04?`Rw=O|YfI$#98OiO@2n%J*qwfIN&)(oIz%T~T}A zz`qz}FLyccn2rp8yIC)yBf|+D8G`OD!5L27g9c~NCP!VFl#wYN_Ko|SQqVWOez*Mk z%=5k8M;ktgK)WF3IfStffav7GnnYa~yUBtoE|M<0rM3gBN1^CI_$_ z@-)n}_`n!4EZ_IK&^0LfT~L$|sPq(}Q*+EO_Rt+C@tp2Ay(}-IS&)}C~%U7op zB35AgO!2<+Fd+SoK#DguXJqb#BO+adN8N!`ZER&Au< z*m;o6qB)B^$C#{DU7PuS{2bUvp4)WCT-q$%qWZ>0)6t`DOl@7z$yUakm7)P1$TKyu zD!dcELrl2yFvR^EK@8GnIP^`S_tBh_PHL$FGaX*Lzq8e_O`RyshHdIt`RzYw)|emB zopM1kIX%(NRWibuJ*IDOYgZB(O_fbz_w4rQ27|8?U_STG`!{dBXFXGSB6je*6`^-alxF+YuNDtP*@= zJe{qtFKj=@8=Vo11%xJe544o^OeCjc>RRU66kh-C^SdSJUq$YJp;>sH4Q8v^1kq_a z2fro}zSX0i;TqLza5fR+zGrfqef`mEEbu3>TEEb2bzO|%zJu``D8|e2m^!W>T-jIQ zBukJNz6K>(p;Re2wX2t|ovs`5@o%2rB_Ch(%3kknzq7Mku)xV^I$2GYi_!Sx{^{xc zD_nQok|RSA2A!#Kv`$A ztRK2_);yX)8yO*caq*_gFpWKZu7{y0p#a;&8p{OzM9V`bVGZw7}9;@)6HeE z^Ulyzv_wL&L1bU3ml^J>m3;iRpzS52R`8~}qn7cX+38|>XnXkO835x#dW*p9l50Q6 z+9r-q-P9d01~FG1gDi2k}T2dHOc0-&fiv?o6Y}%Yc`**ZhuSjI>p1U zX zv90-|b+tC(`0dw(W4cQFRFl|0{MzirfG?O9M6*&XOVMWhJ9*&>kbXxXQ7Q?6QGANp zuzjn3n>O38ySLjF{4V_V>mS=WHldEu0ZGaEe7+b!I9yy$+gu4@U~CzIiy8=`ttE06 zutv#uxEi#5%JZ&!yN!e2jvwCIpdh2=WVu>S$1^CN#e6(pA0(bH+{WU;lvK*ZGF9Sx z3=(=``&!?<-SZ~mC12F+>UjhQxoDQLBBrdESIM_tqTAuz@4dInx&Iq}J=@sA8SUL< zHk_{(Q))pwUDLA5E14x~>OB_0gMIHCF0TR6?*O96Vsc23eAP0yuQ6|A(ueQu$|NGK z|FzNc^$3Ex=KaEFpUxgB18oQpa^72;it2hN^!-!TaEat<*`B+ln`jKzkN4fAk#L&%g0ZYhAU7 z_r0pu`|(BzY&lv$Z&A1YcswDkb#WQUoUblnHpQ@%xmOvg%UW(hHn}yPb*O|r6mS0~ z@J7Xq)CmH%h#a%xwb*KOhi>}q0>M|lX*WEa;M>2qNd)Kfz> zXCHhs{>u&ejPIWib6rm|0b3fa3yzJ`PE`eHM^+XJO*qCNhmg;eF`e7sHhk`VZ`$Q^ zG=Svm8-oWQF>GYrIA1H1NB}`Vo){1fqv;UdjIT{WUqH5@J{=a(wCN9CvD>GG#jOw_dR zW6#$h>32m!N7*UUgiB^(*q0b>N+_Rs)9x(APre%RrP(>ST&zhEbxAKrbiCq7I9!Ss zu(HaNLE=LMar>THeCPRB{<$}w!MHj^;Cnx}*Bdo2UrlH&prFPSF-Su_INiz_-+x|X z*PuFy6~~l?YwpWjuYuFQ5jfRAR1q!6QiZf{$$t|>KltX|AWG5fBMq|h$jEhByU8+g z?S1i5>=8sS4{h#}r0s1i_biCgXFtn&y`N*vkuS6+<0*|pU5tlVnBD%CED<=J)52sf zm5;>~E)m>o+v`G)KzsMXx(OKw-MMw2RZQr!FT{uU?=?pu(qY(QO8R^Zw1bmV5+GM5 zh0^=f@G|3JO?#`4Yhd&{!QiGETdLS)tMRl?P+1uhh#P+lIooqJ7UfGBV7ocTwoGzxvu)bK{Qna)aCXW`{59Dx*rtgAlb1FK@;At>?JDLeBZ6NQz*gpD9iDsAaLxEfn?&u72K?$x?(n5nS zs_Z>$z3ANv_L^1ecU(1y>Kve0Lt$9Ceb1AwS+#!0RVyxd305X(M1ZgqN{W@0)wZX| z_FlE0zO&sIL7!!K=iS#f!A_w@$IJCCr@`hhXUpg_{80!Ni>^_;sKNCOApMR&kQQP| zmCN8u4F?1gau%lYO22Kv5sF5WrcMhdMN0wKPvGpz z2`fsd*@_z(%XE`S>8(GO4{Brjc@%vWi(bo3tz(i95Mqosj9+ zJjy}|n;e)AGA5$aibWAg&L106!)S7?;z=T{M^O!@*^yY)(zVI_#*xy=un#a!W)!bl zBwy$TZz#v+Q8cZ~T3?wc)j>yTE3y|R)!J3O-?&Ew6S;`4r1kJP4|!0dci;d=XQw#^ z;!izbPSe);5jdB}bZ*upwq&Q^?qVDO{GG=9sc$L#jOwiJ54EuhnKtBvmmT z;?XHSRzUHC)Q%-11#psUSt$FX+kKsV44_UUTAe2#+&I!CGv|gzU6CgRjbQ3h2)S+e_e+k{D+@lF=xIC=t8K1)uY?}s)+D2BfQLBc{o%EF$rdmF7GE}LnwyyQr0fwI>oXKM0w%KBKf_S&Y>GN|n(N1Azs=esC^;Agk_6IY&bWsmJd z!UUn|I(jq$;yF0yRLqR0E-(7tQk0^z9U37)-Q6zHNLCFa;36NJI$eV6A z&zNIGS&pqPg|MV^VA#^-V{JsY2c7=SB#xZ)r%hdwbcXXV;2JbbviSnLaT~ks9qiNK}vE?Y? zL$2`nXo^gkMO%NC)3tvHx95`a236)k<% zu9QG?b$nr7%c!L)wRP0fOOBM$@Mn*}Xpy zVKOkTwA4qJ_Y2}F7c}b}IVyO8aHY9~`isZrQ9L4#x&ZyexJ6L)_+@>KTpXL7G9-E% z;X)o(P-_YxebF*#|6}8)l^Fw$>SYD%FA7pm({OVy(y{HnRmn&mY@oO4P4pqv8Y(Nk z!}N(;>gYBOnMvzNhY+lWl3k+b$Vxaiq*cjkcoC)ON)N27EKBvul%u;N;jLE;v7Rxc zDHer}H87#*Qu>i0E!9cJk?cdDV@WQ7HHLz`e{4|-{v$l}b4|t@E3!7G1S^9Q$ENrK zT{DugNa*~MZH=W?RJ>`G_b5f;J(mQ#|r6xWJn?I6ga*^`cziOU?5Wy zxRpP;nlwFY>EJ+Kh~N>X=)Rufs%Cj?NPVTb6g)?p5UFxv)_NAW4sCB8z4UA93Z$Ol zj%!)qUDP-y=;_R(>*)X>6#(= zk1d;GqfM%;7G0!(eJT?w_YV30*m6{f)FtnQgxsf-NJnR;Qj5|y*ZCz!3T-D4_lrif z$_6o?743C2i62{4(r9}9R66OMue?&%a!NrGD+{ApuCdm{O6Ynb zQO;5QvnMtqm@a2?y!u|R_ny}`W<}BHx!Gd5nocL9IZZR!yuP%VJjYZF(So3clzAI3 z*}?JF{w|I;zZ|R9@R#Y3-jNv(XuSCxIDp{F+_^OlP4l-4M^tB_?UC;nu0HX#-nss+ zHw?VfNwq`C2kKQX!kGLXt>(+OMTamv1 z9Mw1Uw+EimXRms7ulKJ$r#b&+L9aruU(#O82Q-vyJ>+<6;HlT}2X0M_IawpCCDsl$ z6gs!Oq;qJ$(%%dvyWHU4!w+BHn4dKn&Q^;dO{*Er=HuZBE$aCMHJXuEgHT}Usb+8t z+x}qk8HjxTh5aWtyZiPF5cJtw?>2UKK@WJ2Mw7*AGM-PcWT#swqK8mG?C_NF+LmZ% z%Hjsa-QK$|c8)&#aPt+|N1)hDlf(DJ?Q)@0RzqWGLa~|dr*}TFzGlt(UDm81yk_W@ z%wzCveIJinvwqh#1NE~?@Q}0Awm?dInv`rfYkOGuC110ueXObEpb+%^zRwlj#+rHX zHNsjyIs|Hd71vle<$3|HE^=-bbHl#IxlB5n@8`i7m&Y>H3w&D=E`y zrS$r)p5!F#cwt1Ip!T}XM7suPzXQ;eTmhG(5kf(xka$W3B#)U6J^OdItndvNR(Kh5 zza81(J&l|?Tn=es|7*0p(V={X$JH+)Jxe zL{KbkH=K5+ynXLQw7EnvfJ^*hV=&&7g3WX|7=b>G28-GHCC}!=Pm1wM557woxNM0q zFKp%lZ@A>gp4{b?X3|ZKHvX!oc1gm!5M&-Sr*AIkAp*$j z)tufVIJxh(fNG}{v$}xsg{-4D)Rqu+a=Pm~+cykU85>NI6+FQpJXe#ehB~XJL+STc zfI4Y1_8sz;NhP5|WUaP1{QKkARBi<*RnZs+jg3qPr@1opE@QPkcbznR(?FAvN#vR} z151|xfSUD2gTc8;+6jZ z03VA81ONa4009360763o0FENfea(yRTXxoa?>({aN4IA0tWLJUwzF65=+cSxUG