From 92c7cfa1c81ad1c91e44eb8cc4cf769c1c5c9d4e Mon Sep 17 00:00:00 2001 From: Christopher Hartl Date: Tue, 19 Jul 2011 20:11:31 -0400 Subject: [PATCH] BWA bindings and tests moved to public (was required for ValidationAmplicons) Integration tests for ValidationAmplicons. New argument to disable BWA, lowercase letters only for repetitiveness instead. --- public/c/SeparateQltout.cc | 70 +++ public/c/bwa/Makefile | 21 + public/c/bwa/build_linux.sh | 7 + public/c/bwa/build_mac.sh | 7 + public/c/bwa/bwa_gateway.cpp | 268 +++++++++++ public/c/bwa/bwa_gateway.h | 82 ++++ public/c/bwa/libbwa.so.1 | Bin 0 -> 380907 bytes ...tute_sting_alignment_bwa_c_BWACAligner.cpp | 437 ++++++++++++++++++ ...titute_sting_alignment_bwa_c_BWACAligner.h | 61 +++ public/c/libenvironhack/Makefile | 10 + public/c/libenvironhack/libenvironhack.c | 37 ++ public/c/libenvironhack/libenvironhack.dylib | Bin 0 -> 28904 bytes .../sting/alignment/Aligner.java | 52 +++ .../sting/alignment/Alignment.java | 221 +++++++++ .../alignment/AlignmentValidationWalker.java | 157 +++++++ .../sting/alignment/AlignmentWalker.java | 133 ++++++ .../alignment/CountBestAlignmentsWalker.java | 125 +++++ .../sting/alignment/bwa/BWAAligner.java | 38 ++ .../sting/alignment/bwa/BWAConfiguration.java | 44 ++ .../sting/alignment/bwa/BWTFiles.java | 240 ++++++++++ .../sting/alignment/bwa/c/BWACAligner.java | 258 +++++++++++ .../sting/alignment/bwa/c/BWAPath.java | 101 ++++ .../bwa/java/AlignerTestHarness.java | 165 +++++++ .../bwa/java/AlignmentMatchSequence.java | 151 ++++++ .../alignment/bwa/java/AlignmentState.java | 13 + .../alignment/bwa/java/BWAAlignment.java | 190 ++++++++ .../alignment/bwa/java/BWAJavaAligner.java | 392 ++++++++++++++++ .../sting/alignment/bwa/java/LowerBound.java | 88 ++++ .../sting/alignment/package-info.java | 4 + .../alignment/reference/bwt/AMBWriter.java | 68 +++ .../alignment/reference/bwt/ANNWriter.java | 95 ++++ .../sting/alignment/reference/bwt/BWT.java | 172 +++++++ .../alignment/reference/bwt/BWTReader.java | 86 ++++ .../bwt/BWTSupplementaryFileGenerator.java | 60 +++ .../alignment/reference/bwt/BWTWriter.java | 71 +++ .../sting/alignment/reference/bwt/Bases.java | 108 +++++ .../sting/alignment/reference/bwt/Counts.java | 151 ++++++ .../reference/bwt/CreateBWTFromReference.java | 200 ++++++++ .../reference/bwt/SequenceBlock.java | 41 ++ .../alignment/reference/bwt/SuffixArray.java | 159 +++++++ .../reference/bwt/SuffixArrayReader.java | 82 ++++ .../reference/bwt/SuffixArrayWriter.java | 67 +++ .../packing/BasePackedInputStream.java | 92 ++++ .../packing/BasePackedOutputStream.java | 140 ++++++ .../packing/CreatePACFromReference.java | 64 +++ .../reference/packing/PackUtils.java | 135 ++++++ .../packing/UnsignedIntPackedInputStream.java | 102 ++++ .../UnsignedIntPackedOutputStream.java | 118 +++++ .../walkers/sequenom/CreateSequenomMask.java | 50 -- .../walkers/sequenom/PickSequenomProbes.java | 334 ------------- .../validation/ValidationAmplicons.java | 40 +- .../alignment/AlignerIntegrationTest.java | 27 ++ .../PickSequenomProbesIntegrationTest.java | 34 -- .../ValidationAmpliconsIntegrationTest.java | 56 +++ 54 files changed, 5492 insertions(+), 432 deletions(-) create mode 100644 public/c/SeparateQltout.cc create mode 100644 public/c/bwa/Makefile create mode 100755 public/c/bwa/build_linux.sh create mode 100644 public/c/bwa/build_mac.sh create mode 100644 public/c/bwa/bwa_gateway.cpp create mode 100644 public/c/bwa/bwa_gateway.h create mode 100755 public/c/bwa/libbwa.so.1 create mode 100644 public/c/bwa/org_broadinstitute_sting_alignment_bwa_c_BWACAligner.cpp create mode 100644 public/c/bwa/org_broadinstitute_sting_alignment_bwa_c_BWACAligner.h create mode 100644 public/c/libenvironhack/Makefile create mode 100644 public/c/libenvironhack/libenvironhack.c create mode 100755 public/c/libenvironhack/libenvironhack.dylib create mode 100644 public/java/src/org/broadinstitute/sting/alignment/Aligner.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/Alignment.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/AlignmentValidationWalker.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/AlignmentWalker.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/CountBestAlignmentsWalker.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/BWAAligner.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/BWAConfiguration.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/BWTFiles.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/c/BWACAligner.java create mode 100755 public/java/src/org/broadinstitute/sting/alignment/bwa/c/BWAPath.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignerTestHarness.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignmentMatchSequence.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignmentState.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/java/BWAAlignment.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/java/BWAJavaAligner.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/bwa/java/LowerBound.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/package-info.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/AMBWriter.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/ANNWriter.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWT.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTReader.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTSupplementaryFileGenerator.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTWriter.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/Bases.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/Counts.java create mode 100755 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/CreateBWTFromReference.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SequenceBlock.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArray.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArrayReader.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArrayWriter.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/packing/BasePackedInputStream.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/packing/BasePackedOutputStream.java create mode 100755 public/java/src/org/broadinstitute/sting/alignment/reference/packing/CreatePACFromReference.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/packing/PackUtils.java create mode 100644 public/java/src/org/broadinstitute/sting/alignment/reference/packing/UnsignedIntPackedInputStream.java create mode 100755 public/java/src/org/broadinstitute/sting/alignment/reference/packing/UnsignedIntPackedOutputStream.java delete mode 100755 public/java/src/org/broadinstitute/sting/gatk/walkers/sequenom/CreateSequenomMask.java delete mode 100755 public/java/src/org/broadinstitute/sting/gatk/walkers/sequenom/PickSequenomProbes.java create mode 100644 public/java/test/org/broadinstitute/sting/alignment/AlignerIntegrationTest.java delete mode 100755 public/java/test/org/broadinstitute/sting/gatk/walkers/sequenom/PickSequenomProbesIntegrationTest.java create mode 100755 public/java/test/org/broadinstitute/sting/gatk/walkers/validation/ValidationAmpliconsIntegrationTest.java diff --git a/public/c/SeparateQltout.cc b/public/c/SeparateQltout.cc new file mode 100644 index 000000000..7644c9603 --- /dev/null +++ b/public/c/SeparateQltout.cc @@ -0,0 +1,70 @@ +#include "MainTools.h" +#include "Basevector.h" +#include "lookup/LookAlign.h" +#include "lookup/SerialQltout.h" + +unsigned int MatchingEnd(look_align &la, vecbasevector &candidates, vecbasevector &ref) { + //la.PrintParseable(cout); + + for (int i = 0; i < candidates.size(); i++) { + look_align newla = la; + + if (newla.rc1) { candidates[i].ReverseComplement(); } + newla.ResetFromAlign(newla.a, candidates[i], ref[la.target_id]); + + //newla.PrintParseable(cout, &candidates[i], &ref[newla.target_id]); + //cout << newla.Errors() << " " << la.Errors() << endl; + + if (newla.Errors() == la.Errors()) { + return i; + } + } + + //FatalErr("Query id " + ToString(la.query_id) + " had no matches."); + + return candidates.size() + 1; +} + +int main(int argc, char **argv) { + RunTime(); + + BeginCommandArguments; + CommandArgument_String(ALIGNS); + CommandArgument_String(FASTB_END_1); + CommandArgument_String(FASTB_END_2); + CommandArgument_String(REFERENCE); + + CommandArgument_String(ALIGNS_END_1_OUT); + CommandArgument_String(ALIGNS_END_2_OUT); + EndCommandArguments; + + vecbasevector ref(REFERENCE); + vecbasevector reads1(FASTB_END_1); + vecbasevector reads2(FASTB_END_2); + + ofstream aligns1stream(ALIGNS_END_1_OUT.c_str()); + ofstream aligns2stream(ALIGNS_END_2_OUT.c_str()); + + basevector bv; + + SerialQltout sqltout(ALIGNS); + look_align la; + while (sqltout.Next(la)) { + vecbasevector candidates(2); + candidates[0] = reads1[la.query_id]; + candidates[1] = reads2[la.query_id]; + + unsigned int matchingend = MatchingEnd(la, candidates, ref); + if (matchingend < 2) { + bv = (matchingend == 0) ? reads1[la.query_id] : reads2[la.query_id]; + + //la.PrintParseable(cout, &bv, &ref[la.target_id]); + la.PrintParseable(((matchingend == 0) ? aligns1stream : aligns2stream), &bv, &ref[la.target_id]); + } + } + + aligns1stream.close(); + aligns2stream.close(); + + return 0; +} diff --git a/public/c/bwa/Makefile b/public/c/bwa/Makefile new file mode 100644 index 000000000..6399a0e6d --- /dev/null +++ b/public/c/bwa/Makefile @@ -0,0 +1,21 @@ +CXX=g++ +CXXFLAGS=-g -Wall -O2 -m64 -fPIC + +.cpp.o: + $(CXX) -c $(CXXFLAGS) -I$(BWA_HOME) -I$(JAVA_INCLUDE) $< -o $@ + +all: init lib + +init: + @echo Please make sure the following platforms are set correctly on your machine. + @echo BWA_HOME=$(BWA_HOME) + @echo JAVA_INCLUDE=$(JAVA_INCLUDE) + @echo TARGET_LIB=$(TARGET_LIB) + @echo EXTRA_LIBS=$(EXTRA_LIBS) + @echo LIBTOOL_COMMAND=$(LIBTOOL_COMMAND) + +lib: org_broadinstitute_sting_alignment_bwa_c_BWACAligner.o bwa_gateway.o + $(LIBTOOL_COMMAND) $? -o $(TARGET_LIB) -L$(BWA_HOME) -lbwacore $(EXTRA_LIBS) + +clean: + rm *.o libbwa.* diff --git a/public/c/bwa/build_linux.sh b/public/c/bwa/build_linux.sh new file mode 100755 index 000000000..c713f3963 --- /dev/null +++ b/public/c/bwa/build_linux.sh @@ -0,0 +1,7 @@ +#!/bin/sh +export BWA_HOME="/humgen/gsa-scr1/hanna/src/bwa" +export JAVA_INCLUDE="/broad/tools/Linux/x86_64/pkgs/jdk_1.6.0_12/include -I/broad/tools/Linux/x86_64/pkgs/jdk_1.6.0_12/include/linux" +export TARGET_LIB="libbwa.so" +export EXTRA_LIBS="-lc -lz -lstdc++ -lpthread" +export LIBTOOL_COMMAND="g++ -shared -Wl,-soname,libbwa.so" +make diff --git a/public/c/bwa/build_mac.sh b/public/c/bwa/build_mac.sh new file mode 100644 index 000000000..bfed900bb --- /dev/null +++ b/public/c/bwa/build_mac.sh @@ -0,0 +1,7 @@ +#!/bin/sh +export BWA_HOME="/Users/mhanna/src/bwa" +export JAVA_INCLUDE="/System/Library/Frameworks/JavaVM.framework/Headers" +export TARGET_LIB="libbwa.dylib" +export EXTRA_LIBS="-lc -lz -lsupc++" +export LIBTOOL_COMMAND="libtool -dynamic" +make diff --git a/public/c/bwa/bwa_gateway.cpp b/public/c/bwa/bwa_gateway.cpp new file mode 100644 index 000000000..3f6850e37 --- /dev/null +++ b/public/c/bwa/bwa_gateway.cpp @@ -0,0 +1,268 @@ +#include +#include + +#include "bwase.h" +#include "bwa_gateway.h" + +BWA::BWA(const char* ann_filename, + const char* amb_filename, + const char* pac_filename, + const char* forward_bwt_filename, + const char* forward_sa_filename, + const char* reverse_bwt_filename, + const char* reverse_sa_filename) +{ + // Load the bns (?) and reference + bns = bns_restore_core(ann_filename,amb_filename,pac_filename); + reference = new ubyte_t[bns->l_pac/4+1]; + rewind(bns->fp_pac); + fread(reference, 1, bns->l_pac/4+1, bns->fp_pac); + fclose(bns->fp_pac); + bns->fp_pac = NULL; + + // Load the BWTs (both directions) and suffix arrays (both directions) + bwts[0] = bwt_restore_bwt(forward_bwt_filename); + bwt_restore_sa(forward_sa_filename, bwts[0]); + bwts[1] = bwt_restore_bwt(reverse_bwt_filename); + bwt_restore_sa(reverse_sa_filename, bwts[1]); + load_default_options(); + + // initialize the bwase subsystem + bwase_initialize(); +} + +BWA::~BWA() { + delete[] reference; + bns_destroy(bns); + bwt_destroy(bwts[0]); + bwt_destroy(bwts[1]); +} + +void BWA::find_paths(const char* bases, const unsigned read_length, bwt_aln1_t*& paths, unsigned& num_paths, unsigned& best_path_count, unsigned& second_best_path_count) +{ + bwa_seq_t* sequence = create_sequence(bases, read_length); + + // Calculate the suffix array interval for each sequence, storing the result in sequence->aln (and sequence->n_aln). + // This method will destroy the contents of seq and rseq. + bwa_cal_sa_reg_gap(0,bwts,1,sequence,&options); + + paths = new bwt_aln1_t[sequence->n_aln]; + memcpy(paths,sequence->aln,sequence->n_aln*sizeof(bwt_aln1_t)); + num_paths = sequence->n_aln; + + // Call aln2seq to initialize the type of match present. + bwa_aln2seq(sequence->n_aln,sequence->aln,sequence); + best_path_count = sequence->c1; + second_best_path_count = sequence->c2; + + bwa_free_read_seq(1,sequence); +} + +Alignment* BWA::generate_single_alignment(const char* bases, const unsigned read_length) { + bwa_seq_t* sequence = create_sequence(bases,read_length); + + // Calculate paths. + bwa_cal_sa_reg_gap(0,bwts,1,sequence,&options); + + // Check for no alignments found and return null. + if(sequence->n_aln == 0) { + bwa_free_read_seq(1,sequence); + return NULL; + } + + // bwa_cal_sa_reg_gap destroys the bases / read length. Copy them back in. + copy_bases_into_sequence(sequence,bases,read_length); + + // Pick best alignment and propagate its information into the sequence. + bwa_aln2seq(sequence->n_aln,sequence->aln,sequence); + + // Generate the best alignment from the sequence. + Alignment* alignment = new Alignment; + *alignment = generate_final_alignment_from_sequence(sequence); + + bwa_free_read_seq(1,sequence); + + return alignment; +} + +void BWA::generate_alignments_from_paths(const char* bases, + const unsigned read_length, + bwt_aln1_t* paths, + const unsigned num_paths, + const unsigned best_count, + const unsigned second_best_count, + Alignment*& alignments, + unsigned& num_alignments) +{ + bwa_seq_t* sequence = create_sequence(bases,read_length); + + sequence->aln = paths; + sequence->n_aln = num_paths; + + // (Ab)use bwa_aln2seq to propagate values stored in the path out into the sequence itself. + bwa_aln2seq(sequence->n_aln,sequence->aln,sequence); + + // But overwrite key parts of the sequence in case the user passed back only a smaller subset + // of the paths. + sequence->c1 = best_count; + sequence->c2 = second_best_count; + sequence->type = sequence->c1 > 1 ? BWA_TYPE_REPEAT : BWA_TYPE_UNIQUE; + + num_alignments = 0; + for(unsigned i = 0; i < (unsigned)sequence->n_aln; i++) + num_alignments += (sequence->aln + i)->l - (sequence->aln + i)->k + 1; + + alignments = new Alignment[num_alignments]; + unsigned alignment_idx = 0; + + for(unsigned path_idx = 0; path_idx < (unsigned)num_paths; path_idx++) { + // Stub in a 'working' path, so that only the desired alignment is local-aligned. + const bwt_aln1_t* path = paths + path_idx; + bwt_aln1_t working_path = *path; + + // Loop through all alignments, aligning each one individually. + for(unsigned sa_idx = path->k; sa_idx <= path->l; sa_idx++) { + working_path.k = working_path.l = sa_idx; + sequence->aln = &working_path; + sequence->n_aln = 1; + + sequence->sa = sa_idx; + sequence->strand = path->a; + sequence->score = path->score; + + // Each time through bwa_refine_gapped, seq gets reversed. Revert the reverse. + // TODO: Fix the interface to bwa_refine_gapped so its easier to work with. + if(alignment_idx > 0) + seq_reverse(sequence->len, sequence->seq, 0); + + // Copy the local alignment data into the alignment object. + *(alignments + alignment_idx) = generate_final_alignment_from_sequence(sequence); + + alignment_idx++; + } + } + + sequence->aln = NULL; + sequence->n_aln = 0; + + bwa_free_read_seq(1,sequence); +} + +Alignment BWA::generate_final_alignment_from_sequence(bwa_seq_t* sequence) { + // Calculate the local coordinate and local alignment. + bwa_cal_pac_pos_core(bwts[0],bwts[1],sequence,options.max_diff,options.fnr); + bwa_refine_gapped(bns, 1, sequence, reference, NULL); + + // Copy the local alignment data into the alignment object. + Alignment alignment; + + // Populate basic path info + alignment.edit_distance = sequence->nm; + alignment.num_mismatches = sequence->n_mm; + alignment.num_gap_opens = sequence->n_gapo; + alignment.num_gap_extensions = sequence->n_gape; + alignment.num_best = sequence->c1; + alignment.num_second_best = sequence->c2; + + // Final alignment position. + alignment.type = sequence->type; + bns_coor_pac2real(bns, sequence->pos, pos_end(sequence) - sequence->pos, &alignment.contig); + alignment.pos = sequence->pos - bns->anns[alignment.contig].offset + 1; + alignment.negative_strand = sequence->strand; + alignment.mapping_quality = sequence->mapQ; + + // Cigar step. + alignment.cigar = NULL; + if(sequence->cigar) { + alignment.cigar = new uint16_t[sequence->n_cigar]; + memcpy(alignment.cigar,sequence->cigar,sequence->n_cigar*sizeof(uint16_t)); + } + alignment.n_cigar = sequence->n_cigar; + + // MD tag with a better breakdown of differences in the cigar + alignment.md = strdup(sequence->md); + delete[] sequence->md; + sequence->md = NULL; + + return alignment; +} + +void BWA::load_default_options() +{ + options.s_mm = 3; + options.s_gapo = 11; + options.s_gape = 4; + options.mode = 3; + options.indel_end_skip = 5; + options.max_del_occ = 10; + options.max_entries = 2000000; + options.fnr = 0.04; + options.max_diff = -1; + options.max_gapo = 1; + options.max_gape = 6; + options.max_seed_diff = 2; + options.seed_len = 2147483647; + options.n_threads = 1; + options.max_top2 = 30; + options.trim_qual = 0; +} + +void BWA::set_max_edit_distance(float edit_distance) { + if(edit_distance > 0 && edit_distance < 1) { + options.fnr = edit_distance; + options.max_diff = -1; + } + else { + options.fnr = -1.0; + options.max_diff = (int)edit_distance; + } +} + +void BWA::set_max_gap_opens(int max_gap_opens) { options.max_gapo = max_gap_opens; } +void BWA::set_max_gap_extensions(int max_gap_extensions) { options.max_gape = max_gap_extensions; } +void BWA::set_disallow_indel_within_range(int indel_range) { options.indel_end_skip = indel_range; } +void BWA::set_mismatch_penalty(int penalty) { options.s_mm = penalty; } +void BWA::set_gap_open_penalty(int penalty) { options.s_gapo = penalty; } +void BWA::set_gap_extension_penalty(int penalty) { options.s_gape = penalty; } + +/** + * Create a sequence with a set of reasonable initial defaults. + * Will leave seq and rseq empty. + */ +bwa_seq_t* BWA::create_sequence(const char* bases, const unsigned read_length) +{ + bwa_seq_t* sequence = new bwa_seq_t; + + sequence->tid = -1; + + sequence->name = 0; + + copy_bases_into_sequence(sequence, bases, read_length); + + sequence->qual = 0; + sequence->aln = 0; + sequence->md = 0; + + sequence->cigar = NULL; + sequence->n_cigar = 0; + + sequence->multi = NULL; + sequence->n_multi = 0; + + return sequence; +} + +void BWA::copy_bases_into_sequence(bwa_seq_t* sequence, const char* bases, const unsigned read_length) +{ + // seq, rseq will ultimately be freed by bwa_cal_sa_reg_gap + sequence->seq = new ubyte_t[read_length]; + sequence->rseq = new ubyte_t[read_length]; + for(unsigned i = 0; i < read_length; i++) sequence->seq[i] = nst_nt4_table[(unsigned)bases[i]]; + memcpy(sequence->rseq,sequence->seq,read_length); + + // BWA expects the read bases to arrive reversed. + seq_reverse(read_length,sequence->seq,0); + seq_reverse(read_length,sequence->rseq,1); + + sequence->full_len = sequence->len = read_length; +} diff --git a/public/c/bwa/bwa_gateway.h b/public/c/bwa/bwa_gateway.h new file mode 100644 index 000000000..0ef0a129b --- /dev/null +++ b/public/c/bwa/bwa_gateway.h @@ -0,0 +1,82 @@ +#ifndef BWA_GATEWAY +#define BWA_GATEWAY + +#include + +#include "bntseq.h" +#include "bwt.h" +#include "bwtaln.h" + +class Alignment { + public: + uint32_t type; + int contig; + bwtint_t pos; + bool negative_strand; + uint32_t mapping_quality; + + uint16_t *cigar; + int n_cigar; + + uint8_t num_mismatches; + uint8_t num_gap_opens; + uint8_t num_gap_extensions; + uint16_t edit_distance; + + uint32_t num_best; + uint32_t num_second_best; + + char* md; +}; + +class BWA { + private: + bntseq_t *bns; + ubyte_t* reference; + bwt_t* bwts[2]; + gap_opt_t options; + + void load_default_options(); + bwa_seq_t* create_sequence(const char* bases, const unsigned read_length); + void copy_bases_into_sequence(bwa_seq_t* sequence, const char* bases, const unsigned read_length); + Alignment generate_final_alignment_from_sequence(bwa_seq_t* sequence); + + public: + BWA(const char* ann_filename, + const char* amb_filename, + const char* pac_filename, + const char* forward_bwt_filename, + const char* forward_sa_filename, + const char* reverse_bwt_filename, + const char* reverse_sa_filename); + ~BWA(); + + // Parameterize the aligner. + void set_max_edit_distance(float edit_distance); + void set_max_gap_opens(int max_gap_opens); + void set_max_gap_extensions(int max_gap_extensions); + void set_disallow_indel_within_range(int indel_range); + void set_mismatch_penalty(int penalty); + void set_gap_open_penalty(int penalty); + void set_gap_extension_penalty(int penalty); + + // Perform the alignment + Alignment* generate_single_alignment(const char* bases, + const unsigned read_length); + void find_paths(const char* bases, + const unsigned read_length, + bwt_aln1_t*& paths, + unsigned& num_paths, + unsigned& best_path_count, + unsigned& second_best_path_count); + void generate_alignments_from_paths(const char* bases, + const unsigned read_length, + bwt_aln1_t* paths, + const unsigned num_paths, + const unsigned best_count, + const unsigned second_best_count, + Alignment*& alignments, + unsigned& num_alignments); +}; + +#endif // BWA_GATEWAY diff --git a/public/c/bwa/libbwa.so.1 b/public/c/bwa/libbwa.so.1 new file mode 100755 index 0000000000000000000000000000000000000000..bfa3c28473de8485fed89f8458be2a2bda1cdc4a GIT binary patch literal 380907 zcmdRX4SZC^)%V?Gfdymls-UsOy5MRPp)^6zM8IarF5J}sQ9g`{CLtdLk&RF8)U0I$u0#+I_fB!4<|e57%|LX5#uiuAkvL3)dQ4?%y;3mn+2exc>&% zrMT`@xQo^OQQV(U&+Bl%5Es?wceqC2;_q&G)9%N;0oO`g3vu0m>le8A+lXrjuHWMN zIj#k`_*;!@6|NuQx((N_aJAw(6W7VOX5iv4A#dW}GW9&cg>yfqxq+{Fc65=+$A1lt zgFX%qr#b-c&e$BRaNlsH4Rbwz+x5)HK`uDpdgfyg*BL3e%oO;0fWM0?L*c*UO3!mW zyB{M87_IInxDv;>p7}T~1$Mv^=K=R3*K_J4$7!hx>Bn=0f^riz|w260SI|YFtZk@mGv%v3Sz7M%=4# zEyESVH5?azQ{_#&5qBNeMG8J0_Zsz_uI{(sUXJS$Tz4s46YdtSIt4qoOKqi zwYVnYnv2W8bt^9Z?!Yx$JZai_>VAs4^Y$3778m?=kLj9EJ3PLB>L&xGy>qb7tH#X0 zfIQJb;!fS-Z*mHrjw<|p7&s?j6c^kuLL{`R z0WcSe;;uJe4cO%=d}gR}hG4L={2U4$zbe4|)u0{37hfh}h1PzHe8l@HWc*gC^8e+O zqxfu<$(oJ(NS03nh~Mq)&PKkd=$@Baaxm~8}JxjD}h!*&R zq@V7Tk91Lf3je=T^nNNUe!r79Eve+3laPqttMXfv9LiG4xl@(1{xwPO*b6ozAW{AP zJ@aeuzZCN9BOR$8f2HJEtLlAJ;p%BgYbJ+>09G%BP2p8gdfm}+Hu<2DT+|0YOg~dDSt760{2^me?XO=^8<+(qU1bA z(HCDQ@nISZ{H_)HAfNf~>+11RjZE&+f1(GH6C779>%Q{J>)s+_iY5;0cc+Z}n{(qEpRLmn(AkfLvY zQTR?ZVbC`7cY%^;t4}ucOw`BUbk(k$MG}5njiZ}|9MHc>iT{mKh*K3mUCR_dvN-L1 zXFR?n>A$DSzYcg|f9{a<0hz3gRP<%PkqATKzm4)}2ij8XKtW2stcb`Ahd@s`6htMw zNa=~0(!aF({N<$Jm08g?D}LJ4Ksl<)=~8lQQwsI8EJnLDWqjSQ@HImI3c&5hN}ky$ z=Y&4A=Ge(g6#rdHA7(4LZA#JGla&0|A)W1Q{h2K6m#WM=6n?#ur%&-eP_;`q-k^2n zTk1`uP&!$n(ztMSHK3_`#4De|tr{B*PFLmUBc0=S-5g1%rUz}NlFxy^NPMR%=kE%?zDy#%tMFM4|JU{B z=QNBb$}K0w?iDC{`$v-A>G!`YJ{!kL2A_xC^YmXRc(* zG9~A06#e?YNqT3=;gl5parEt9iqAmGxZ9@WuueIn&9XS{3DLg_z^y^a*`HGW{R)4R z;1m9svj0y1o+|k4E9V{c=t;3-m8zT}s$DtHOF~CKZ&v!YUYCS*r_1#Hs$Uw*B<%F# zVZkTnk$WUw4KwX`ie5PJNOEk8mC`Q9Zd>R#zxI!wHYGP%nx@%mycDa3J^(rL7f%^4 zaf#M0R`Tg8lL*KDJgVehRwMD-RR2Eb$l-gky@jg$FBHBtB=HX^`j=IEYwwoj+$nF` zVDQiJRhH84MeUp5;m*yV@hsq zf0y{t`z8K<)xmG6ax|q6PC2H7S8@g$;`f~5zo1&e z4*wMj-}r{a-yv_>p9Nk4xZSSg{1-<*l|9KJ2ypc=cs=CP}N(k#;;Rv)7RLUYlWW6GWqX9CAU*ld%G%R+8L@{AyIxGy`v{J z$j5$Z+b+{Ksdmj&?K+S$9xqC%S9P2AIr6#ne21#))D*da4e{HSA_p{6{O(k8%Te-o z{5?a^Lw`P%bZb>Xvjx4%jN3ZJ{}R-DEIqGDp?Cc4djy|fT_^ca$)Pbt5ARQD*J+A{ zb5hDb!(pwobpE0xi%aWc<#n;rQmwRl!Qus4X=%msaz2(XTyTArR(j#m(o3r5FQ|`I z)fFx*udlDF*Gey+e%^)Cqmji+OY^5!Et@g#>Z*!Zr1-qjt1C!RoSRd6^`fd+&63J! z_`ie(AirCFPY17T3oX#2R8%sC~iW`K73A{^CVdi({qpmX()Q zluo`hR2U+zs;+c?RcvxqeN2EPVg2WTVC?w$RY)&qAt+;ERbMHQ;)^P-X3Yyxvy0u_ z@rC6J7fxBYq&zlFFc>M$E4_M&Xuwz4?rmi5Im{JZ{2%lD_uE*pWbx9fx>#{}tfqd} zlHR6L){Dy*IW0aXr?pqBuvCk7A(!OO}B9PX(?SC8&?`DpSQ3|TU52E zqV_tC$#qp2WA)@#*pJ-tl6zN4(rSp*bMkt}nlOfa#zhXeV^yyS<5j*8W1^z8c1e9{ z#ge)zfvl@SAyw?%+Nw&8NmYw0wRww4vZRjmVsQuCA-90^g8~gQ-CA z_2t0LXKA7cr-wQ0ooCg?@K}hD)zm=?z>MUmu8O^{q407rfm}{1p8LV|<>aEi3KCxs zTYx^}j3A{~IQDCLnJjDIa^aE+t*WlBbU6o|thc(ps_Gi8I##uCp;le7a0&W?wL{RQ z6)^p>!LU@zvxSC{`ErH;9bP7tF0=XM@ehd^%dodVXDbW zWkao24HnU9ed;RJ8+|Mus>{*zvGW$kI2IO3HtJ!8E63$&3)P+Ctu7yn0VS31`abQO ze|?`kVs#4^iEfbnR$E)QWI4LG_F@5xJa9BvSZy5y1g%?CI`d+!vbPqZTi*Nb33Al*evmP*S)^;YIa(o)?kbG2*g1?SZgSw9wy=(R@mWUng zU&aVbd0k!kbxLH8P|g|KM-^JQ82<}c%$t>cQiO`Rg(_^RiyUlI z*5wV$Mee>SgV@Ef+;f#d5=7-LKxR~#K!$n6mCADZW6C+=-6S!nQN7GkTGb#5>W?bz z)suQC2@GY!qH|@Nd*N~8MLxh#RI-RCF~rn!O}&F%RDP}VI1m20q*nTu0xyn1PXw_{ zud5teSLuSw%g2_NYYP|5yM9dlk}&CtNjLtig)JyYJN9`d}dt^os(mLsGaEp zk*4)RPP)S%Df(#LSa;bD*RmF;u09GUU1A)(EX|Ah9Xy|$+W0I}`nYp)I#e=!P%oty z`}PA=u0AOfBgkz5>wEI*b&9X0Xr8j(Y0V2}@+HZQ()3idBo`d`^ zK5`9K0tRV+Qn*+)cO1N0n~Q#-i~ghq3$S%LQ2VRG$@0@(<$JVwDh^-hD))z=c20A;#Ts;hnW6CKtQ#7rN+6T=+V_ zp6&WZA6E0Z@VX1{cj5gme5MQUUY85F@b2})EEnE=elEv_KgCsko(um?7rwxSclI)v zY`E~lUG&8+ytAgqcSVh@KG1O#D#ay zFJ&(LMK1ap7rw}auXW+oR**(E*~h^)CFS0we#~=)$Y5ZjsRD!e8d1-{Hbv?!tGv@K?C-dtCTZ7rx7d zFLU8-7rxwuKj6a8bKwuW@bg`Gt)Wl<7r5{~7yfD&-tWRMbm22y_{A=Kz=dDp!e_bg zwJv;)3xBN(pXb8Yx$p%pe7y^AxbO`we6b6^)P*l`;g`AaWiI@6E_{s(zruyDb>UaK z@XKBJRW5v^3%}ZhZ*k!pUHDcPzR87O>%up?@atUon_Tz@U3k-lU+==-;=*rq;Vl=w z&4s_!h2P=A-{!)1y70HV@OxbNAGq*cF8q&Nc-w{lu?v5|h5v~Qf7pdz>%wcw&!qoz zmkaN6;qP(b{Vx2^T=+~Eew_;+aN+NB;j>(LXOET1IWGJ!T=aP^`~xn0feZgD7v6B; zf9=8-yYLUX@Fgz%Z(aB@7ye-vzQ%?BoeN*L1JdrR%hGdt3hY&u2aHoLN2%ku} zO~6Mc06vNEdI29KJe2S{0e?Vv7~xg{?<0IN;YI=f{gghvu?6Y%wf8A2r23%HJOknlPIUrm@HL$X!C^9Y|sxKY5D6aEh2 zS^-}|IE!$ZfG;GRO}JRVlL?)aGQXSoDcXM!s`WmknlLd>jeA(;qio91-y^& zxr7@9{5s+D2-gaDH(`bf$ua@&B+L*YSuEhqgwH2jAmAqmPavEl;718FL`Vh%{1D+u zg#7}(pD;s$q$c3I2s0E&9{61JKj8wxT>`#^aENfHfNvx`nQ)tcuP0ncc)ft@2!{!; z6Y$l9844s@1w4=N6vB-HzMSw>!nFdvgs?%lOu!csjuI{w@MOXl5-t$%`GhYboFm|| zgo_9V1bjB(X@va(9!YpQVNJl_Bs_!gfg_^-2^SOY67Ueh7ZdIja2nxD2)7CNNIu}1 zgx3rBAYr=p$#nw$fbeX>tpeUhcn;x40l!Z8Qo^+Y-c6XUd$LTxI|gx3qWj&LR6bppPca24TJ0nZ~`O}J6OmlK{(xK_ZI5atAwEEDjB zgclGl7Vu=kR}(G}@cD$VA)F)Nv4j^A4hZ;c!ixy|1w4}QV#1n$ze#ur;RA<7{}Zky z+$G>4gs&ysDd051b%fgld?XKWJ>m5NK1eu5c%6VhAlyK>RlxfQFD2Y4;MWN+BU~%s z-GrADE)(!h!gT4A#RA?;_Cz_;d?xyza3kR^!1NmoYrwnbjL?kcwpdzEr*Iouo!{)nNi}mYI~eA>p;kcw4%hBx8i14!zLk5QFfQnemLDol=K++uh$2vt6QsE zjl^V+VGcMA>7kyXTS0mv9+6)6?)CvkXOGZ4SA^z1vxT(c|JkT;D#|1Sv60}kP;q*) zk(l&7Bt*?E&xCs3t`_9gtt%FgzSd7Io1w>BJ*Q`K1{T^O<*M9M%2^24!n-hPjA9A)8_^|$={ybH^^QW z*|VeOUyIBbd0hcs*L?UOD9u^HN^7uTe%bs??8In%MKDu)(gS>KKnTnR3@f6UZyM(Q zp4Jw4W1=DBeq-@plBI|)*3xFaZTgCmc;97BiD!2}JW6f4px20M? z9ai2|EWa2airpUjP1HjDMstte^ekvY)~sNldAHugx8aOLI9Q9uBvu5M8fG|H+&x)8 zU6fN~0s1p}HeW9?4+&n^!c$}&0tmoOY--b+r$SHTTRfqBv`fD!DKbFhM+SJbY35Q=q!W>m|3xx0(dqNSo}S!Hzx;Avk|VDl{~s4rio%J*+tdl@o7*is)H z!N!i0^0!BEcu_!!@tp(V)T4uob3`|E=uO{NbwN09Gchsj1uxa|s1PS#c@t_G4wmRm z2jLB|pr{oK=85z?*`bEHMQ?fusjNE>of;cp{|Tlugzn0T=o^ZH87MIV$QQDTf?nh` z(BUOi-2kL(kZ?5h)lcR10aXyvH-v*{;VDl%or$L$@f5^UR!Dyw|Kcf=S^cIrnA>J= zrZwUq0yl=@)oBUGjSvFWTWLwt86av+ehdE|b3Cc>E(a7wxobehKg<(@|F4Gmd{1i$ z#1In0RE%0#d~D3u+JPbnFslf%%7I15>m4WQP!wek+MJQn5&JWw6^6kux7(gWJv}It zW@dYAsA0APFB-VlJ_Jt%mZHe~%(gyZH>Kx3Q)C`-%Hx+El8d3Ju%EOS!cd6z(LB(2 z2!jE`AsPvT!2ymY8Za7})694E6Rkv0#4z)*sf z(KOr(c&AAjiD(mrK*z#TK0U1rFr!W%VHEb7Y)DOSIv=88s}jThhWC4ivQlu5` zGtPR`36jt;&;e8wY8l9^G~-IKn!ClQVsUrN;zoQ;ahoM!|Kg^2Qp>x$x4cVbdAoQ! z277hKEH5p!yo-9v`=%`Kgs&-YWdHKGp^rg0Cq3SUIe2e;-_gOFLoIXCQGQ?JKNdre zjej0n`;g@}-j2cSHc99-K8uB$Q@p9n-rHN=y+cHK4PR4U!!gUlB;3DqruUYYBl&Ck zn)1%g3ZGkD&GCJH-;aa2p{l!n}iT%q_jPdI;JaMseZ!)CIN*Y>vt8tFwr z19jOS!bpl)U~|K-lk;d6(Gr86hO|4q^WsUglkkG_wAc{R9_`>=IBMtu)crY<#A-x#) zfK{BqdCDsGalZ0f#c(lZ1$`icVjaYQgS|rQ;3M3-Uuj3X-gK`tZejDFGrEqYcgy>m zKJvbvA^?CPfwBgK3pSvI3A6}iu5#l zY&W0AU-~&*dGjs9>?op}u?13kkTlX|{H|z$av9-r#J(@xsMn#k^Z|=&jpjFEXS0_u zZD84tLJ*tL!_dCq?-Y$pNRfIqlTs!o8}uqM>9P%()^6x z@))4dq3s^M`G=yGiNPZCK#^xZ#`aLe-V7dPJ-wPLy%+_`mb0&N*jI2vD}#A-)(VQO zwWOiXW>9~N;hdHrFn0Qd)FPidi<=gnorw3qkp~X`pr{od*psZpQ0Be?jSs{>ECcF> z_8yx9cX7#)Q1Gw#t4>TCrTW2d716E=aD_|$s<5YrDH{=w>rID*oH#*04m332UfhnQ z0nbT75m}<6ga)t-%vpChWsJX>WjMVpoX4LyX~*9n${)5|kVvsBs}_r)r(uY4+mP2b z-skvA|3p6_9L!*0%5!X({)z4fN*^;|Ff>NL@%vZ+C^CB_*1pdc*&J)N(P8%(9o*BZ zQeCCTCW`RgiRJ&RVZ7@9B5S}uMvR~`Kkyk%I>L*8Nl9_ir~xnFG#o6mCn*2L)vTM5 z`6Zco*)cMgIGKy>qhiADZ(gTBgHXU5vVe<@QGjquSbKr}mtzz#Ko;<*EZ~e|6p-g| zkYlenMgd!4ufahwAliQr3YFUaET@2gJ>eJy#AN{+WdUs{0R18bh>|G4Os4?9eMAK8 zl!4P#|6$o>EsYbytaT_vt-f$YON>YTmM7`QCWie8?S{4bBP|!Qw;M}`};{`HTPbTJojA?it?_&(i(n!^c;nVj#0xGqUe6%r4vzN5|T?% zFm`e^3J#II?nA5@(0aQ&JT|Gb`RM1Aj2S=yf@ppp(WxM! zGn?6;OmD7-;(*lXBmJ0jCP)LRq=qET=_B2uNMSVH&C{DtlccBgk*-ywFvTNReXwp+9Md@F|4vhovHz6oXDQ$3!aS&-y6kOyN)(sg(5&C5?Axzfy}+ zX{Wkr1N~^REYP3VZ@Fo+`q7r9((b4Ba;-pmukM!Bq|$D2)8_Rnw>FjbPS8GGh5=*W zWwTG=d}E!j2=go!FtFamaHv?VR6#H?$PvQyk*bESvR&~WeJc1(aGpii+$gQOsMc{8 zawSLfJqnz8rT7@p&EP&d!#!^mzo5>3Gu0QbD5XD|PUo?VN3%`QE=tXB;T5(RCfS=KFzj>$Ndb1hA1+2ut4;ixtz=_M>;D7%w9#|@ ze~6z}H$NRyxzM3?Obs3d>8k@ciXyTAp&zRIPRh-`Fm>NcD zKpI%W#H|P(5pKn>p{qNl&XQ>*-xkQZ_!m9We|~~5)qVh;$&eRLbj*0jc_&8Y=& zR^@v6_3y(Cn>tZdSKXSL-&_{vjMqEgnahk6I41=Tq`<)xIIABVnjJEqL--wNDc)p8V&Q*@krkw6)%cpw6~Al<;is&)scL*e3x06O^5~-yQ|nDh!w(tiglMZ`U5^ z8Qf_W2CTx$UZ9ejdRJT-nj5;3+`Hr<;!nn%c1Q-D*mH^n4|8J-TVzr5=+m{R#~%MS zBYIfNEgc1A=)4}IgS~~Z%B5|tTmSKIL>3B12=_dua_gz1P&k*kmk`xaSS(T)Mluq? z{@OzBuf;GznN>_RVbi z#aR>U|HZO}R)Hi44coc zK}YuJuzD>JoPq7dKp15Wr>E9QQOV2HHXJO>TCe5W`F(?_9qsf_~8a*BS||%1krgi{$p87kGRKWSkL1U!9I!v4MnZ zqt+CxLWP6NL)Kz%Xu@Kjev=8>zhT^NDkST z_mCZIdR=IG&)`Geg=Y8+;eHhG2|A7CSUzR)shCf2O5wQhDT_~Ke9GZd4WIHt`HOvZ zHH7`}*_Y#rg*qZR8~`D65qI2XkA$$f;Wi~bzHdJ?4-0RDH%FR#LM^jLhO8+W=&>xl zDIdKPvQ`G7R)a5h_ZrO4x@Nxv6O}xj{U+C0vFJS7S#%zaMd#uA&9A}ji*`iB$Y#x` z7~}urr^OI_fby4HMAFWLt_qcIk@0_**nc-)6w6y;VGCm%=B0*fTivgs)CBepr^#7* z8&;`udxUkfU*O1_mV>aQ4^a$w)ISD7LT?i<*$9`CtjT>^r=Gy6OYQ@YPr~w#&38~NH(6N6F z=~!$4VDt5mIncTx$4&!VR-s{-Z;BpN=5G+@qCRuTa%id9QdsIrbrPcg4rua0^o28C zVYV?QR{Nx3T1@V&3HB5*I&bjX3DFrhWL}Ys;|b?M`qN*a=udX&uk59tDd{OctQtXd zVtvsGP_byL0@PQ;(GGLxG;=GK6Uiy|Gq^pl?`84=0bnUS}LLjHmxnd!GR*+gmQ$4G%}Qcb{nIXZD2x-P_)0*_CK-y{O>dw0A5xYHdZoP$lRm zJ)Hwz)BYBeAH5K;Pvip^GX~?KcYa7+&j`)hl!t6EFhZ+m3SaCq)}0u(;yq3Vlajaz z^RUI<1YVz@ONy1|T*TcS_7^Y8#*M?>h}%Oqp)d{ZL7LfN77n+6C)=L8o79B?`#C`l zzi6)*kb>HNkV>1l0lwQ?PcXa0{t#GeCT7|j0!Wa!Rm2qzg*2fZb}QM0sP>5a9^tcL zChzC3g=V24e|#4oY^Q@{V6iwcDU0n|^PwMxxG3>Jke@EM-$5CIC74rD7wSNiY-g~> z^a)LZDs~c9l;o6j%kfAkTtAf4GU3&H8a!tBa2I9jLqF`?)dDG$Zf_)1BX5^z+;Doxv0nb5m82B zId+7Sh8d?u2-^4u9y>(7-~nF-a!fsJzVLj$DzBT~!5^jQ6_=0f@VI;%Ga?Q)r8N}= zLlNwa#DAs#?iW}g26k+p7)(iy-3&Lj9$3Y`^(6UrIvwW5OZG2eHn`>J7#w*^{s^t% z`6Kr4D{s5`tpxXiUq5pkgI}LyAVuE}vLjOT?M;fTug=o}x?*!avZ74|=wdmo{{=>< zgXadcjsmZU01JF*3!nh|r=uW9Idl|ciSVw#KR`f3MLbxLDdNF`44FMAAks#Q2(Vzd z2oej%$?SO|0xUpu$EM=7h2HTE|JD2~GzWo5Xg20VahfVL3(Ak^q#0w7`-PWg>bL!X zIfVz2m>Ue{J`>4(hwG4G&hx{A-Df-VQ^=Z=8H#W9}m*!rL?%E18BDZOd}pBXb8Fq}G|{Ve|9PFxEc|9OQmN=0W}J zHaukn59*fzv@d$Trw6#VH&Rjub^oOMZTwXf0@Zq!jcX6CSCKG|IWR+ae}jH1{NzbC z1RpzBf20!$FM(7Vk)$^%_DZ*w$rmQ+TR;w zWWR!OV}*|kz=hGZ{0_Yc$2;3=jr@N!B=t+%J_+gmX~Xp~JC>fJ4}AuL8c>7XZ2j_Q z>@svBtH+dE3qK*2|JllcXh4ic6`W&{ZaHiN8OyObAHZqO;rH`l2I0KJYcMK$?7Lym zTnSCVbs$h_tC0rxo2__|**cnn8*%54&+Fx*rZ+9apb!T*jKpICZ6un+;Y{qkLz5{@ zA*oC>4ALyZR$k&(Q4;}w_pILW0!#kd**KLu6#QC+Wr%@VOcXwW$3~id$7&LjHi968 z4R0g9r8aE7YcB_FB>ywL=>XJ9^?%6xFce=o&a?XWeX*T!MxoJIG4kgfg$3liqfmqZ zeZf8)M=WG?6dFurqg#2m;9cW+EuO7w0&Cu7Cx!Fh)o;%QH#{`*E}d^gv$x8YWAc#s z^6Z|^VXlyvW!1wUzf}m2K_1J3E4KzqxCmWd1699%4q%E^UA+>vUPd_V@Ff^aqQmP0 zh+}hP{`>=aVmcb0y&YUOijtLRD_-H|26;>G8>jR?Ojl@hoXXC!oVPWC8BkV6EIR$HC$s zVr*KnP!CoD>@(TdC=0`MzzDE`j(PnJ%tL3RRuv!l>_!w2imw{y(Qg}1OM`-a_On7& z@sxYWUIJdD6|2T=%3-U@ks=@}B?}MO(}BVaEl!7K;z0RrKV}jpAwYWAAx}wN4oq z&hJ|N>*lsqXXL)oYfq!rN)3xuTd)?g&1b&}l?dfuHEy-d#s%zE$jPTnJBBB_hy65) zQo^y4E=~Kyz+?`I?N5WmtUNJ=f1+PCU_a9Ip>6u*ZT3@WvE6}u_GV`0KGuLXk|!qp z=5+`-h&J{L_;e5}-oa*Nk@-0U(tR@Ld(l+d^ZW;dD$tJ^aI+}rhMYcUl^XLJMle*km7MrbpKB7QJ}BeGXc{&P5t)1+ z{BQ<-9O@ajBk{xETY3sB5sKjA5cbb(i&l8CT-nvrCFB5U{Xye4!4veQ5p7y|b{86o zCA@LnclGfHIy04SqokXdTmrUx$IF27RJvA47fep=N0$$}O?-jUurUAZwSU9T<6`P> z&z?C;x?>>K#AeFWD35-;PXKZ)WlZQ1q+K(=pO1E6lb%^(E!GCsYdWkG&Xj1Ph zCls&B_%eAEYOM4C=WULrv^ya4KohTTF;YC4AevTp=9*b5C!UAyAnz$YBux$6)wOcp~ z@3#-*&S90?<{DFdBL^z{E7sK!8P459XjC9M`wJS7)8?Sgr#Zk3>l}Lxi?NP_<$VMV zdE&c3%h~=x=qo%gGXGsLFPj8bH-Zddl5EZ=RBCHB{I>~6`X`cL+ujs8S>)(fP~`01 zB4+fmq?r-B=UZVyjvo3B=YYPd6Pt9?~0TMB=TB`#1b}<-+EP)IaeZa zNSnwDByy@mqSZuRCXr($(kqcMi9AgrVJ=B|yF~gV621VD4@u-FRBme;4yh6OE|5>0 zf##xFuOUUs_4GZ!p5H5k;@*TQOqkT0a9(f1A|_nioA7OsaE|CQdsjQAh2I_Nzl4F% zRC;qEwE{TX{O6`j6oB=gXYG%n2Z`ApSnE(C9Q5dq%=GGyq>FjavpJNQ0!*PV;Z4IE zD^L^7-UNCqruz{sXqYeaq1L${3HuziuJmsjigKUka)_9HJ>Bpx{2hf}V(d7wM)IrR z*cAF2c2O~lOgnUC=&Jtfd42lleZ{?ho#FSOyxH}Q^d7dRL3z;v3&3}a9 z8DSIUOZ^dF*xacb&8W3kQT-E+43Gn2ZzLMZwy5 z*B9}((g*KonLG%bEXUi3-mrOzAHp-!L-B~u6PZxuUFnUORZyk>c<9uFne7Ht2Jg?T z_Zk(4!iab>jl{SOVXp0jvD}aCwfEBO3dqwshtg!L@efnMKqe2X2a;!rDGYv)*gu|W zorl*GzLWb=?i*@;!D-f@AGQSES|&ExxA^VNPt*R44mP~Q_ul;Wqkylt5x_ zX3@9|j zd@RUAs6y3>(6R#Lc>o9=fLX3$6=Z)9sED;7D`J)V?1If0I7r(8B-}pc$2(?dcmce> zvkmJpQ7dNTHp9ZAPb1Q`#)%$p{B^NRk}tEY{xJUz5en(IPKBupSwpy=_A4yrMXk&I z%uPGp;T0hxQn96hMHYYl&W3I$)()8$dP5zP>8HbHq**nYSVAs@K4oSAu@(_9d810< ztbdx3Ug&Eq8}xnIRe%$f{FLfZ=l;2NmK(>)<;vNsf;?DL=@K9tN6gOcTosAS8q z2KLnw2d}8*j<3|RW+(rsj;PE3zgHIu+lVS5RZ5(TDe~3T$8h)=aa2?ADRP7#9^1r& zc-)Fd^2)Hh41yAiQ8Bz-`H1XIjJ*y|X9y++PcJ2g;!r?SFn|)#+mA6{;*KD9Y#Hs; za`CbTlJL>DdJGGwp$Stm_1ho9Q?%kjtZlu8P45f+RxUMb&H~^1OATEvT8=8W6MN~6$TkZ(%2QMv6!H;nx1KgT7 zCu2$xG@JrDN-C5- z&^*}w!LaM@h4&q5?;Z+kj^hBK_}iXvHdbI)1eZh249g2|dS0 zD+4Qmw+6caGPx1=B5QPe?DU9Ljd>WBFqHjaGlu8H_SkVw{h{owTzvpUc3qls~!O_&3- zbM|1iy%F`|U5Y6gz|4m5&=;M;l2PZblV!jqq%J$aK@Nu__T%o^EUWWN_@G$N;5W}T<95tf# zW3R!`J={c-VHMrvHFu`p(L{scc@7!aMd8Feyr}eU*nB{Al^*{DdcYU!Lh&yLV1*Z} zTU1Y$4UAXTuWvY^VF(UPH6BS1>36n4pu;p&*=<#~Vw^W$31>KDrNIPvlNaNe^8ppA zjOql};||dprD=|KgVm*^+DiGew0Y}u+JH%jxs5n$%9Z$wJ z#Y?-zff}|R2)5ylp)AJqW^)HtQ29tQU!?h+#PBq|nf^G|hcXi3{0FT5JXmKC$BFk78juSv#0l z4g;K7j<Q|NiL#~{Z94?yffI1iziYHh>+Du2tV#fqxWgA;2d*IQfhg*iUT$mQ`IvO6l zr)6?Fm=@+iPI?|F1TaF5muFytP?J^j%zyR9?MQD!aa594${Q~$OCe$%hQ5+3A`^1V zmmLE81*42k;yfFgurzP=2GwK9awPPvZ#cZe^m1?~<*Sa$$<$mS6bWu`kSw3YXX4ihs@L@Q^;lVr1G{=}X&KvXv zIx%d{^O;w}H@@5p*SKZ!z}OHd3=M>Np`X4D{hG*x>%DrS5`!sXUhhp_fd?mEe@u)f zY%q?Qn6BUa#b+F_cL-g>Kn}lQ0CH9Z(LaHTxmI6C;FOq~FNzC7@zT1)`%A zip*ETz=%;}wu!OHC?e2-eMBT{+)FTbp)$ejxM0|P6G?t1*|AR$OVhPXUX615 zVOjtl@~^RGo$56^)4ks&RGTn7miOCeudqm+`8!na>T&)VXGAC1DE{2v@R#I$XawEi z>egiKr+7@v{YaaDnDul`Z~pyZus>)*dOwMyJ|z7JNy(q%pTsy+FT<;123FEwJAoD? z$3RPjSKh}J?~Jg7JgszwR|dnY;^d2vswzy#Ll)7Sd;12ZJ@23IxO_ClACmwD7C_p+tcP_H)9Np?Z87C^(5w zS##fDzx_AlUpE`IawIwWptL{@CNa?oB`}8uLD>mPC_yHbfXaZr!+u01Ha*Td&`%Ro zhw9BWh*Fb(<-nD0Tqu7jTK6On<^iWprGW4XWbIThs43a{8ddC7L~qc3c)#iiF))&4 ztZUGOffSggN2H?yZKJ=EY&+B|W~cJ6Z7^QG{n{2o^~a;yzM+`RhffRl>b-F2H0M=|Qgy(1|_cbK zmI=3R)v{>CRverTMEF^s;hEe#5#>e_#gO;TaQ^O9U~X4oV(5@vM~W)&(r259ZSelO zWgza#Vff=YVB~LE`PZWSH&;Gyz%E=E~q6HY# zee09*eCR=}3f=(oiPf_Aw~K|c+DD-se8U{YnLAfM8A=Qto_)~l?EYh)Jq}$OHDBN& z7q%ks^#QfqA$B#e5;L%ax(F8upW)$Gp)zn@mk2HY?|R}Rq($K5HhJ(sL?`Z13tP?= z+wk$Ev@mwUo1Ov=t`y>+L`ItVLH9{YKKdrC3be!ZJP;d%=_vp86(5|GaKk)YAd;*7) zDY3SjIP$`#t zCH;%sXT+Ly@){Jw&vN-Uf@>Ve!TajKbU!=OS_V!(Z$8v;rZvMiVMfNv?;6%gXa(Lw zNkd=4ew&>*^yB!al4!*nPq3KocUn893Xx;IT=W5s`exdB=u;tnvoK@zppHTzdh4PL zDC7>jtbrp@F8!T_6P|pgb2bH@1th?{MDNtlwqY13@KL z^thZCz=WA)fAU*o#K%Q^t6!4qdHaBH*7IJ$vy=}7h85V}_ko=EBY2$hb8+Sk+y4fG zIC@TEqnhh9nf4#4ABkZnKEV+?sR5n}PEOqlTtRLAz zlZ|r;Pzfj`_XAJ}w!C3SB=M#!XkgSFXwODM#G3Alnp6CYK5ygPQk1}J{!r!7ct*^P z=^q1A3=+Ptuo#O=<7bO1lBgvY4qtbX=Sw3o+ix#LaoicX_aQKCMl$>2jrd{dzr{{! zNr&9v-Mnu<2R{lJb-#}Dl0fTlt0C%-(CduVOL_Cxd_tPBd_;h=^A3SH*O*cZ^TI5=z3 zbbAx6Jx7lo{j77u<~92o@r&7JXA$X91ys4+_J6jyu@TjUJB>sr4eDd)k#-G1@Sb!W zXDMI2SR7}$0c95byE0K0ju597*T1Yq^F&$S6a2mPZ}{V@x%{y?mvITcY;05NG!~H9w+E`@nBVro>VY;6I6_ zOmO=3V)^~1KCvj37!7GFv2gg`1`01$@5~3z3j|`*rv@u9az3b~ zU><~t*%ZK&7`HG@_D_I$nm!3$&cUm5nfB}lK#~Iz*a}5eT4N;c2^M3y)Sgdrd73(d zHyZU!+XE)Wgzz+{SbH*vXmX_`6F9@1?6=Prcy5vjGs^SeI3a16p@6;j*T`bN_HR}7 z?!U0UuMBrJJ`=+({}a-osyKOyQSS_`AG6X#ELaT7Mp=lXWifep3@*C;&--BI5>wG- zcLe7`T9~!Qu@$)I@y;BEde^QIFdUevWl?KM4R#9Vz{{tH_m6Zq|I>UEre)}f7}iw{ zYc`g$dL5I9FMgy4s~;?0Z+Zj7@go<+2E>mPG;BhOBTE@jo96G~i6k$T5 z9a9Zfi#vOT_e|dHhY_iw@)nEbJtYbs68k3QZoriiM+2bwH-Spd7h!7=Cg_j!W(&C; zZf}Mr^>RC0xKZHt^B$}|lH1sI@y{>Nn=S@b{PR(=K=@EKiTmP2&K#v;Z?Ji)!%>AqP=&Sq`~qx*n5)&|AgItqnk6 zg`Ko`f|GPL!VJKZ1Q2`+QhP=0mHLHj45m>f)yAQvWjdm4zq!iv2-`G1xA~Kz=n5* zhn2<)GCiV3`)Uk*b4&c~^w1i_EM83CdFTj=1Ds)E5hF-U`yuhK9sMPfofHhF2k*sc zwB3Imh`N*fVx@>5QCn6V40Zoh^tZ4K>{+fh8lJS|Z?Qsyb)j9AJfnGCz(DYYm;bPo zcD5)Uj){;B{2;hP$swZHFoaM_Z|{QFCembvAjiI5~uJpT>TVW>5b)nug)k^a1c4TGrx*g*@WAFq`|p$(Mn2 z^i~J^F1?kWRQv*r^p~g(yYbB|Y>_~Q`j0mYVTIa{riDERg$Nl~rh^qB6l_pGhz+zQ z$j0_Ka>prxBj19LxFv*d?>MN!~^U-KCOlcc7lB=5IjjY19$q0Qwgux%4x+l0f~M^ z|KY68b=YN%;X$s>rQ6kt9h}FT;>R1xNveVQ+MzSuiDM)s0pWw#<3>{tW7Dl^Z(VrrkP z<$+TUp6yb!nq7fHNC56U$qMAcl)ZNY7*0&`Jq{jc-UAfz zI3C&;Kl1#JO@ADN=M(zlhp`ru8Sn9!U#^;o4;SFD>999{E1G!%T5C`I8QA|a4FO{e zyT2{yhpQ>+)^`w!i_;s09$?t3LbshtR0_QYr~ zD3uz=b~Wgx*l+B9vUka?d3VFa5Vnza9Y*-0-}ZaZ<1HGzva{AmOdN>sTyHo9_Us!i zgIb1ug9|)Hd_Oj#cC3E?6V!@v95RoDb{&Dcwdz{r!yaPjNlXiv7m5-Y17UBl5r0D1 z+!Hom%j(4TqCUNo_HkfVJ2p3_w1-+elUs%kjAAh}8c%v6`I}ciAAjEyyFXg784FA} zRCEF|EgjNw0%~hSx!OjSp4Dk?1`2C*-Mc>Ne_qO(6PSthxzL)oFaY?q;M?SP^rqu$ z%Ud=|*~zecS7TPRWJS+kEH*tp_M);@T%Ic2mprP7=ekZ*qo zY<4I63uh5=D-iL8I6P#S|KxGuOswl%4;kmaffv741c!=q?O4ac@p8{J^W$l7Rq#$z z6rW0-W_~&?`wLi|(=_{JXa=8rc7}TL+iB`4(>{EcOwF@DR!=qdJL;(f&Ty^$3ZSC= zw;TRIb;7{uvpUHqk<@QJ z@}a=KPd=2`YsJGgn9o0ooq!2evu`2>^Uo@A-)k?$9Twd9is??yMT+#_j$ze>|AoI# zL8dW@;O|pl9^nSlVfmUN-f@*nL+5fdCx-p_mtrEBydF2X`1ldpv8fjJO9h$R?BhV{ z4o(&9E+jP-1u(lVqlS3l$wtSV6%W8P+WEk_9I+hJv)TM8jFc7l!%dbL_8m zCE~$FHO+4{Ob_F1E!RDo=VLNZ4q}4gQg4hUm#Sq%I4d|yZ>mM;(Hjmv&2X^kN}v#N zl+o!x9EhC|6#>dGmjD(Kp!lT7e%t%Gj03;Jf>s1)4T%k-H(B5c0^d4<^?vLlae=mB zwZqpnjFK!Bsk&poxxrbn1tRdv0|^7aD-ihM@Dd=zeZN2_xZ}QGMe(H*8TU0H?)w{@ zCoJ|C!0n$Ug1%`8`Wg`QJrirDKFR7=G()PhdhjDHHV9;(2mTS|%|Jzb&5_}4F^c1| zdH@p|ima6LmGB(6SyIL#_}QY-Q9wd247t|4&&lpg@kyyy@lmPWA$~B3`!tpErOGIF z;qQWmhY%Z8@}oq)&$e@B{i-;W*FFtnV9fVeiY>A7-&XRdR z%%1t34KGFGD-F0^xx$10OfadwQZq)>1$E;evG9q-GANpCjNJ~2iZ+VG-Y?q>5qZEj z(SEQnTov{YVFOnB?d3n>c<|!DZU;XY^JjqZ!)dGh^pR$zA8kDUdwLT;RfdSOHar1p zuQyNolD!uWVo@0 zM14W{EaR?S;79mtu=6GMuYU-qd?6w}^hjp&N2^X^5QFy>?F*=MYYh*EqL6*^9yr`0 zpHk;re~M7TX_%v1Y+%#r&=3|wm+LG%^W8QS1Ap{{zXsrsj=vFJ?k7?G>=*jpbh~W8 zFNwt4=IU*?=OOZ>jXJTI(nMo`vq|Cfp17Th?f;v}%=R?w$iXfqZx@_l1YcQ}JYBS| zk@UxmZyrapjuGEP5Z@p~c@0fM^OAE!(~c3}EI)Sk*~pH4vyZ_Dv#*AQz$cRsn=};H z71R3j?@ci@!4L!)cw}7i9#anCGy+rFnZ}@GHiDAbeL=|^7!G|wNyO;h$$rBCFSc(0 zGw9Zm?w^TVpp!vK1VKpzK}n>HV$hpcp}yn@G^y9s7Z27{MLYM{HuskD^T?q|boIKI|_a zqOaFl-W%hGPg(tIQB(G2{6(YQ{WQE#S27K+N?d9!3tOv;G4F=q#eb`}zcH|4r}MO1 zK2ETqpJQj@kHKUVn>+B~wr}W=e%|9Bw?mbCU)lWxV?u0AU@vK_{U-t8Yn2`LPLN^l#sT;-i}CL5 zgxlm)4nwex1>iGJaqQ${*HG~5yk9UCr?yw&Pp61iMbQq>MbkOyB_{ppL70273Z2DI z+FP}mT+hFiE707Ye3_;Ig-QLF`w!y%V=Oge!jJdxBMNZRVXQET(FVeX_^pBr=+bcg zwr7A5p9|ug?a%!P`vQm^{N1DF`XnkwEtyji!IAoH52N2O1K`j1bdyGW#_QJeQGoqV z7%9A~AGU7r9t`Pc|COg7vX7XZ2QjMoxroj9Bt-ktNA(TqC@|DA1>e6Y?7@pQ zQwmTuY8*Aw+so_ioW<}4irJ1gaScXyJv#OAz+fETm}1~`Mc;P_qGwDovUk$zyS_3v z#r>7JDg4SDRH*L@b5lm58+*UK(f6KuU#Z3!la0oCBY%!XcB5mm@D;w2r2yh2GjDx` zLD3snZIp*aW~DoRjg{sm#tbRY44fb=}(0xfC8=~}~@6%v79z$0U z{viIy4?Zp_-V~DGOOJnO9GaYt<&4ilcpu2bUtEA=2tE0JQDTwLjzWXYEj%q?zB!>J zbE!A?4IH3L;0yFvN#*j>o&@w7>q|q?ftd9Y!veoT)XXhSexYwgFpcX@Ax|5Ca4;Q< zi0RSvW&V)oFqAuqn($@0QQziy0ls*$G&6CL$KGb5NGK#$cNPW%bolVDAa<|${Vg2N zHV&4#Mru}z0xCs>7U4qVehku_K z=rGs93OoRaWj!TmQs3cU>d@}xLBw?swq36>Sja%zQD2S@cub_RoR>t!D1EQ!58liB z72Wr?|Ec{7hw(tXuL>X2KTCZ%_c>XOpsE%2X|IvEN3wJP!lAcYY+wVkOs>Hdu((s$ z;R$*2*+|G&Uog3I@dzJ@a9kBc1aPU~G0>s+qnw32%zO`Mu(KvV7J%+WSMp;4mE@3r ztqVSY-gF_nW}NKgVP0ZNP-Yejr?aph_J$^`M6f2f5{{Di+)~(D9)GUiy}lYAU| z;gfKnaDxb_FSCr-5fT(LHpoMs7A^vY~hc> z;cEok4q#M}6E?TuwHfPjFRWPh>tVj6G6`u)e@wpVV%qv`x1&Wc}~P4-nax0Q}-{yG}-;g6UyFWU%^1>qt$ofj7OHo>2m-j zwG&q0qpP={0C$O5^XwnAz(e^H+!WP)pNQ>;(Wg0wr3#};s??6RKKWZF?i@g)HJd0H#HhP8+pr&G@81G+BqkB}8 z`0nI8@K-O@-^me_`s3rUQ#@(M&mBi8%{=hGn0p`isH!vJe2t*!3X-O{eR^|!AtQni`@ zl7OuS@DFOM5v|S`w9#6KNSXKhoOAEYB!I2k?fd!7hj8va_xyRz^E~G{&w0*s&S9U_ zWpJz30y!1Ab_6!m#9|5;wP{w%4%%e2HAwLXr-Q-vUD6q|VT@Cpv?>kVRtlIv0o~ln z0y~jWp{MOiq?MWlD^1d9^H69wHu3tW{EQzH_fU*%8`b<$#`*!XRuq3uaPYz*vSq-R zV?4QPgeP$~0yk4oU|-8_N%s_cix?mHCNoZKPq$m=pdO8!aJDoBF zYZLSwc!#lGwqvUC_smTYPy{O{5 z9-s7(&ApLRWH(!$?j=^Q?%bEW!#ECIzbtSAt6#%fuVKCA2?gtGl17fc(e&_rMYvhR z_^$|6fzdw%#@-fshs*GHk|e|Mrwei&1hq;}jbE}G{R|NPX^qp|Aq*dXSq2Ic{XivZ zAyIONj(So@&6B7kDA4dB>-}jP&(~lBRd@2oDnX@^K|#uUTxFI`m?zt$WO#oyb&enfNYU~D1k>3bBcbu+g@wsGTzG^Mg~{6($29)r zl@ia?u>YHsb2n)>DxA=tSn~8dbq@bI^iAYK8P<)Gr!&6lb+zed2D`TbXlm2k$mcc{ z>T-?t*U4nYZjk|_wuB^|k_0gyDLA;b&}hftqJ$fhz;FX(XC;hOEs;LNlF!w=;q1Oc^|SqRjvKs)p?N2 z64ZFR5V3#;clxE&B73tjcFA0v&B&r<1ku%$qRNA^N>}oLmTSq?wVuErS37ZU>yEwyPN-eBs8C^Rjxpx-Fh|CP&10EeTZvR)?gEmCr$Y$$tx z{9=lF98$BZ8&q-*k+j75X2rOO@6nSjyDL;b$kquNc^6=vVXLm{uGY7Y8jqISA1>&6 zm-xfP%eH|9(RbOWgIU;PJbIelt1~6MEqM#ri{H7xK27v$5D$Qs?N8hc-wP1eN*LYF zg2TQchR{kz@O?7N5DiZa=A2LMA{xb3hOHU#31=oQqGg=(tx&E6b+GIebqI6d1!Kcp z00aG?*@%msR@Bc@u#cofX;b(&;&O)2UqXXm;DwHnYkHC^RLAfvSo^HBt2~(xA4+)+ z`r>Aay^i<@Tf2Y|htUFZrPbwhxOB z{1A9i&J)ul3)?-)?iKQGw@*s8P?#f6^uc_h15K8efHR>mn@M+CF~llFWRYvlm-?sZ z^g^h}WVNCn_!zkF@Eec%uCTYdlJuydk^e3+SURNni)o`R-TySqYwgZE(9?iz1SXb7!22( zPQJ&QXF(aec@_tN)E`W=BmH8tXJM|R%kE$o*_?Ro=Z=bB&l#)v(k!=G;GsHaLD|59 zQ-mNn+<6h3J%60oFEtG6(u?%@rRn-b9#R>tBS%eA zz0_BK1vAgAI;|N;`#T(b#C@M%P}Z}+9+hks0hP9Y(kIG#EN>@wPB^J!k}Ol7ScUo=e{in2ZY&v8PT^un zG&Wy-CZxiafe0H_fzgOl{tT#HFo35Fy&$wQDK>KAgsLISr-Mp8F zo|t%Vm5df2oY>=)L?`gd*6!HuDUJ82g7B#4^3CmufBf^szJ1)1tsZfhe>u0?D+Zy& zvy6)3zpTPy7Z%{#6Lc*rnF0qFj_j|9rv6M~S!;k z?bb*Q+0Jk;p*vh)8xG1+0UZ!SoNVz`XJ?kGHTh7tuY|PBN&bPkp4!c;6q=Y%I{0%S z_`+_hBrHiP_F%CRQ{XIlV^E+ZHo#R<(T&>vR>}Bri^(yE=m{pL_{wCR)wT9-!`jLf z`Ai%PDvdccKPk1zoow94fE_c2LvK4X%0E)7xph7v|Z)KD=#%*5T9W%{P8 zSo=}ix0FLFqo!=2`%4{CnF}SRLKZ1fmOfFyXRxCW<3AA$kmM7qRJfF&3-=AG`rKg6L(_VNLH0h z+WQFz4CCh;_Z3jwDA~>b6%lNkI|KWS_S1M2)4!bS@ZOrAx$_6h+gK8==7QBIxb_Ni z12>igjl1tT3Nyzq`W%)%IGAbDADV~|rQNbG))kjfI=^$IZ{XbW2SLZf>C&%B&*^*^C z_EcF?7LhhdCU@l`>q;Z@WU5iBCkvdpeVjX8JT1t6VH~#TgkrasCN4u)%j9B~`6;)$ zyh&QgTwcSWPE=iKv~5LiQy$YYc^APrsp7rYHbusnod0 z3sdJtcC!pSuB|hI-O9BK=&c|c3V{Gf3I&QJtevHw%8^T@j0le)B&01H{+$Q6Ej0`9 z2bOkJjdO~~JEkFwtI`ZG;K^!616~9}o3Xk&6#Kf`eDiwZrKMOXs?MAn>BGZLrUj9g z5>@atam>+LrL4$0YL?aIuVn+@f0sJDQ9Hp^KoN15WzJjcKrHh?fB*HOM{vwM*p6L6 z!<*;CzCBB5=MRHu&Q;gA7VQ|NkwGBwd0_;*YN7FH)dJGhM*9rZvC+K??5YJ>lsE@? zu)jtQXspSZNR>6Qf*PZ`dsAV{dg{#U|Bj|V{u_nd^|Y9l%2v4sLUnFcTH#}5SQNhB zq42$+;AIF2dS6%WxsQsh5A?UZEA^CnB3Fn3G$-hhj6` zZYJxv$GHLNQ4%KiACdWsc^xj|sPD3TD{CQY`Bu&DMZza)8U;vs+RGCwxw3mMPf_Fb zMCD>W#!vtGDp3un~E6qZlNRgPCPdWzbM(q%Q_R&a!7I zXBZ(6NQ>FjgG-e^k%DAa>^^;hHRVkbFGhVe3G=N|*82M|b@-jtOw>~6I7qc!GjSWa zS@}}xoxYV;6#W}$K>ASckl+p;{y^gZJ&yC`CEI-}8_T98tN%4v&qjpbtHri+0hb9P zec)*)N&h;{uR-j6|K=Aw=iJ3Uf(mIal7N0x!+8qe6htgjqb{q>(m+KurHVHuZiRM< z&k28yS1w?8P|ih{MRE1QeWX(@Udb`%d2J9wA9MZ!B_{fWF)5!)pI%3$rlRMsJtot3E!yIS7kenkzY;^b(mbwtGe~Di5aDJ6wW6ev6&q)QepH*Gm{$oJO%YgI( zSrdJb5&~;%Vxmec{U#BJDW`Yt5l_g4N)&@iPvj(ttxS9b z2?RF>RCp*+3p65l4*VUdk2wNe5iik8b*g74UzfhB-<$FPqN%Vn%~8yzgN4Wq==ZHbe~RMFZ0Bfbs$^1-d@OB+@F zh@#-4mw@)MN!0GJF85)8;^a3N@m#Ms=1^+q1wLVG5A+J43;^8li7^(<1k>I36!h`a zCliWdQK_#V=C0E+9NwH8i1+{58;E0<1>-_VatufS(5H!KE zqrqIf&KCMfA}<&&bI-JF*w z6&WbUGV3Fi1U7C;X8bG60+EeMz@G*Z_~KJhw-_zue#NJStrVO<3q93~TV+C9ph z%|-V{B)`pu+e2*dp<9LQyi==+fYrQCvtK0aPH;C=EhOpHGLdvn7if`d$*mi-`(41*~z{8NB5FG zcrU8DuPrNE%T6dDF8ianOy+nE?SZIV?$(x)V; zEjySjgUd5Vnxi`}@32nxR#EiH(Z0)~gO^2C^FoyKwk*V4ITY9#Sx$jItm&PqG+#l> zk8}-L`(F9oRgx5rB#{*!(* zZ3mU(iL7dE`3|b3S6kHO$QDabN0U0;D*Aum?q|o6wyK2rbflzHh`L4C|6$?xO|6%r zm!Fj*JX>4R2?OjC_fW#$IANa?z9pftIj7rd+sBj4@OxxW*?G{BpLfNm07-R6& z`&%_!$kpd9Bv&s7Q@2ZQc42kBkz;nflLI6VT0r+J0bq;F7}#WndBIa{4^*UNJJ~t> z7b#Mq zdRMj;#6O?>5IqevJt#v%#!F6~E&3GA?$DU#=xKJn~$_%lO?bbn7y^S z=G`YDs`I{}7tcMWfMNniqF3XpP-x-|*~|AeTz`AJ61iPgJ_`j(3P84 z?CSY6Q%}^?r_sIbuX1WYWJ$%r{)@BhwPSaS!-ef`fW{)0~nlY3FN(sZoG1xj4*4_3=s z)&!&(3mT9JwflW%(taJ>$jS)&tTU7JAJc3dSDbcBxLctD<`rDYRoZSOY$I~rXpvXAf@uCahA7PxX<1o%CigA9C z%{afvW}F@vM^HF=m)UWuxx?dA0}X2yX5bHN9)vLzgCzSh_#!T;H=ncty^E{D46eq{ zbMb&Lk(40?cNzvS}&pfV41y@3Xi@Q(htBmV+=6Wi)p_!@4e^5tUFlg`NU zdxuq_7zoL`G5(E|=A&d0jm_|Sh0EK*>X$*s46Dm0;q;w1x}3hYhOUEHI}XEF&rhC3 z!T1MoA5m*bSEN!xt@E+2rD-#MxlX?SIm~WA@XxUOG{2=X{}Mdh2Shr^S4-Nt$fG zl*pe;!Lr{3B+mK1U+|VFy+H`@elV83<&wQ!(e1^_Fo3A}ha5D}vwQ9Lo!CjGt zyH>=Z1NQ^}C~))7U6@tXYrRPh-2l$hHJogOiH-PYNglsw*GdJ0ctt2)X%ZNI%iKrF z0n*vJ*#4iSm2)1h5jB{DH{JD!YWtaLj=i*>F}prN8$0Z?(0n+%ME&j0?MjW58z?(b zlmoBD^Zy9eJ;psaRt8=(?&a!TyZT0P zy@WcDlS?M1`x&w@;4*b642t6kdu{C5dCMoO_RAd{(au8T7o(`Lc~@7W*hZ7Ot2EcT zo0airyWTC-PkVB$@0WUaP}YiPEW6j-!eEy=#DcjIFY++Y_z;z|`YdnRR)kGw*zU@$ zy^8^_(_~dyl_~GW=BN4EBagYYQv6mg7VjO@;pLW$&0D+P&GS~}d1vR}ur1h)E&$=W zrzsPvbG>S2e9Nr=;n@^efdaCX5eN!^D5LG4c?Ltq=I5d>a%8Oi$F9Vv=EMESrS#4SdVbiy5)$a@U1FIJ%1&W8_D;S5kcc{Q|58%mqjK zZEO@*0~~rW`u11j`Cn(V4cWFjK5)UkeZjFZ>onRo3ySf!IR^VA#!t>o_#fsPqR0(6M zy$#0Z?Qo#o4cFyxKuG^Q7hd58m*4PWbJ^`Mf<@R3ZT3^xE%;c5aJ?`dC$9?QZ3T+u z=bACLQ&eVH@72h69oD-CNQZ!fT|!pR58y3m#+I=~s7_`M{|y=n!XA1!9a&z z4vu|Rbhh?qWJZl(bATUkfgcEuJscd1z&lYNIWExf7+zzYR#{JV+grk9DBlAnM{KjV zb3m?taA@5=2CWMFP^?|fBE8`zD|f<1^F~YGJSF!{Tk)G2i0|;*A0B$8+J1$jTbNXL zy(@Hw!qemVS3`L3P6+xDWbg2O3dIMFw*Q36LU9(<+iunrXVH>41S!-OiR6&mem(k5 zpB3mvAZ=u;4s>TtS_Be^7UARe@%}n@+{@5C-FuNU5Z;k9J3c$%kvQGUC&mh5muJYn zCvuNN_U|1F4eJ4`Nw8#;CM-e@qe_3d>wsEIv9vX`*MC^3aq%hc5G)Ga@fQKO$ zfQOMCpPA?ko}Fcb8JZ6Y&6A@bvLlaKGyWJoGCiJRUej_$iJ5j0GvD(ipVdi_FUS23 zGwFiG;auZinz8%c2j88MwN6*K z6S#)~=ipfmdCy}IUV#*R)0Ki}9A65u$>hrn3E5IG-K2p`lcYUa0`W7qB|g<-4|0PO zlS9KAgqQQ<7os*|gA07K*tkm$3{z54xP!gtF<(iyN0dC#va`%ln(r|=?tzaoJmlC3 ztoXIe0&j5S=591ak(+9~RC3cN1E!Ljg^GdTp6H2q2k(~u1H-^OA~$1Myo2&B9Mkm< zViS&uq5=yenSoCzc2 z!$;BQs%4YXuW78zi=9!FL(H5e4G!ZUrq0fQ`q%SM5&V;U9JXQHN50Dbp5w{a*s7>+ z{HpKz;5e1TCLR70Y5D7ZPcBkLwb2ya802WhFuD515%@8F(O14niufg8CrC`I!ASDN z;SD}&-)N%2N;Qyoal`Wnhyg;M z7x;rQZLbwLY8unKIqVcKG-e~H5`kW$?U%?}9NP|%k9kf-s%>YuzL8?u?}yt1T*VeH z>5J}7+51fUx8yS2Nz7KWYk&46<|@DJHak>}aXvQYJK^{YI>1jsBg^Rtixt~M^mmBP zx4iw!k2^nk>lpco(OxmY#c_kZi2^xMce>!zF`9LNFp){_ni~k)h*V`+I*= zay@UX`%i|WVI;#lTRfy3k}Y@pWgEh03jJ1LfO)6UHj%ds=YkKI7i5_Bs|piu{uxZ{ zjZJm)`M1V?#{QETyA6)5 zj*W(C6|2IzHTcHv?F-2aoGnD09ee2I@K|Q>Z|U`yp?b`*?NEb$7TbPCp7nO8ITq(p zn3hq}3hXuRT8nUSyeM0FWbnwIM;r@BS{~qjkll?wD?IGi-@E)AGuG^@j-4E~cWR5q z2MVRfb!hU(DpG|f9ne+Z4oolFYaOAnjOZ$k3O!Lt5^cBHP$qA(< z^X`)wd9tM@LM2;h1|N4#0*>VTGx9P$K48pplxKw(?&iVOdL5&gw7zdzEd1}jD!+T=n0c)=$DNzq64{$y1uylhKuw)r8S+YhF05{{7i;;yBZXZ9rtKdk&VYj&Y3J zGyxe#?vl+evb#sXUTn zXTKypF?k#MVw%)RrE>^R;WQ-6NGva8G?hHFgKN_Y}yeN0vKgeY1 zrDUOGL9bMSpTqmsYdKZE?8GE8gzpO?R8+?UHdWxy)y(?e7>FA^a-YW*tVfonx$Rl4gx z@Khc6y^<#XeciR}SlxByvGeV}HT{frn?IBOT84Pb=&xt+rg&Is@x46%9u5|qRNLD= zqY8T)Sj`BOv6c0ChcS%KSz{;K8$ zL-bc9yj~E|SlvZ)hGX?O+@WObU!#hjSbs%D7*P7F_*zv5K2}QX0hCxd(q6_ll-SQX zO6=j>4h$g1y8F^fta&p#8`A4}T5cvU9Hz&Lp?#<*b;rTas>hxI6aGKbWB<|BV`JgR zeaYvQ8rufF1TCl))|79v5IrA=v{3yIHC~Dw;W&L~Py}060A==aQD)(7v5mfSJY6>M zTt=0Zqb7!;tADwxw>k5+zp)-0c+OZSe$2*%Z7hU>K|sTd@N>c4NE znBP8w9=qoMy&kL7R920Xw+_){W$wV8Px4hTnWe_|@6HT3SR`Ea^(&&jifXEqQFJ}d zG3~MJlPo)sW!oRF;P$t<`lESW;6<&R%9XSnHy|1*XKtcOc7#hXetp7F7c=6s^>(hn zv&OU+nfstkZfE7E(zs^_L#Jc(+=v*Na!WCrNL9|Cit16Zr;=rf_8-IOfoIeQ$Lz@s zT+H^~)>fl!CMN+5@!hBnycU~)!RTrxeE$Z2vZPea*68%ISM+VHMt$DUQ|{EEf6;O5 zN1ewQ2h8X;ZU5Y=>>ci?n+Si}0uoE--D36hJ`GsQCzyK5=QwR}+Shj+Ei$H|bYN5P zFrUIgX2z~&{w`}7GJjWA&*6R>oRZeQMpXA9ZW~Sm{uXW*x;Wapf%-eML{$AysTqd_V1vfwpPT)^0+2DKA z1Oa6v$IA*|t1MRz$Hyk)v=MsJHBE$mnWiec68{4J{ymbtK(T9w%!Y@PPOW|aZ&~dfLOTb@zXk1B z1uwJ*$_5qT3=8iS)+p@rvwFPSzg6!O8KcQ~6HkF>=z?OukZvkQ+~-qj5#mH^5aLMn zLhM4vk1CIL$}aI#hLoYS-_7wzD4tm?^G5sVuy3&a?hOmBZq)PW(i+!;*! zAOUK|Q!JR~RFwN(bmibL(kn`12gQ*JKLX`dwJV6fl~=!WC9-#`NF3wmFNgBXXJ+t27jdFSWUdyXU0x5Re#8shW4ZV4);F;ft-AJ3<8zw(%+&pXzaOt zLCLU$T{xsKR4QojD?3lAc9w>Ycf4n{T?|dm8{$HBoYQK>Ij#2lzU2Fity6G8+|0#! ze}W|#%#^poKkz)|;_SgJYYordf!o6nacBnqh_{#1eI5ieCdZMd-SAlYU|7frJ>l|A z=&7_AO#8a^U+4B`A&RpYtnHgJsl>HL`9%!vHGSc+`-f3|Fv|%Hu#kg8yboW^$U$Iq zAk+OdHp+hO&w3tW{OoopuiOq@gTUW{>ZVuru9Xc88N0Ahp;O5KW`Ax71F!?mp>Hg5 zmv4Lo>h?fgvBxWa!z*}k^au662K_TF0n}=>+&x|YLNVI_&mtYxf3G^gXs%ucGs#A_e^=mY$r;pL7UWC(>1pgsf0>6&FfM>9s zgf_}l?l=bT6z9TWj=|3;$KYAqnNoyn_biU|VEfDRi`kzZ`y8%=_yuppO8OW11wXUm z89VVf{bYz&@ZLAgv%XZe)!4jEyngL{S-!yCUHygHEBG158`$*vO0++)$v$z%BUm2u zOfRlP>}f>9*h3SoDd(9ql8(mC#_H^5+RsY2%JzQ7fyeQF4%vmejkEHWPj%!y{?E}J zf2sR3E|HWb{?Ef3%639&=@^#qhonh}Y zHlqr6iT`+RpFF!q(QE^5yW!+yGQ zdDL~wQz#}(wZd3yXJ60Jm{@UIfQ2T)Xf7pKo z(>L7YVpv?d6_y1zTr|vpfb!B^sj+ep1vDsp&k)>gs<{5AvNmY6@0lI8w?#s%I3M|_*D^L=nWy)vKIH@a)l=ft zY!bIBbJ;1bpzI$3f7>6hSNG&1Hik@k*7#m0Cs5|&ZO93eEpOouuF?hNz|ovUMVXxE z(iK8Otmjl0U#0zf)-McJ?C%@mzbYq`BrYD0)sC%7 zJK9sL&l1E|dSXQz=vDg*@akK@S(tJ(jBlnKrx|!|4GZ%FpjD5T?B9B77U)RKD;3HL zVWDj0BquC)!cqwxCJ-FE{l@j5WrzIQ11m)y#wxiJ-XG+qYgsR{*O$nqxb}mg*sQ|p z*g}-h;F#)ICB9$!L{R$?wUszb!VXE_R+1w^YyiWk8n$JQfkFrUgNQ6ex#Lbnjq{g2G#6MBwt$Z7xI!FG!IBf@M242EC^S zIeHaN$>X|-7EW`};A_?$qxqi=T^A}qUI)ZP8IRn}tuVq4+3spNBdW8z+NT47-eYVS zEfv^FwQH__D$LiWK7@fnAPmJkttI96$1s2O0gmv1ZL;IKD_{0=a+ib@Z*15`JUQfZ zw^^hrFbj2%F98RB)TlD1n0ae{ET;^r=EvxaMdi``_eXbCb`+I!MR=x;{(quCA(*|( z+f%kL`Q8wFrd!x*xX!_4EH@3QRh9tC!f{Cz z8@1a?I{9d9Sp5anSb2ft1p%Bb8e-#LcUy^{H~FzPO{bPqnlSkliT`7s>etjDND`nW zsoc3DU~wt1q7I=VkJ&v+qYd-pYk5Y;Vw5Gfw5R*% zD^e^Ya#2nEmV6o_;0ZBxZTG3(>m8)Sl=8uQm`*0WKzftH^*tpMcohPPl#o#%$N0JK z>+q6%!*e1WFNy}^*W+ZohzJUo_!>?|03Fg**;yF2UyI_9Lc5EFMMLq%yNYoqNskvM z$}^|?u76${&mFmDZvLG%=N*0_x__|a3ka)x)z-?w)wf6Yzmtv;T3zmoUH%bYr(Awy z_3f=^->=`)7tS!<0b_Qv^Sm=(hOJ=WeWOhwSa#*i@^K17JCZIhuxs@ly4s>U?a%9K zqr1H6ddw-6g=^MPb97gpiYy#f^BT?-V{^;>(VohVQL)R9=<1WJ6vtU^o3abf>G(pj zgwI;O4{vjHmwfl7zl+6?4G%~qP9p$;HSsagVErSSI&t4}cWeyBtxG?O<*B9_>kooW z2eI`I8AUEa#q$NhrhT%p`82^W28Sxap*f}Mh_T^*ve60Cu{6fCtMVYMa8GWV;uS}x z-qAp`urLOlj^%s~#fvV*xdnPmgE6NP(|XoBDq_6LmsRXfh-wz(6a=W*-`v>Fuq z%FWy&nthsXwov%Aq?c?PVYz7EJKSf`!FuE6zBH)QeaS5Vfj*t-Y@HOE?rf@ZQvCy| z(8FZRb5L9{tgamD`d->DLXSEwDTMH+B^SHtk}Vx~=#XQ)y#1h%a=q7Jm z?@pZ@3Fds26JS;%eI&{FC)BV-2~Y4wZ=CXk&a?iXeM($N0Wus5cL@STW|;AaClzK? zlaImr7lC6IPwIxTQ}Ic%N|puDMLfjWUE|m5k(CZ!|1tvBq7X?pR}ns$JvLd1&MQHal;mfX_{jSb%=5i&Frt=P^)*EF^8F%dqJ$ZYlB??mb( z+*>Y(qje#~<%;*M`8DhcP)0B_Hd$7EfFaItY+M>~dmiWWZ;*5K6g)-1bE*Rm{XJ8+ zIR%i^p+{P*>n|eLO?xOvDiK9^w0t)^7kf3J`}?tM+V zXF1vBzy>5%g!`*7uSsqDr1oecU!MDT2Bk`*Xu+g5%2c{(6UTgqGs5))WF63-mQlwQ z@uKD9btyCAVL(z+ZGFigNo1xLl-6j)E^g%44skb2jb{#N;Wz<; zQ9%y*<8JO_C7rMUjTeA|xeuw@jwj$yBCX4i;D=m+CTgO9mL=H2-oapQQ5QIWkkX6| zVxXl)0at1k)|1Of;4_tx0z_nqY}0LRD`}N???6qwcG5>dFSGyNBnN$__oth&ro6x* z?!TfW)puRFRUUmCxmA9w+29daLO*o#fC#eoOMWppn0Jn_D)_1&P*NX^=Ez48S z>v@(QHi-~DE@0*Qq8#|a^Z6jCvg3SML202WqvIr%R_iNa?{l$HqKr;Q89kd1(YN1` z)Dkyd>!WtHFIKH45YRaV6S8|g3lplXzT_Vjv0M2jAK@)2ZMv&&IB7YZ%lP~W7XFyxTSQDB8H9&u@Q=}?+QpH&GhRpOBP9B6Lk3$RO%2*w_A1{jS$ z#u*WnZqtJHL#m0QLrQXsxruz|USpr6(sOEYAX{0nN02A@2-W84xFU>M-*-!O^q_?F#!_TsDf)bM!!@8dzEndn4yGZK>cPhx2jDcJ%+3($884*Jh7UH|^GpZ$y`Swt{GFiDfP z*g2~%&A_TpN<2T=AZ&M{Q>s;w;p4yCXIVX%2fHCk+F5bF*PsvhU~}KXC5|W(gZs(J@h5*4=~dn?tFIyM{hoc zk&z{#zT9M3xJ(m@dKvi}DJpr8QXCM2^_mB1J;o6kW@TH!N~7&7e^6<*QNyVE_Cl$mqHrB$|WV+)QxgCy2?krAjq*z1rFVit6G)P9k&keVGj%%keHpt-v zsMcv4igasiH5qGxBuJZXj17PEF==(pH{%`IeEBgZoN$19E-#SkT@#KGUx41H%{az} z-%*T$tQN>lV9GJ>=D@R**87K;bJD=sk{}DXpJoH6TB0?7Zp9w|h~^4L8Z6$X?S@G3!f0s=9?mC`M1c6RXh3cPkX(ZYoHZ_nXY&9b`rjwi@r44$Y33|J& zfMx^MHH;0X!OUUYkv}5&gR0UtM;+2Lxsd%bwzr!2jd@rl2g?TR9T2yX#RXAE=W+y* zOz6H(M*5!_n~}#-`Fgo#7%ZX_U@j@=X>{}r^dfd4XU*rtVKDjzdXb7o&TS`xfff9; zA2e-NW;-Ma);m4Dt7Lk&O(I&yGQCrg4xLHtO!2gxE+u;Mo!Od+YSpT9T$M`p_sQ6}2|U7o1u4eo zJ1u*ZYZC0A;3^$=%k2KdNtMY8?icEFm_o-57^9hgoZ@ke@@QW7z@j1xPE%11uEah) zT`6AEVT>3_TF<(l7!hYOm&j7lrgM0qqsAcQ)bNcCAc>M+RxTumL!3|NM7in!=6BHo z_~+Ro|0P53fkMQ&bP^eLR5|FPl2Ogks6)zhXI=Xw(@ze`xs19;K{zWfMW3*7OA^DW zwjjzeAD@y$40=L6Wi!mlYSlDf^0dP;TxDC1UF2Mzm-J$d6GqD&BFe-t{9UrCFInPO z$nb^aw~i^$kyelJLHNfntRqGpC62~KL-njOi9W6uO|M{P_))Ap7L@+Bem>X`iT|Rp z{z5?vvqy(rCZ}>E2-pjNLJ(m>+J`g;yk~SAm2AqOtnA4T^NImL(x^zciB);v)g7_4 z>@wn=AYJTt!cB5EN5^s)gPCnk#;9CPjOp4p(}kf`X%-Pq6*3}&D+OrmXW1OX7%yy* zyqPiH83In@Yz4O*Y`NkpuXG4y&d-facYC*;;X{$lidWK1r};7&|0CxvYh!hi)6BuS zo}9uufs~@;X%5bm3Fx%sc34A=s;i&R>SLFt)X;izIxTYTwB)^{9oDrz08g2&CBz=m zt^OCEb+;1Y3B+1JXEz1YKsJJsyFL=k$ReB~S-SPu*slP9EG=e(%&F6SEtSL5m(gRg zsNbpp%uruaQIeOEh9X%or2(W3=?*Ve-XYLUP|&HB7@?bPadI@M9;Qw?;5>YAXltDM z*Ft2F>zLQgjx77SXp2ARb2itlE=qHpD!`YFAEyB4Qxl(L`9EvCxK4Th+xjNiwFjgZ zRm((|^Ek!sEX(eqS5CruJ;Cx0v~vJ0eN{FH6kD+emFIJ;NJ1ThcJFu?_bI8}2r+~1Q2-b@R6G-0yInfiP)8GCfw`2}?OP|X0C>1MK7WMVZDedX6KG}6c`-dm`PR~*`Xrgr z7UmO)XMy6{aQu;yR$^hGS}D)ZTnjghK$y=rk1=L$wc-oC{iiJ0Xa)L>yA!1Ja+I#R zdS$&TPpyj6r^!1@bscqibek$v-yhwk%m?}kX?+=^zD1hjC@830YDTf?plInGLDV6c zbug&iN04}M6l@gOE*CYaJx+9~%EVL- zbmmgJr0UGlm6KsiRZ>VF)p#QRCZmZwYR|6zf0WBszKn}iY{a5V{T|Mkde<)mctal= zdvvSV8jMe9C(e^4AGuSfL$W~Ql|kG03Kw>{v-p<%Sg%E-3$UPF zic|E%RWuRBM}SY#riwi;`QT+47R8kaN^^)I=qSl4HCofk)s@%Xi)8mL!QIoIdTBYclM2 zaZYIW>3-@QPfVxh5}K9|gcXD@7Pfb4S21HlE9W$r@yiilZ_%t@QIq_5yjMTTqp-aE zD&A8?i^EN)iU2|2UaFN3>ZMrfpu4)O8{7zIfsgxC4#oPy(vls_i`2QV&t=UF`-L*sbe zy)aU`;MlvZvX8AmugUS>Mh$nM9yX0D4_C#kQ+(LdgeQ&oxrhY;b-KWroXs1Qe1K0p zi5*TJk?GROvpfe~zVq9I`TiXTk?aaz~R?FpKzZLn8lB`$=pn{kt0hBq8pCKO(hbvTOORbnTUi7*WbKg?>}< zYDH&q45-MupKGH`WEIT@WW?vfm0!Z7Z%j& zQA1ACxkQSGyXOjo{CA(~+-^tjr5Q|MCL{awU1nmalD6SIcN%G1d1TWS_!1!^yEjVi zm1f{~&pd4-PvVIwR!>--LI7-Yt2Ekt*g3(IG=9A9-m=gP=uEZ+0%SrKns5qR3zQT3eH+8}-q;1^FQh8f zPtDw1kj3%AopNWiZ)tKau2)}Z4F z53os^UMgGwGxQOlfrO&{K1}yk;E$a5D$M(c&e2O-;WJiHgmov`xu)@y^}QT~ODh?T zA9dodU8V`f=HIqQiL}6}pbt6~^dtLC8;Zt+IwgXDZf3@j<7aFnZV9Kg~(=q9m(16W_Lt0X7W zu4B7_%lo2!$pjfJ?&g1>7g^@e{nW51D%!;W*#SF^sbioDChJMDlZjD#T2+z7doLg zfO5R*LQ2xZIF8I^dxP-{*D~QLrsSfv&io@bGF|KrihWV$fsVD$MgiB}NVM-4jC|lt zS}w*Q5x-`9LjL%NN?M5$yU>Q85(l*e5>nhtKo=2~8NXgpQoK+SyvkkXy@V(++zNJ^ z*Qn`2rD5)Y5P5?5VEi66S%%YoBd_qQnr4^B=5TYM#%ajQf}GaIL*DKE#m*RDw6&3A z%6%&1ns1A)A4bpQze?t{>NXd2G%nf>#+be8h2kJy&GI~`>N1)2w<%8*;)`jAKm=>Z zS9n3`_p+uWSdy`ZUXd@awyz|ye&a z`&dq=5IiU3+LxdF>1g&qE8<(~kVaGn>vMw2LRh=@qadESca7mZiHyD&%W?aQ=G?fI zo3eA@8XV|SbbFlB^Mf!&FM^?H6CT&q&O=s=Sh1Q~o2?_0K`Fs_c+A|96 zFNrzn$7FZM*Qev4r*stG^ zyp;#1Uzyk8bMBmeX4$`<&8;xp3dDj*~t=u5o6Peql2T3 zyJta1uG<_NjGb~xxu59>`bWrk()R@!f6Wfx=hal=g*E-{udY6aM8ztRGk&A=lm|13 zt<#YPOg#3k`5n4&3A*rHkqEUVC34fmS5O}$n%nT&cofQ)qUg)Evk&CDoOkW_@h=Ku z11b~(5lSD@z2?KgqfAAu5;6Gr8M!u`!6%dI^Yk!1AZKYLEyIUp+ej2EF!B z;wViN{q~?jK}4E3wd5BHrS&_snD8mD+E6a+fc40*cAIcre(Vbp6mtJXD2@OC-By<% zzX-Py35yeF!ES=+AiI?&g@^16Qho}9rx|w(j$kZ5JoCgvVXOrkPrz5k200mtboc0V zY5yissy~&#+8-d}i5N+w?F;}-ZX=N)dc4Eu4XT7S(?^g6KDM4DCS}8NSc2P`$aLq48>>SAeT(q+mes-N!a8iUWHAbBNPrv z>`)Q!FriJHfvidv1HIGV+0SPFLPof5pX`E2CH_vNN|YUIYL_Cn>HbcH7|Q@7HkSDj z27{@{7lZLHM|oMme)4F4UWy4vu6$_uKr-I2ldO^@@pBQ7QWUs3!7u!Gb<69{Yph#x z>v^*yO^cS?I>j?}(UL`xOFcCqYqn=e%i@|v&5P?I^$Q!CWl6--+)%%C$^2>Q_tH=( z^oa8gJ-dWrCAM2>KY^8^4zNNl=Ye}#f__5vgGnbjSVyF7B_h67SBsR zFRQChKQCC?w4$zQ{lzo`x@2*~lE`_c`y^Q)&^JOiRNW8?Wzogq6sV@?Ch7iya_6n6JFot{ zY1f&v>LLpzkNi%UuO+gmu^FBOw(6Dj4a*`h8R_f6+R})BUW326A>xlL_1F1bw(~Dn z%%FV{$)*z7S}a>%fG1EFQrBnQY_>+ z*W)>U`sRk)ZfR^-a-8&Tt@BnyZfSzOVf$Nn_GG^|*QwX}EsK}klKm{{ZU8&C)HN=- zMLNt){`$Jc`j*DJNP~a2>91e9v82c(ZrptE zc3TUrZ)o-}tXtjyCTY*IrlmwQH_Sg5W25>lnY?sz!{kMi=TB~&{LRVr;j1Svo*bDx zvu5&@ldC7+HraDcbKR{Cll^`_aQlJVf5TEmqUM|YQKGx(PckcNM{0&V_ z{wBdjsp_z0{u?CvCJL5<)NkTae`sc{JUSiMkF=Uc0TjESsjgo5$G>!epN{EYDiVw` z=1U3srwif#i47+zN9S!wOS}pGi4}f2sNlP9UZcR>xRn2>fx;(NsKPVlE^^DAzf>{X zvX+Ry4ptIL;$NXeiAphV8FZ9R->it)u!M&UsG0KSyXCpS(vFflkS^tI=5aSVS`ieq9wW$NfBAKWYcN! z@chV}rbV!$lNau6X-H?z)Eja0xpZAXRZR_bL(r4eo*Fm3E1LaKU1W2*Y@vLn{43q` zE{FV?^wrtv(_Aq;Q>Of@-Sh~DW%R zeO$CeSTiFg1S*F%^-CLJtY${22B+t^nfw#O?qlQ9TPHRnz+eGErHgf8$sugmEJe7P z{1a!ok4u&=nTRx_zoSd|7cZTUoYCk~4FfU$B@;7Dr}0zT+_-e<(iLTHhj!)OO}fd7 zPbQ0KxJ5~vONc(nqht#z37O`ie7!vO_Pn7r1#Xn3L9sDgE zR#v6+UQ9S}W-8Ug-%9>w@h4@LI-xv!c-B&E;IeNQ{pa*IZolU;Pe~*sar0Vk9qX@3 z49V&{Dq+o$FHn4O5r4M9y`F1H|FHCC!X)OPg*X-T9ED(IfSZn?J6_Gp+^I z&v~2gJ~Vq)s7Hw9AJ+oC2%0_o@kmA$Eo5|`A>{21O-o(jk<2~QDra9cdiKo8Ef9uH=z^iS*z^26}q!P=AlCeIQlX(f$>?%xgL(8%B;bKcUX z$k>xS>YeW@Tu`^DQAPz}N!6|WXOC+>``mP5(uw{WnLL@mReEK1xCjj14p*zsh^sB3CW?x%--4)jL!D%xmdd%rnwGv!C)2h0vdU`Mv zzM|&xS+i@eyZ+j9=1g?5dA{1-f7O<+yztbu1rz`4hrfUQGUxp=?-{vM8X-g@3G9!CC34=MQ)V+M~A$*uHFPCwGu$Zuqu#|9+a1vqRNYWFQ63!v4 zBwR>1hp^O#PcGpW!UqWR@OSAX?8qZO;k`Jm6psSVlgLN7iEs|#KEh>$JtuQ80O1xq z>3RqU3H#*xDU3^blxI*c;TFOm;iN*wJHj5qR>DoE(mujDpG&3o5te=dct_KYGe}Pu zB%Dh)=%>Aen;7$Y5l2s_wzJ4iT*TY}5;!NUyTCmbYfCERoccq0t5 zl-w!tSCO7@<<-D-3h#uagmYNX4-#%7Tu8Wub66gb@LK94EWQDJ6#xgJpK#wi@Ju*o z0r(|M+zNbzTNaXD!f%q^AY{Y$B*K*u+Cf;ll5~W5cs}(={O#aL-dE9X48bc`0|()z zDDV^RBfOVzOB-~M@9ofmuy`H$2saTHp33*TXa`}B4P1l^H=w%^?)xt7ChYhT=?P1J z3jCkr{lCyx2)8^+J%q(usE4o=d)F4ig@nC?n+OvU|1$YbqyE<@pD>7H@pt%-=rx+TDaI-SmP!SJp}yjr{dcPBodFaz;i+;V8a-L~X&t-n(-) z6O z5z$d~1slCm&AO3yGZ+Zm;t=`3`$toYdC$OIqv76|fmk>h}yK|}wiZ|p|7x?4883m=LqMlVSR|u$t5y=D|;hO~c{zdXB zyw4DpQ9mhH_+}+Kz|+E*Zn;k9@(>RO;(eKl$PU zuhZWxr@u@2+etUK44q8iAYC@Sq09Oay1y!HIOtXV>j!&gp$maGlX-^S!>LtEi z;#GU5r#V>ayO8gd6H=-Fbm~jX2i+F89YW(P3tCak_({2<7jMC}QH~4n++6T*u}B9Q zig}fCWiU>hmr9+KDL2C>E}nYx-15nx^KB4?5q6_p2xPK8f#nC+3&5lykb3>enBU zuP~TO9hPz#FV>B`xnLHjPmPi>L*n=e9>yRG5~Q0W_)61LQ!%Sw2_9y+@=n@SOTI#G zk)Fx3^s99;PDc?jeqC;a@0EOS5ZN>wzrrSNe^nA%3eouFUJmbCs&6+7EmQz8`h)tK`awNT#a6}U+P=v=p*3M(SxdFtS+I*F*Iq?qEzZuNB+W}GK4EVX-2_-#43KWNZ0yz z%u$?lS1E146bZe~=ipPupGMLpz6sqk{F;^zjy~lI3B`Z+k-l_EDs>+Gn&Cg)2Zza% zR%kBZD7pST`TWhP)Y%f@jGNNl1H?}v{$+`u5zmSGuA~)u3MAk2WiHM4r_}dj<>3Ur z-^<+c9{D~Kk&+Y5m3$WQ{uW0MQT)z$A<7Ux;Xh20o{fC>3Vs}UDfn1Le8=)sO0JGg z^YigBqQ>usNWYLd?RV3Bh2FIx7vbjezhK1@%~avBUkT(^EW{#-0)_+om&EMVX# za1~)2D7-V3`YGXXybq_7q_;@7aXoWdCq1n{ek+)eN&1MS`#y37_@w+#;a8RZ!T-zN zcR*=TWNmkKzj1m7-eH&^12{5-Au>C^-z#hylcen9(%=Yhc%et|Gby zT;m!sv6#_a72Q>FRTd-v^W3VMH_)@+e&2rQ|Ihi)_JOXx_da!R)vego_0|AizWMOs zW+orNU+Qu4f|(2E0qes3lk*n(`wil6J$!g9o}K=B{w(DW{wnZ2mV=a2f0mQ((BovnV{0%orqWT!r{X&XM@o80_F$q5#!Qd5O4dRO!`f}sP8B6 z_k#c1n);;YgXt{{^=Uwz#df@=KRUtw=ywkvF2=L<2Om+xb9-EjOdSe`Uy3;wM&8wV zrtCl0e?0IX5B$di|M9^8pB~_rO1%gA!gGlI1GW0bUX0nra?6b>X$gDCSVk%fRJ( zBml$Aq7l z@Sq7Jy^MVmHaB4x6Am)rcoWVt;R+M3GvPHRyv>9=O!$lm_n7b#6CN~S1i#?rRbayA zCfv0mxPQ=Se0TfrT;Tw)6H?&1sS+=h_^;%t*#8I5_HQHpOJO1arRs$Ix^>;0x^=Va zhAHl|>nc^ZZUR?4J06cjJTHoi{X-tE za5xs@A5X4>D+^cs`t|D7&kcoGN3*hWa`FpoPZAt*9?Cg0=5z9&sMI+$`I(pHnW?LT zpL*5A{Rz)jhn?#YPmINrCdS6&6ca4LSly{Kan#Gp#^tn_`Y1zCb6K9{TiXn+ed@CO zRJ|5M-8Q~W8=^@1ES_@y=SAZTDYJZwqs;Ocavsev6$?@>@_E^`X-d=N@nSstZ1ZDn z(BDii<(9{1i?jME1G`2KF*d#AQ_hRJ$Y)61sd}l4ddbVwOPc&ty@1xlhe+aB4E=4f zHhSzXJYK0GpP5JEtX^VlJchh1O$?vec5J*< zA6wlv9#`a3^-@lm<)zZ(Qzv!Xbm39bX7$>THmwdslX%4PnR+Pae~TxN&n!y>f?)`< zhNhhVQ}q%<9^>;O-qH+dgK4HLwat@Gy;9R_<5Qw)Dl zWEoj3al}}EQ!g*DYSL`elWK=N>R}qmXUKRK15)CXM}J$r4F5O(TOZLLZP|!q4*i>oC&reUZDZ77bu*5A zW=OsEVqCUWmNR9f?L`~pQ8#&fw*Ret`odzYUedg5-Ldg)eWNbMvwViBK4sh>z2p;X z?Qyw`bmo66XUP9nHy$PL#Pec$Dd%PL!?aPJ>J#e+VlB?%qPAfIwx%sXWa z?HEcvc~&n&(&Y2PV_JT#Udo8g#E{2&!jS2+x~*OthoR*WWA$3O%_C*@nKG_xTAd)% z>I7-4)8^IcrOd`lO&58VX4|px81kY%{^!MzJl5}2z1F7HZ{t{+x~Q8ppP3&%^FJ@E zm;6+{v|}$~Dd&IUd08FQZD|pho5e9q)k`sXjL(oZ7}{s1k^jMxI+!Xa&*DP#X>a=q$%6L(q&2wscDYqEv=9QWz%BYi zVR4MhXI?gKnY5NB%ZhRvpZa*&_?k-6Hm;Rh8w_natUbm}m0P~mL0PI@KJ!20S^az_ zjx?Vc+RN&;aZ*EKtlY-4VQPH)928o+K{E2Jej8H1eP)>H52i2mB8EJMyi!BrD6@GY zO$TdjM~vmC@+_acp#QZRhk)gqItOHbN;;Kqd8zzV zp5>Dl^q=;3j-y7$dESJtnsBiRMHq0i?|^KFsTcYDeOUwi=eq^z)XT~_4`$rV`25k= zF#Kn#_>)-kvsgUo%gXtF8x)_PW#sp5gZuZ#uj~hp9M`g>Q@e_G9ZEWsmv=3%sOVf$ zhJ!nn;>aMJWzn*vZ0xwP151X_S-5;vOUFrz)UjRpe>neb7n*V!Zpz2*c*@+LW%qBf zj_@is-|Y)EdBpuBa9n2%?yNc3&)!=CXRV2_dAz1C(}7byA)VfpK^s_2&<0PgbJIqv zfAmBxT4*|}5l7=E*0cESZ)pI3n;3Ly{N_T7FL_s^u1@`t zbV@{x{{}x1)%o3(qx;EUnSD`w?yb=bQavmd=dd0_oQEyD{qE zBlv0A(2+w1E?hCCd-stuSB{!CcjokE4*n3ZGZg1qPQmGdQ_@d6oFb3Qi^L-Xb6w>8$PqJ_ow{g-a}+9EO%f=? z36(~4K1KiIWZ8kQ52HqEt=C9-N=BNrCIK~& z=PYFUqQ$GH$N^PI#j-^_lS0D zev2LBi{Par&ulvc6XaUI^+V5p#4%|zsXlpjn)9ro<><6uW3oVD+QDmoH~BZGv%LrV zIHGpS!eyPN$njB3o^zU{kHhir0lyO|;gM?Qv|FbKQvzS{&^0itW;hQS&a9;~X9|b4 z)cHX2u8(7zK7Ud5OlK5RgqYMO2ecKV$jGoqyNUH~7~5QjgZo(YC?}XWID2gAqE&F@ z;xW#3;4NNMJq4$B%FpVkg8GmRXO2;+j|bxcU}MKP4;Utjw*!;*lkq~v8Df>X&~ai| z^7t)hm8l6e3M-u3(c91_&eB?nkx)FbVCD>GkL9Dn;qbjFOrsoU$9a!LND7>(Xup=X zU`B=WE~+qTeb^f73JzOy-aykN7ssKcoo3bmtuCD}zAKiJ<3uM94d^Pgy?ND=a5hTE z>4X2S!+FgmsC2~;Pp|H|Y!9J5JaSEV}xK*b~K?!1$yQb99HX$L-kKKPvM~pSmx}( zJed3rR5?ouPZOP(Kxdqcd7O?t1X)X%XTKr+jvU23rE2NYs@2YmM!R4wP}bk2O7-{`!P##^`ymB;xy zjaOCGp{mM>q9std*0yv;hov)|wuV=|fbH~zBsC44!}0}P;G0Zdr_K(Kw31|+zUH`4E#sk!X^V?6aUfzEC(q(hnjr~_5RWEn2 z7E#i7Y)OZ96!9+(e%s;LpLQlT_O}gq$>2e6R|Dq8!YfDO z+v2??7=!op7f$r1O2kC9!YOMfAva;Ks}8zNAr+c&zXUVmsP^H|?D`U%+D?Kw10|Sy zvIO&%OE90ERXDWZ76}$UE5V}AB&ZIfp@c)r+DNcsqy#H@US2q~YLf(~ZI|G59-0>p zo$;mwXMQ2U8ik=N96Gm1g7ex-aQ-j})=iP%f>jb+bh88(-zmW*PfD<1p9BxMXqn;A zj%E@(I9P(6{9%4L^w7l&!t?HuVE!Hn7O#f?!r>+SO?x=J^g#)#Uy)$hrxL8l!Cqt?H&gw%fsW|EMHxM;lW>n_*vO% zEup23UpfqAuDV^wnEw%evXNEKkO}{3SjfsVWRZVEXOQ&`S>m7A5M;g~%lvHQHLJjo z<$gX@6&kXe-xY&IR*@n5_^K3S14CB&c}zyJAxHanw*}eIkQ4nCXhKNO!x^_yX4l-0!KXtDq6kszBJa+!bs2#{rpN9?$ztNa~sY*rgJ2c%cJM)}j= z+N`!}rO@Xp|0pQUYNz%h@Ji29{!715CV=~siEt9<9E0Zx zrsNa14+3@4Es(gSAt#r0hlljk+lmKI>rP}BH-h2B$!J5_4YdoM#DFoN8gZK9md2cT ztQ3$uMvuqD4dXS&!&kCy>Ug z5g{I9!;n0)WKHtoF(yp7zgTXb<6VgS#7DFP$BTG)oJcq2tE<}J`8`+hRMP_9pp|`i zp&HW?&u`MAV#lq8MQS6S;~%)fXHRbVOLD>gQ1jWfYkr~nXe{_|3!GiOgZRz@VLZ{mX@Rlw{x;ugh$C#l1x^u9hoY}6^`m?ZoHR1KXLn94(g zk3XXkm)0QoguHMDo2N7MbR#gXb{SQ1JhK*F!Y8HpF6|=@aGHOVx_=a!L`!jLk@^So z(?olN#8G#PjCZ3idksC(GU*yI)?4@u{0gzgj*(>bL`T#gv;SJ(z5i~AR-!tu(s_mfUCeA=Q zLOeI8Nc}?b71~#vMrm=8D#CN(S|!bf^D4{l-5310cPkR4IMGU`D5aSXM+E0+HDk^S zCvl9&B2b)->H5UH!0>?KP;`}UTAZU<1M_piFZEb9ijy|o=$Q^gI=?A|b3Lk1oXSZg zF%048P7to}SmD$y6v|C1mo$QS?6=^$q3{??S&GywxH_>=>*7Q(6P~X+4+5f&r#L^% z?8zjHCFRY8ag)HE(1v6K`&526w|IXw~g7}0oViPPnc?IxqQ9nq+{mPUl*3dDO6r7$QDWM zJ~vIK>%Vs)I-bz1dVx}SQoubN8zy>>yS616vv4*r5foA%j&l{ z3L{jxwtv4aQTbf8H%tGq7VXEEI zqQshZNvg#3jP=`(uF+qfroY1IFUFtlaO!uE*OSt09j&~6=f@ds6Uw)K7ZWYSL_w1f8eFqeQLzh3i3`PwP_WI`vi2HA zf{ZWN)(s;DM)N95swiv1Jg1ayWpid=lC5P^o6RkQth?lxZk*A4 z;Q~u%L)DDp1wi-E#d9hu&D}^vJa!rCQ9O1jo}~iyDd_)b9?J4VW^aI2?Id0rlcYk4 zM^dO*;##~_77WzL?8IawtzeL*5{Y-vby=!@;yG;muvAg;tc6RRg2C*b-O`4ML(FES zj#rY{T9~AoCJr@8Qe}y2NDa}*HVOW+%~Iuw?o8QGE$ft6f|vM$VVdfe_?UHWxTbnK zEtaB7=~I|>Dc<4|tX_rL^c-q_;vo!Tg*h4+lHx0@qYE>Z*pHf7SeKDd`x72I*2092 zmyoh4tf$MSNXo4+PirYj+=Nc2FrS?->(yge11T)fRI|jSQJ@Mn)jaW1dr(Cz4|GzA zZK&sk4YY+eiTg1e6c%f$oW)t#NEc@}tnavwV1{4T4<8w)7M2vlfi3Yp%PDNaM`X)u z^(35ASZW%i*XoVqK{h=`qzSK84*G+_X8hX+XgywQEGd9)(V_%Bk@^n#ZPOJ<<+N3O zdO}dP03A#(a6Hv8-wO3`Cv%|Is>dL}TDo{y@D%=Hu!I4QWcY6o7i3R^vbPM7u0 zQ?t^sPCLac6?RlNCIvE``lmOiy&BK}iaV(XYofJbVJN3uJxv#Py$^$&dZR{Kz?ul* z`6D3g7RIDgeVr6CDunN&Mim~r1d}6Gr=TWJMtEyS2)lDKr&=E&Y=D_(VUM3sM5^)# zVHGNIVb2@eKsfUV;Y?P!UQ0a)&&?24sIe^|?AsKLr$38lTM{7dk+LXPBU!@+|79o; z?*||XhOkgZrQMtk>QcN#7miwiRYIi;KUr=C?CE5-n{pdx6|h6$*q>Vg4*$zIrNG?- zZaj7ss#841aVj2Tp?8c?UB?(x+cCzBc#E50%2bz*_3EJ4D9WvX|Gk${Ws3@AQEtsh zRMZwS^LY|Oi(4i$pLyiA--f)e(DK~2lVPIMt#F-kD_X4R zz_7!+;B|fsH?$SywCIc3l5#u#J)QdrrUT0D6e~)@)@@5}=f>&WUr>>h+ocn^jG*(F%Wt!QS-?KU-?dkGS+++$aybLVs+xBDgO+>1Js+hc1wcNBcA+@3qrx!D*(`Kzoc=!+u$X)&sSi^WAu)<67F*QI&f(`_6!;Q;3>CXt}y6C?^8I* z?cYQg^q-f2f7}5bg+Whvhv6D`-~eIJYu;*j#T_(G7<8C-8yw;eo+S+W%DWW4a4S~| zgKqNX!VT__^}?Wsyo=!hcPQVi<1sqN8;#7n!+t9a`o;SJ`F4l@UKn(V*Mz<7h!=%H zZ+P3$O5GWBL7h3MfiZVh5A-5kPIKJZXCNnCraA7ZOZYI|apzpXhib>2dj%inIPN^w zt1fdLcm6Cs%yQfXH}YYI<1XCBhxv}Xh{_$0+TF$P!LqmN7(7;wft`440`6T`f}y;( z2ZFugRADb9`z*Jgf}I3EddZ_!jNv6Lln>#b-xm_<9@2Wb25y`Laf);OPz_{p7wcIb~r zirt^xFXs~HbXSj{XK@|xEi_DKULB2PtTx|6svzV6VQ$F#8ocjdOpUG)>YjwID0mM| zw%~V&iwLU$aHHN~t{1p%j#0K=;ZjpBt5R9W* zrz0$^oaNKd`xM={7@d#-V_L0Ie%W_E2SVdClQhb zd424z-o_lqZSD<3t!6~^XbsHip*}#i-Im^?92NGY3L3MSwDBf&h2X1H!F_Cl?Y-NP z6_tYuVjN+NosS&29lgdjP3@BirV**Wh(*)Idke)YJ{Xfkn8M3x4n%jaxTir>XCQj0 z%ZTXZ4ex_ATz-UfH}llTb4nn+Cqvp@Rnh(c?;^yNqS}*=C@_iOpNViQy$aNON#>z+ z7&5XRu5pKXuRF&54d7?v43pI=*0E7ubIcb+N6#d})SKIT0dc&y7cEW@Gtv=K3md@J zOMQWx?A?L13vOcoPA_qGSDlER;+>C*BiKiRu!Uog?Cz=!Thesz_;IFYzn{c1E)ajg z^v9j${S5+fSNQ0fToPex_CNXoHqRS^;+G~_k%ZOMMQQI)`3>vHB5w!gNRq%wNvx=( z0%p2^<+#fGJ^BX0tqs80OfRL*GrVFn3BleQgf*Eyq<}rg8`#5S>8$`P?Q^c{UgT{= z;pnWFq-R~a2w8dVu1>~`3V*EO0R)1r2*MisSFmYa;pMh-91bIDQUI3ruj>Zfue^gO zPr5c`)$eU^;U=}`{{VZ%*SuGcGuVnCtg$~9 z{eip3y8yidrwD3N0G9Tfw*u}1Z#1f>;MNA0(4MlXjb&doVq9dxh4PnF#%BjbM_~yFDWG4~-vfdauQ`MPPCkpxwA5LU%I_A{?kSf=SLLoEe=PnwMp|x<;6W2WFVbxf9bS_mWUA%yXo#9a$ULCg(lO zHQmcYZwxUJ&aO3rNzQdBR`=Qvys3M>J8O%;^UA!_&C;DS;V4IwVjPOS2Aj(W4w81F! zPrTDZeZuPScIYuQbTzIvF#F$O3c!y-kD*xAxwQdxGJ2@(oTz*n`U!)-x~EpK@yH83L!~s0CMZPXpCjf2*J9@2_7C@fD#7BfnIDNY&9lQmU@OA| z(Zk8iWJLg$ebrRvX-v2|#&*GN4ZzuM>TNVrcU<^pyrT&AR|4Dac75np$!UlWDwTae;qP+et58L{VM{nw0{fh z$nx;u64Og;4Zzv_7ZSTVd?)%x=}}$|!W#QK`v7}Zcp0jebSnn~u(W>;66jtKzH_+2 zHG#uwEB`EFFAm3t80^R(tg&AolU{dYxDnhg6NVK5SlT~?#RvDg@H-F-ZfgL}=6?&Z zH-@LzG1!-bu*Uu^C?t1lco4EJ9pS+MEbX_i1n!RT0kkl|H9_Z8TmD1D-V>gS*|uOu z24Ri;Px;b+fA|el8R=nH1Yl`@CkozuG<-HD1cKWdfV2634{t^86X7A~&IJ2%5Z2hA zhmPNUCLA4Xy6%GkSlaJSpS}`43H^k0@lEhhTmEz5B=`03ZY<~tc4QD%?DtSTSpI(s z=Vzl&TN;4lRqpPW0%`qYbIOI31*KTAe`b((;BZ6Q(l$QlqG_nBIUo8)S*eSuEiSr}( zaLBze2xq1Q7qe>Ad1d^;qC~1>ww;;0DOtqa(MXYf>|VaAr!dAN`SA8o3#xuDUb`XQl+# zA){`K$l$)fJrIO5Q-bwqUvBHjQ^-yslB(WKV&_i7vab0Sb2rMtZ5R0--X=%=l7ulD z+QT&?fT)Nx#N2`rRWsCR;}jwa?gkV7ZC4ktPLbU}tI7-r<3UrY3rM%f98?rZ(%dA> zd|Ws)0z{9!{l|lEc#8lL0Zd-a(1D z10!FzF}8T#iOClm2GYBLz539Is_!^Y(1lGgWEuohCoALjYr^ZOQtpUIe4Obuh9$9# ztKHp0ZDy&AiDZw4y6OOkRky4UaN{CX=!A}?>8q1iQ*h_NNAASP3+SbX67k>>2p&Y@ zPL8ZXFqw$AjzElJYEFraMz1rUh#!wYJVtj{MGh7?&Y46sLMM>o0yt?0dJlI-WD{Pv zHxSYL2t+?{+}V-W;j){Fs5%1i0;g|tBiEp=ZYScrBM^_Ue_R;(2K_zjoBC}MLI3Dt z@2B?{QZ~x*%DH2s;`T z*JPgVvM~Npc=xmj=Vy&(0yZcKqt7htFzefyk(W`j{Rmr-gwb&pc5O4j)P16Ti`!7RMdb?$*e082{8ANt`&2-n6)em@&AwN1n|C?)4;2JV$q0oR2bf?~L4m zHm`n6;>30Ir^WFH9PYi5FR^5%%235k@r(bY50IPb0yh)^{6M4|`cgG62^J@&fRTOl zmshcedMI)QW}oWZBv?F|1kOveH|5wlSDiO3fN0e&|L z7M~`;^e(_lSV5kSe8yocjxOG0P#l{C)5QQ^UIOqRBi-q>E=jO>HwmVn0q)6x`K8E7 zV*s9>1dEH4U^*M%%_sx+waCxtMb*Y6IK$5$%ocEA6yQCPn;QfCXcC;^XYM$F=0+$D z_wC4|Z2k^tdV-5Z-$b_LnwjB0k`d&kz7jB9_q(h6&?~wJBjeCD$Sm?G)L7#XVxjHt^?>*x z(szJC^hhGO3;@JB&I*2xk98)dAbP5mtU~RgdqR%GtxPI4 zzT^F!MgRB=#L9XT#UFF0M%(nUyN{}(d*?yS-AB)k!o!ScU367aiEgBwKSRphM-5oR zW=F5>V1!TA2n))>C>y37u&tb?%#EJW*uXxlfkAi$g$tvdU|bBu@SqK79b`H^)oCXH zwj|mOwMJm65HaI4T{FaSDx>aGH>2}O%!7URX-bfdM2?5>8BN6!N0IxirV@$kiTtCc ziW0Kh>iPSj0(*25vc2l%{X&%`Wbf2&t_i!P!)SF#|>XVTDPkT=k zsxl!voZeX`)aZolW_s^Rp(Z9|)6%{Ng_@d>ZApKARj5-FvIXhm141oM$WEhA!=q_^ zRYJBFeO3U9Kc-H|hN8b}a=l8(Hli=uh;*Y$$iATigM{3q60%9??^A``tP-*#=sQhr zQwiDr^N$rG-L4X{-{*Tx?og@-1{z=W6#9_zWxGs{nk@7h*Oz@U z33ay6>s?>=zT~N!h2H4;vd1M~aaXoiy2Pn%XbA8z-(o8)l^lsOey&z@kPeSi? zebi)Zm{mUtz0dV!Uq>5tG$sIE>3-Ljy&LUSZ=nykzUdXwkN-iThRF4~e;y4mw(4@7U(QRr=+FZ&$& zstH1G_k7vQFhH#odZ*{ho`pf`S3>Xd{2a7Kw^BVW^mCpsdk==G4}{+B`Lf4ggz_+h z@=Eu5zU(6yrCJER&+}!k!11cT(EB}K_63}vrVD+*^X2>hcy*r8hhU#gb&}d9^qP<_ zpYl&quL`|B!q5I;>FJ)ZVAg?#zeK2uc+y*cE|r}R_R5}~(+eECv7 zPhBJQ_K+_h!xyT@h29zRwO{C6AzwaaFH`*0saN`3$d|9xD^(YvcZYoW7=4WkQ2t?GKBsO{p9#G! z?8_I^U#WVNDBm9T<%8%=s)x`!!@hj)+@humy({d?C(T>bg+f0U_T`J^?dm?EcZYrX zAo*MMy3l*WzI=DQOC1t=U)bNy_OM+wI+6DFhkf~a_&e2C=mTM2z7XzEQ-wYh_T^LH zPIb1>Ya+gU^Ltp`B=q`-FCX_FQI86}G2+WtyvNkLLT`%r^0{u8`cdf35nsN`{XsRD zO#9m+zI=dtN*yco_J}WE+MZDph29zQ<&)YU)heNPMSS^m_JZ0Z^m7qkK9Rkob_%^a z;>)M7SJYmi_eOmA`qzz7Up^y!X!_SpQC~h4{n_-do1?yb4*EoG5`EjE zzI^uCuO1P4d(@XtJD;n4Lhp?F^7-Zq#Ur_Ax{71E0=mSw-K7;(Mz7_gV)R#{k zhgH!jw7(|i%jXQ&?I!g4m@l6#LheaIZ;bi!X(HmT5qeY1m(LGzceBu&W4?T5@ZG0` z-WK!aQ$mirU+C>IUp^Nk+}IS_+ZpqBvVGQb+X%fY=F8H5eRs6b&&7OM-Y;|)3%xt$ z%QF5^?o~qXjrp=vU+nG_dSA@%aTN45cHaSE)}KoIhhn}g zr8jeR`&bkAWx2eC+fn4}uwZ!bKI9D?e^|H zLT`)vvUXkJzAW_ixG#&;9o;X4-Wm60HM+B#T_xoo_hs3+tJ_-W=i%x89JA~dJ_htRJpZkK)2jc#Z^v?kIuRWL7J7Y_FYB{I-C;s+%<^R&c7(fJ=uKI^EVzzxFB5ummM_bzW8C|N z-j?OdI_g;W6`{9h`Lbv_-u+DIomsvtkxn%6cV+pqAbO%3o6ht+m*vaK=Sgm<(7UsI zS>~MT_7WQB0Lh}}bhk?AeObONT+VdY2)#ecmlev{?q;D6WckbbApdjShlM_r<;%L` zeD@8Z*Z97yGA?w#6neey%bMb1H#URm+35SSg1FReBJ?KTm-WJBZcm{%`@XCWu5iZ- zz0LP!EpU~)Sm^D(FDrkiyO#;Q)AwbG?@af8p?CSdtmU2Uz9sZ?zAuY*=eWNJz1#O? zb?!X3bSBfY*Y{-^ZmnA>^giF0^|lM#1w!xleOXw$(7i(F1HLaSXqUM634O@-^-7uh zn$T;qeOVFP=>A>k_1V6xd|lzzn$u?t?<_%=TrG>1Ov`p?78bvW~RHRahE9`DgpGRP<}NxzM|_ zeOV2<)g31E-fUkMd~SCa3%xJfzq|zH`y2Oaq4#I|vTSpQ`>@alvVB>Zx!e6*=tJ4Q zEWT`a>tgW%{a=nR>nr!UorGSWbB-@- z9S^(T3cW4I$2`DsA90)F-5=wBjxQ?~yWFut@67RKRpJls0-<;1__7A^w0nWj&*k{C z;_$4Ycjx%BzVN(zm&o_#__CVtlKYy_`*M6)FnGoNhtT_Td|4BC%`KQq`v-D-dHH|C z#qWJUAIkCNCI3&Ry{@U_%iH{0roFDO;0Xi~9TSETK2m@#WS0L-%x{ zHv``r`TdJ~z0lk0`10EQiF?1$+w1u9lD*%3S?Ha0e0in*!rd?Qt~yRbzKY(Bx5EP7 z@Z%!>1}Glm3ur?w)5s5RL@pmW`RbnXW1M*G&j9A}8(BQ|u#LmlOHrpc^v7dgQo@ZP zeAUzvSp$g2Vqi4nCsDq3rb(JmLKI&?wM3Sfs7C(;CLZfa47q$UO=6Iisg!W-f-k8_ ziT*W0Ja!tzRL-~76fsj`m0w2;y~B4|jgjBg;Ma&&!uQ&g*z#p49{Ur;gz`mK#7NLV zFnjUk)-rrN_u}iVR$$^b1VfDS1yoC9^&lSWKnat~H&rb$J{%54FTUNTac2=v5Aa1d zDbZhu;;}O*rUJg%YO$=!#bdv+629XmCFZ9l@z?`a&UfAnx&E*okG*Lz{D7W`k&o)} zSR;t?_`|t)?0dp#p0CLoZt_+JhU9`T%t?tY$9@#EM)3VP#g5cfk63|V`mOAY)o8h$iUFfrB=?hhYY#3bC<<% zJt7lh?7T`07c{v>k%6I|@2s3_78!DDCm$lw!F7yGjInbpG5o5}wT%o6?VN1oT$f~!D5K$=}?L0lRN0$H;+SRmJo0)fWt(*&}^ z;9^n`kY>NMAg(Y4fvnj)$VD5Mo&tf!Y)=B&e{fAI2uQOtEr<(MK_F}PA`9dSRv^%r z{T+eqPPnuc1f*Y`D-C1f<#LEr_dcK_F}PD+}bZTp-YxErMM79Dlu~Cfr4Z?< zUk!%ynF85L7>^C6n7z!|f#O*dvp3)xV@gs??6nlL)pE)4NbxNcvvqL=@<{QM6iX`z z6n{i9Yb)0$kBqIrm0HE+$|J=k6m#U67%1)yZm(5GitH5k6v{UR$muH-;#rjM43N`V zkl#f4-T*nh1^Hu?9}1Am7QcAxeaefl@R*S^{k=CH`-Spu0dhJF`Wr*kYhr+$?GW@XrV9Uo81WevIWsV9ET338 z7g;mp)=mT>(ZPk+OpLM9iWvHyi?JCP+8Jr(T%662TRRIah6}ct7-Q!OVpxV;#Ld9a z&JHW*qHc!V+Sy|D3>SzqF~-g`VpylR zXq*wR4-raA7$UW9&Rn4C^Krn=>%9^QDz@@i{|o?bLxtbZ|jB6JzXj zAcl3Ei_{qy+8J- zA?wAD2q^&BT!dvla3u}=rl7@UrR7aZ_{^^fDG_CVV?6d1#J%`oAw^_j)&W;i%kKRO9{94@nc4sqz)JjxtEWhHPR$y zlyKu7KX9Z;`cbkS5`OAPlbl2eea?>_TEg7f0$w3v+_=SAi)HJhDJ8ZPoO4eQ_r%Gr zTv`8RZmkYo#~vn#yGCV8u7G4qsnNtX6OVmB5O<5pep~^`7DoeO!@((UkT>KmQQ3?u zAlcezKx{pz3IuU?sO-cQkZfr*AT}XP0tn)+P}znnAUeq=ob4zc!{5zvdhwG?hIj1D zY!l`DM3d?s{iABrBDdHm|KSX(l8oBr*xm`IPD}t!k z@ilUBMRJ>RJl2i!r8RQ#f;h$KpGf%?HF9x)_`}FoQO-|7DfyRHB{$>GO$6~fQHm2% zKyU{<`6xmBc$DIaBuF+@#$z85#4k!I?nr{@7-)(@-ix1^G$>;mtZX-q$J!9YZ%-N| zj!9~gZN}*O2;v7R4HEApLG%;MtssbBsWeDjlmyAPVl0Id#9hN0B&{e3lI_It*vF9P z>0Wae@%*%<@j*L^?6jXxgcJ*z3YkRj$w_@@TqJiMh8#4Zidej z&rQP`pW$BFO&pIM4SAk!kv{-0lMdNIEX`JW_~eFSG;P9fX~5*S!lS_X9Z#De9Fj>^ z!Y_JSVn&H-E8)IiEs-6ya7f$_ATytZTj%> z6QHvC55@_)?&#=RY`FDhOY4{@O}hB|L9(ayctgf~+0;7LkO^P*?T(9Hg@`yk+?SoY z6QaKpvc#7ixs!~v%$Gg5lcTSRwA`1ywkI2KH(gh#sDNdSq%k#8fv8THDcgPHvFojv z`clNEY#ss^@AtUp9hOXU6S8+)1-2CX--3HwLitC{#}?iato19u6&{h@#5edFqI{r0 z#hhMbnpGa=It=D39ka_Pz%)Z%-Pk-}y)Y4!~f8>c@{Wc;+;%Z}ptn_SxzE z4kLOCj#PgjMD_hvAA^VoLh?@0n@8|@O#{GvbXt1z*Js|qbO6u$#_!MA>p8vo(=u;X z0HDeM9_<0Z9ch4m_u_>UzujOD+t0=13(wTnS2UaFY0Gf{=i3qKc6)OtytgY&iPM`u zB=I^z~2}!k2mpt9{_g4$$IOCb-m?pI=yex!BTmK zN;-8*85)rSy5mHoe`rM7R_|fVY}>_0dTC5bX2<9{vjTr46u;@z5qXHGcVc=(sRpG| z4f-yqMjI?Ob%sB0^)5&Q_r;&0<_}ysZs~$M&J-Mf>FV7d0Pd^7BVZXnd6hcT|Al;X z7o!xlB+#|-y2XTieq@Q`44Aoa1u-0}Q74>%-}RC>%*Q#T`B@lpDtsRFpiOl_k5+V6 zo&x;>It4XW(bGdn_nZQHyx8PHDTCR$@KO`hSlFv91YMa!=HQj+u*xYKtrk*rG3d&3 zaB~LVh&wV#%~w+NM~WWh?Oxt)$IThS4(}Y3pDRrW)0GDa`JA_X#5qHIq7&Ubo{x^x zeu54kK;l{~b(~=}{py$C43ttjUP>t!z4^d*2Ew1md)#Hv33P+uZ;&z8`vv~+F5i6C zaOT%NK{`M++UegQ2Z_#6bl-N9r0@D^ixUuk*>dah9C7(Z+88!k;&6}7Tcj^V1n404 zC(^C7&efnhd5$yU9^6zN`SWjs?x=_7JjKrOOt1$vXbw7Gu}k|M=`)9cZYRa{8R;f0 z-U`M19E}cPP}@PEJ4taZB;Aa*x=H$P0X>-A{br;eHtJQwquoGPw!_UC%#MCHMfs{N zMUyBRMbXIReQ?Lsr>_okmO*uHYhaI(WWNp7qwvEWXV6KxU^Y;E6XhS78YFouR_tsx zfga7V;Ql^Z%M&J4CF(jFe1NtFOGgh?>S*;OxrdmHzwx$@w>Nlu9yez&2cE@5Hd0>@ zc@srf@V1V((|B8qn=^z~OHA6^nou=X+9L8d((rbf@O9=-veHbl@+p!o=5{NXYpza$ zymoVv$t6bBSte91)LD!rnHc^K< zbQI{yx?~RK7$-lqt5A8Uc9q9dG>EtEyp`kT4CXjDmGL{Nb_7*ZG>4+mZ1vBh8uNPF zgldQyM_954kOW%srxrHUCNPh~eBN1z(=m2F?s<>qInEeWf!3^kl#aH)w(!0QkB*?# zt#F*s#z{l` zh3#-WB!i@#D#bzZ64Fni!&D)~GwlCB`Z3aBX*>KI<%2mGvw^Fanut_Y{y_hH$=gS~ z?ZwR*&#Knlq<@eJ)#25|OVLl@q+l1d{Bk9kS25~Ej5?9CfdiB%Rrv(j5Ab#eWzNuh zo1g?I@X>J|H(|RN{!-k^)IM)EI0Ixp`Kfea7lGD&-lx*%-38k8dHZYnyb;ppedft( z=TIKOy2}_CHBlSfjcDhm8H&fPp{K=A8_?R&(_&~8Xk+M^8bi8seb!@dfX;P{YKID- zws4}c&$z1d5N&ZO;+LLni+&4>{tmEo(Qjd8`Ie*<{T3E|VU{iWEiC%!BqMdK`a41D zn(*snO}Ldc;X6piNlnkunv7jGK=N;F~OxdAYY|)KQlZTq*RvD0XoO+O{i|WZ57KN^~jORQsN=b zt-HufR+b5@ETbve_5mds^+h7Cq4giO(7PuMgJg z8TW_!kZ-4}UtrSjBY7|93H9NR$fxm&BXiGncII}4ezvosIr*1T-=-qoT)e=#J;!iG%%ABl?j}vlvA>!2PHxCl<}M{ zH-YnbRhUrioJITzv{A|1aWo_a+{r-tSm;9+5ULB9_NLO_@9+4h_Li8)Ypjw-_ta+Z zd;{5Np^yC|-QGsp+f{rz5>adZo-mPjTP1%OM0`;GID_J~&qAM^O=vQIyJ+v}i)*!4 z1y|(`(n?B9sGi+H{5f>xa^B|ApA*=-9-zACUP!l55(e}6uWGfy7moQFo%Y`9O=e*JXzvsgd5Kl> z&RpVy>{-Z}7W(eR>Gt-~-uw6bQ+s!t$d6kk``!+)cM|J}g}h*)AL_iJcsWbzRH2wdNagQ*VdcpSJ}iIxSb}YuATHPEL9tU1*)$qOBMrB zpSKuqzc4*e#~I3DgU&Z2sX7>j;lRm?xXR*=vG_yARe5EI*HQk)SYAhY)blmC*HLpg zhV?N~?iwcMoga~CgZ3O|`b?5CXwTuGy_uxU*XD50u8Sd7kJ9s! zqx1rf!xJIVqx52q(i=$1uw2by`5lrnXfLDV^D(O!x2QEzxq{>flBd>?Ye~w?U^Vmb zI7lgmGfZaITR$vpw+4RGO`ems#jFksD#^?khc{EQcs3;&jMn4!*&Mf*k$kZe z%$&>j#ov;YL4F;J<{OaW2>$taUhPe&uI&m_+7aXVB2eE5EKpr@G6Bi=08!%@RJK)f z)mce3tkz$e3|XMM@@@ixs`dnVe?jKdxvvou^!jo#8}?LU&Q5WyX%u{I=5?h_%nc3T zpV}LR#ocW2H>Ea;Q;OQd%x1OSNLLTn+FRy3&ezbZ7hyKbia-^3deLRGEV}%bbblrB zxd`(O=s{2P2EA4OylBq?(5uIRzD3^h*Mc6+HLGKckqQ&4+tgdk!w7Z&eR=DOn=_Ga z*i1LvqEh{E%5inTo2=HD=o?1rH2)s@TduWfo-Aqp7CgOhG+Ew$R-mcsJWQ56JPvw* zyjM+@zqY}@`ZVQC6esk5HjfJ_JI~e&OD9Plu7X@IEKQMxr30kDVjic7{^n?hvU;?e z={`x_c^-0;oRrDxQ6dJ>6!j`aK_k*=t{(ktGFNwm&K&gr%m^7PNb-ViF~t@;T^)Ts zu+rcs^?|w1K@QrUfZbV=jYUZ2pewRL&yfX}ouq#=2=she;818(@)Ev=X`Q92sV>>d zN3#-F8f!DH)j8@G!h&kFB=cMY++ZUtP|wsPa|oN`V@C3lMXXdQZBf6wTiLr7bDGLU zp{L*`QWgGcsiRI$i$3*cmg`wcoxBcf^ztHpmQ<2{va)!V6ktDDRXj^7M_;KJXGz8A zCzawXRgSV5v8gT;tr1U-kX6R>q%w?ZQ zR=vX1Cw*J>8R;M=R((r}6za5;>^fDsFqKlts#=o{s##TiD9NZoRinvFc6n*Kr>nF3 zBH0r*nl|<|-iUZjM+ND2p&cJ5aD2Q0EImF>U?=Ps+wpM%$H#Ug#m^$W2;>ZL??jG| zpMaFE_Qg?J|6UWS6F+0>^w7n|eSqaU>74ao&Z0X`}~_IaCL(^Egd$gJnlG6k(8Ogg8u%E@NhdG zF5$!P@bD%cMAgkkl~t*(Q%|R>k`~3EDDoO>Le!RngauUw-8x|()hn0YNYKC;%S0Yqoi0o^ZUL4KZ^LwQi`naFp|J_M~%Db-CE+`!Q~?PE-+PTER6 zQgBkT=NiEV3pnM;bik8Fz+VOOQd9qu4hWgLmoTB47QQge#6Tk$V*xXo*8onYQ`Ru? zvvi`I>FQ7ykYk+VwY?@L?AjPVX+%7|R5ylIdl6W=YL8*nzLlg*{l>6rzXMX&<*}^G zU5|#oG3xG?P+p(%a-;a_@!HS=0KARXKz$mx4J>V-J`H?Dl7DnS3E8a#q_iC&&jC5^ zwQi6W@cYU0Bu94zSwxcOfseZm!?|ievJ1&ip;O70y7NfBPB%2BbT`RZ7-R{_MomCQ zaZ^o5&Lx@4ZmSu`_XkO~LS|G8l95u7_x1wWk|ckJKJJa)AX}5Xh2$**LAE9N4apr$ zMthR|nu7f91dtUZF9)eT*ohwe0HoBJYU8=HO{fb0aZy@{zi9*?TEJ077pDP!GJ@Dx zcm|g$J~kcD!U#HAK%*1W0mmD`DHd?_>FIzojo?BHIOc|Q0RMO{ulp^a^wA`MpW+@d zg4Zmd*?s~}gQ#*LZ!>Vy9ADg| zP-X!w*VLxVLONM!t1aoedK$qA7SQ^|+H_gSR10nMMY^t2jbM!hv@N={mONU>B^KK5 zSVDETM1Ngn1b13M`x&+AvXGq?TD~@2*DfR2YXKG8Ytv;Rf3eUGuO@Y|tKu;UyoQ=k z_4<1fz)!OLi+Q|eSwQbb8<4ZxA*%cpZOrDhGnL??ckCY%=N$6e<)ki=Bm;aDpE5q@Zk!DIA9sXoqQ({#v{ z9sI?Vl_N1Y-CKgQ1@@pX2Pb+{C1O=Co8kD*5(U27RR`Uck@9BzQG%I&50RSvvjnFW z!eH2&(@ui9gCv-DiUjj{LRr{baH#|fxjYy47BRVDulh>~mgPrCt>`Mj%1IKeI!A)j zu9M*OdnGvIF$vDxBf*-_B{)|_8U4H>3C`~%!MaKbF5r>XVeg{#5?p+>1ee??!G=Fd z@BsgAZP?pUC&u8xatU^hkl>+Z3_=rrCBcagN-*^m399~zz=_1dkyzAKv6cC7pBo)g zfglw9btM87SuM6*BC0pIBOWj3BUHp<@z@{WTf|y}SYGrv z#A){iI9VPZaa5Ap;$fb>2JvIr>Uu)4<&8@jv0U|tkTL%w4A8N9hD`W8fj*XJ$RhuS z&LHa>vcx|TH7AyD$TB}0RU%ek$Z|iQstOI+&F|U?WRW5J_zEL@tbrjb{X8b4*pQ?B zyOF6_LqksVSM&$jSaCVsEuHFLULRzMdPc}o{bopetcl6dV*l5ulCkE7T;|_D0%V!m zA{kue?|@@tZSYzL7vYpge;QmHYpX61`dn<2Lyd~HQ!hfdS9+fE`9suL2lcMd>y^Kg z>UyX<1;0`GOZtHxpdJ=_lkz7w13l2to0Y$c@4>t66<*y@Mso)&g+o}9( z=;EQ0darbs@}GwwHe7uq_MgJvw?DHfYSeVK76#?dB*)_8zel;&+20E7EF>(R45Tf9rPXg z3>yy<@1;-)r@mW&9B=tO2cUtQG(&+oY^7qy^(H$-BGRj9^bdL4a} zW)yoVEi6(S(St-kaD~rq%kr1xg8!lBvmev^LiG{mS<$xz&VJ3puc!<5<5KF19i8QO zhZCYtXf?mJ`~p59Va!C4I>@LETsC0EUa^2r1+-vozCRR*@0lCL{<45YDmTPl!`O3N zRzJmFwBTVzB&l!WgCbSH>^R~U#a=e4!>07UK0s{LDP;#N`~sAeP76%s^#|e^jaX5G z;1lw427)}Dp{Ma09KG6QRK?EQT6k$P_`9@^*qdwqQR@Ct_;FWDacPnI2lLZJdxM?4 zcmoA{SrhQTb`^Vko%cd@P97rmlZYGv#Ahh!d%6I6z1BxI-39jNb-=zs*c>5}9)R{q zSdWNa?<$TNI$8ND2P0{;vr-&Dv@=*R@WblC5Oml1;INVu>QbZgouky%c&UrtA*EKR zzUvO+At_^yJvvuKYCN12tRPjGiHsXANhA^HNH5{_QF z>PX=e+yIF!W}*E?LpZYO#3of0sh=@9h;G*i4tgneiqsZbSfYg-3NtJeNOFqQn;1x< z4`|pmH9C@3@|cT4Ri1@DW}f7TBWX$z6sfP!K64xyF%-vFTbetL1^+qm9LHOo0BVLl z4_#)?BB}e?@l=`AeaGXu5p`P7o_8HevrbFaN$U5B?Bm_HkImlfg>J!LWXbJlIi^ssOI z3iY{eD@|n=&z|GdZQV^oi3GRn)@{R_VK;hWO}ivjVtVho?SHA!U!JDF!ssuqS}@J2 z+d<|HrP(@KdEL$n;ON9AybIUuVxlEp$Ero$t~OZ{CxPmwQ<9xnOWVgXl3SWcJW>v- zyG9lzzGen`REw-6(H;(~+mn^rEiFSmoQ?FwWAuXe8l0ObqsoSZ(E_`L60LI4wI?PF z#;x^gQQ#C0@8BfbkdHAdv8OdeZMCQ9E#~BsPc<@JgpgV$F_$-DgnJlV z!lYCaxlQhapwX$Q)48R@gulH7KhYLl3cN|;-&sq*5g`N%@WQps#L7GiEk z?Yut4ix*YbPy-rIuW)iZ^+&nH8a8!HD;wiaSXmlrsBBz4bBtv$AC-x@sL;8cwVlz4 zPo{$EvP$esEM7Ke1}cM=O>H!{46^Q$W4duhoefnpiWdOgLl@7fs5B=b73hpn zU2vojo}~iy$?ZRnRry0^Z-7?qBwoU#%Tl4lBPmoY!F}zy12r-`F&TYs?jTJi67QgU zwp9JZbLf>URa87{;Zi4eFpasT4HJi$%}O1wB(b$HNj3dH?7atgoW-?1{>>Kcx4WOT zt1jD;ytb;<A--29CACgim$kS!&CSQbs79sNVKT7$<|SM%{;@K|#ZQw7nZ)5kWO1Y11kpty8O*rGimvz8@f6 z^gsM^>eMvs^c0L%YtzxHS)|ITSC3M~eX_Z+z}gt2pl&caD+Oc4uMBjx8d?J!qg(4K-42;QLm0Y4pcM#FdGzCeF#O#d+;m? zp#u9~PJ_A(BjSSN&cZBS8Hv(7OW5F*kbA~g%raoR!XZHZ^We+>(p@R|UI3j(-B~b) zsLSpu>ax%qU9xL*$*<96Mw<43MD4e$KM$K#pukplA`)pHGy=b90*6v3I)${$CYG`~ zc+k)(v|;>!e%k>07S=CykcF+fohFvQ`c=}<7y0q}2FAMxcAIt~3aKiKPl4 zwx*t)XXqFB@pgY`yk~7ZnxX3Fb5r~*^W%N*L*wPM@}oNpIVeO+%7*ub8SQi-dd8WpI^gA3%rN| z^EqKH@FEJlm@rNgUc{QiJ-Bu`jwPW#Ae2MX0!z_pw8)`pfq^|vi=3HW#1sR2+;%xK zy=XL07f7`t7Beq*5RN(y$!w&yp8y=!k777bjDa$WVm*Bi!`B(l5fnd*O^(L=;b=tL zOoHH^SGJGuFyS*z_*gkKb=G(!I;QC)kZ$F0B)F$<$M{(TNJsO*KXndS6*cNq3fa^Y zvcZbFQ^2B-kz3h1s)@Tz3fE)o&CfW*xf?XT?e)S(>vke_`yinh!$j&ffL2)RU=nq3 z455_Zv22(=4(PP9nN@XrO$yt(m=1Ox5jG9u7eA##@X;yNF{RpOn^GN9nnft3SjT8< z38fV4W&wVVP!84A9Sit0V2d28t2+S@rKp7*K_T;6kbEIqaZ!r@HG!R`N;zjE%EWZ# zn>ZdC<%Cx`lNKUmXBVR5b61!Aai)RYUicre3}5Gww~+Oj;NO_P_afxGmh2^xO#pz69lKn=6C%Ta;KB-nb z^MOQ<_z42}o76}A29C?=coB|o)A2eS@6hoc92ISFL=oczIHnZA0gX->Dt}#_#zDi| zZarKK%GYfg4>LZ~c#96E(cBIP)4)Jqr?HC;rm-K652Vp~BkV46IoA`F>f~-n)zXy^a+?T@F zQyuUL>OGR6Uh)s>CI6sa@(=1I|6o9t{6l8RUvIMnxbh#|-2)Z@uKWl0&>40kT>Rfd z#(L#Hz?J_{%jeQ?2#Rk?#x$qw@F|aMlh5vA=fgMpU{D;d7?2K~ZKRnpLnbAUuF~q=m zQ7-(4%MDx@<-&isQau3q165s7F8qg6>LG!vqBSU<@Nko(-Y6IT!?gx(j&k8YT(5d1 zgWIB9_zyR!>404Lk8z75`7_>XeoKRi%M0Kvv z?^aPR{D-Hhiv?e&qFne7A8qhn73IQzc)G#2t0))#!!r!-Q&BGbhi59}7wJ8qqFne7 z&lc&q@E`p?0>X3DF9Qt6!hgiw0%wc`9p@5xxX+a(^6-7sj>jeP@cmj*j>jeP@B>;; zuE!OC&lL`E$^iug?#*W-dsWHcjk!6u2heq_uy1YyA@m;PiUSqdRb z6fc)wq!Hiu;VbFhj_Mj2n+N}VbvN9&*=K9-YfKKG*U)CX4}t;;sAkgHFcSU|_YpV? z)p3kATG8KG$_RL)$o#z?bgR*<6s%b9s8OthZ^NfZ7>!|sR@Abh6}^rcOS0ua#aDsd zV6-eM8cfcHee)A>ufun~`Wlm}RQ-m1aJhs!7a`uCY%�hQB+h<*i^cqfi4&9%JO8 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
<>pb~gv8^Jx4*#rM^_Q95m|2RtS4Jy~ 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 literal 0 HcmV?d00001 diff --git a/public/c/bwa/org_broadinstitute_sting_alignment_bwa_c_BWACAligner.cpp b/public/c/bwa/org_broadinstitute_sting_alignment_bwa_c_BWACAligner.cpp new file mode 100644 index 000000000..1ccbef0d4 --- /dev/null +++ b/public/c/bwa/org_broadinstitute_sting_alignment_bwa_c_BWACAligner.cpp @@ -0,0 +1,437 @@ +#include +#include +#include + +#include "bntseq.h" +#include "bwt.h" +#include "bwtaln.h" +#include "bwa_gateway.h" +#include "org_broadinstitute_sting_alignment_bwa_c_BWACAligner.h" + +typedef void (BWA::*int_setter)(int value); +typedef void (BWA::*float_setter)(float value); + +static jobject convert_to_java_alignment(JNIEnv* env, const jbyte* read_bases, const jsize read_length, const Alignment& alignment); +static jstring get_configuration_file(JNIEnv* env, jobject configuration, const char* field_name); +static void set_int_configuration_param(JNIEnv* env, jobject configuration, const char* field_name, BWA* bwa, int_setter setter); +static void set_float_configuration_param(JNIEnv* env, jobject configuration, const char* field_name, BWA* bwa, float_setter setter); +static void throw_config_value_exception(JNIEnv* env, const char* field_name, const char* message); + +JNIEXPORT jlong JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_create(JNIEnv* env, jobject instance, jobject bwtFiles, jobject configuration) +{ + jstring java_ann = get_configuration_file(env,bwtFiles,"annFile"); + if(java_ann == NULL) return 0L; + jstring java_amb = get_configuration_file(env,bwtFiles,"ambFile"); + if(java_amb == NULL) return 0L; + jstring java_pac = get_configuration_file(env,bwtFiles,"pacFile"); + if(java_pac == NULL) return 0L; + jstring java_forward_bwt = get_configuration_file(env,bwtFiles,"forwardBWTFile"); + if(java_forward_bwt == NULL) return 0L; + jstring java_forward_sa = get_configuration_file(env,bwtFiles,"forwardSAFile"); + if(java_forward_sa == NULL) return 0L; + jstring java_reverse_bwt = get_configuration_file(env,bwtFiles,"reverseBWTFile"); + if(java_reverse_bwt == NULL) return 0L; + jstring java_reverse_sa = get_configuration_file(env,bwtFiles,"reverseSAFile"); + if(java_reverse_sa == NULL) return 0L; + + const char* ann_filename = env->GetStringUTFChars(java_ann,JNI_FALSE); + if(env->ExceptionCheck()) return 0L; + const char* amb_filename = env->GetStringUTFChars(java_amb,JNI_FALSE); + if(env->ExceptionCheck()) return 0L; + const char* pac_filename = env->GetStringUTFChars(java_pac,JNI_FALSE); + if(env->ExceptionCheck()) return 0L; + const char* forward_bwt_filename = env->GetStringUTFChars(java_forward_bwt,JNI_FALSE); + if(env->ExceptionCheck()) return 0L; + const char* forward_sa_filename = env->GetStringUTFChars(java_forward_sa,JNI_FALSE); + if(env->ExceptionCheck()) return 0L; + const char* reverse_bwt_filename = env->GetStringUTFChars(java_reverse_bwt,JNI_FALSE); + if(env->ExceptionCheck()) return 0L; + const char* reverse_sa_filename = env->GetStringUTFChars(java_reverse_sa,JNI_FALSE); + if(env->ExceptionCheck()) return 0L; + + BWA* bwa = new BWA(ann_filename, + amb_filename, + pac_filename, + forward_bwt_filename, + forward_sa_filename, + reverse_bwt_filename, + reverse_sa_filename); + + Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_updateConfiguration(env,instance,(jlong)bwa,configuration); + if(env->ExceptionCheck()) return 0L; + + env->ReleaseStringUTFChars(java_ann,ann_filename); + if(env->ExceptionCheck()) return 0L; + env->ReleaseStringUTFChars(java_amb,amb_filename); + if(env->ExceptionCheck()) return 0L; + env->ReleaseStringUTFChars(java_pac,pac_filename); + if(env->ExceptionCheck()) return 0L; + env->ReleaseStringUTFChars(java_forward_bwt,forward_bwt_filename); + if(env->ExceptionCheck()) return 0L; + env->ReleaseStringUTFChars(java_forward_sa,forward_sa_filename); + if(env->ExceptionCheck()) return 0L; + env->ReleaseStringUTFChars(java_reverse_bwt,reverse_bwt_filename); + if(env->ExceptionCheck()) return 0L; + env->ReleaseStringUTFChars(java_reverse_sa,reverse_sa_filename); + if(env->ExceptionCheck()) return 0L; + + return (jlong)bwa; +} + +JNIEXPORT void JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_destroy(JNIEnv* env, jobject instance, jlong java_bwa) +{ + BWA* bwa = (BWA*)java_bwa; + delete bwa; +} + +JNIEXPORT void JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_updateConfiguration(JNIEnv *env, jobject instance, jlong java_bwa, jobject configuration) { + BWA* bwa = (BWA*)java_bwa; + set_float_configuration_param(env, configuration, "maximumEditDistance", bwa, &BWA::set_max_edit_distance); + if(env->ExceptionCheck()) return; + set_int_configuration_param(env, configuration, "maximumGapOpens", bwa, &BWA::set_max_gap_opens); + if(env->ExceptionCheck()) return; + set_int_configuration_param(env, configuration, "maximumGapExtensions", bwa, &BWA::set_max_gap_extensions); + if(env->ExceptionCheck()) return; + set_int_configuration_param(env, configuration, "disallowIndelWithinRange", bwa, &BWA::set_disallow_indel_within_range); + if(env->ExceptionCheck()) return; + set_int_configuration_param(env, configuration, "mismatchPenalty", bwa, &BWA::set_mismatch_penalty); + if(env->ExceptionCheck()) return; + set_int_configuration_param(env, configuration, "gapOpenPenalty", bwa, &BWA::set_gap_open_penalty); + if(env->ExceptionCheck()) return; + set_int_configuration_param(env, configuration, "gapExtensionPenalty", bwa, &BWA::set_gap_extension_penalty); + if(env->ExceptionCheck()) return; +} + +JNIEXPORT jobjectArray JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_getPaths(JNIEnv *env, jobject instance, jlong java_bwa, jbyteArray java_bases) +{ + BWA* bwa = (BWA*)java_bwa; + + const jsize read_length = env->GetArrayLength(java_bases); + if(env->ExceptionCheck()) return NULL; + + jbyte *read_bases = env->GetByteArrayElements(java_bases,JNI_FALSE); + if(read_bases == NULL) return NULL; + + bwt_aln1_t* paths = NULL; + unsigned num_paths = 0; + + unsigned best_path_count, second_best_path_count; + bwa->find_paths((const char*)read_bases,read_length,paths,num_paths,best_path_count,second_best_path_count); + + jobjectArray java_paths = env->NewObjectArray(num_paths, env->FindClass("org/broadinstitute/sting/alignment/bwa/c/BWAPath"), NULL); + if(java_paths == NULL) return NULL; + + for(unsigned path_idx = 0; path_idx < (unsigned)num_paths; path_idx++) { + bwt_aln1_t& path = *(paths + path_idx); + + jclass java_path_class = env->FindClass("org/broadinstitute/sting/alignment/bwa/c/BWAPath"); + if(java_path_class == NULL) return NULL; + + jmethodID java_path_constructor = env->GetMethodID(java_path_class, "", "(IIIZJJIII)V"); + if(java_path_constructor == NULL) return NULL; + + // Note that k/l are being cast to long. Bad things will happen if JNI assumes that they're ints. + jobject java_path = env->NewObject(java_path_class, + java_path_constructor, + path.n_mm, + path.n_gapo, + path.n_gape, + path.a, + (jlong)path.k, + (jlong)path.l, + path.score, + best_path_count, + second_best_path_count); + if(java_path == NULL) return NULL; + + env->SetObjectArrayElement(java_paths,path_idx,java_path); + if(env->ExceptionCheck()) return NULL; + + env->DeleteLocalRef(java_path_class); + if(env->ExceptionCheck()) return NULL; + } + + delete[] paths; + + env->ReleaseByteArrayElements(java_bases,read_bases,JNI_FALSE); + + return env->ExceptionCheck() ? NULL : java_paths; +} + +JNIEXPORT jobjectArray JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_convertPathsToAlignments(JNIEnv *env, jobject instance, jlong java_bwa, jbyteArray java_bases, jobjectArray java_paths) +{ + BWA* bwa = (BWA*)java_bwa; + + const jsize read_length = env->GetArrayLength(java_bases); + if(env->ExceptionCheck()) return NULL; + + jbyte *read_bases = env->GetByteArrayElements(java_bases,JNI_FALSE); + if(read_bases == NULL) return NULL; + + const jsize num_paths = env->GetArrayLength(java_paths); + bwt_aln1_t* paths = new bwt_aln1_t[num_paths]; + unsigned best_count = 0, second_best_count = 0; + + for(unsigned path_idx = 0; path_idx < (unsigned)num_paths; path_idx++) { + jobject java_path = env->GetObjectArrayElement(java_paths,path_idx); + jclass java_path_class = env->GetObjectClass(java_path); + if(java_path_class == NULL) return NULL; + + bwt_aln1_t& path = *(paths + path_idx); + + jfieldID mismatches_field = env->GetFieldID(java_path_class, "numMismatches", "I"); + if(mismatches_field == NULL) return NULL; + path.n_mm = env->GetIntField(java_path,mismatches_field); + if(env->ExceptionCheck()) return NULL; + + jfieldID gap_opens_field = env->GetFieldID(java_path_class, "numGapOpens", "I"); + if(gap_opens_field == NULL) return NULL; + path.n_gapo = env->GetIntField(java_path,gap_opens_field); + if(env->ExceptionCheck()) return NULL; + + jfieldID gap_extensions_field = env->GetFieldID(java_path_class, "numGapExtensions", "I"); + if(gap_extensions_field == NULL) return NULL; + path.n_gape = env->GetIntField(java_path,gap_extensions_field); + if(env->ExceptionCheck()) return NULL; + + jfieldID negative_strand_field = env->GetFieldID(java_path_class, "negativeStrand", "Z"); + if(negative_strand_field == NULL) return NULL; + path.a = env->GetBooleanField(java_path,negative_strand_field); + if(env->ExceptionCheck()) return NULL; + + jfieldID k_field = env->GetFieldID(java_path_class, "k", "J"); + if(k_field == NULL) return NULL; + path.k = env->GetLongField(java_path,k_field); + if(env->ExceptionCheck()) return NULL; + + jfieldID l_field = env->GetFieldID(java_path_class, "l", "J"); + if(l_field == NULL) return NULL; + path.l = env->GetLongField(java_path,l_field); + if(env->ExceptionCheck()) return NULL; + + jfieldID score_field = env->GetFieldID(java_path_class, "score", "I"); + if(score_field == NULL) return NULL; + path.score = env->GetIntField(java_path,score_field); + if(env->ExceptionCheck()) return NULL; + + jfieldID best_count_field = env->GetFieldID(java_path_class, "bestCount", "I"); + if(best_count_field == NULL) return NULL; + best_count = env->GetIntField(java_path,best_count_field); + if(env->ExceptionCheck()) return NULL; + + jfieldID second_best_count_field = env->GetFieldID(java_path_class, "secondBestCount", "I"); + if(second_best_count_field == NULL) return NULL; + second_best_count = env->GetIntField(java_path,second_best_count_field); + if(env->ExceptionCheck()) return NULL; + } + + Alignment* alignments = NULL; + unsigned num_alignments = 0; + bwa->generate_alignments_from_paths((const char*)read_bases,read_length,paths,num_paths,best_count,second_best_count,alignments,num_alignments); + + jobjectArray java_alignments = env->NewObjectArray(num_alignments, env->FindClass("org/broadinstitute/sting/alignment/Alignment"), NULL); + if(java_alignments == NULL) return NULL; + + for(unsigned alignment_idx = 0; alignment_idx < (unsigned)num_alignments; alignment_idx++) { + Alignment& alignment = *(alignments + alignment_idx); + jobject java_alignment = convert_to_java_alignment(env,read_bases,read_length,alignment); + if(java_alignment == NULL) return NULL; + env->SetObjectArrayElement(java_alignments,alignment_idx,java_alignment); + if(env->ExceptionCheck()) return NULL; + } + + delete[] alignments; + delete[] paths; + + env->ReleaseByteArrayElements(java_bases,read_bases,JNI_FALSE); + + return env->ExceptionCheck() ? NULL : java_alignments; +} + +JNIEXPORT jobject JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_getBestAlignment(JNIEnv *env, jobject instance, jlong java_bwa, jbyteArray java_bases) { + BWA* bwa = (BWA*)java_bwa; + + const jsize read_length = env->GetArrayLength(java_bases); + if(env->ExceptionCheck()) return NULL; + + jbyte *read_bases = env->GetByteArrayElements(java_bases,JNI_FALSE); + if(read_bases == NULL) return NULL; + + Alignment* best_alignment = bwa->generate_single_alignment((const char*)read_bases,read_length); + jobject java_best_alignment = (best_alignment != NULL) ? convert_to_java_alignment(env,read_bases,read_length,*best_alignment) : NULL; + delete best_alignment; + + env->ReleaseByteArrayElements(java_bases,read_bases,JNI_FALSE); + + return java_best_alignment; +} + +static jobject convert_to_java_alignment(JNIEnv *env, const jbyte* read_bases, const jsize read_length, const Alignment& alignment) { + unsigned cigar_length; + if(alignment.type == BWA_TYPE_NO_MATCH) cigar_length = 0; + else if(!alignment.cigar) cigar_length = 1; + else cigar_length = alignment.n_cigar; + + jcharArray java_cigar_operators = env->NewCharArray(cigar_length); + if(java_cigar_operators == NULL) return NULL; + jintArray java_cigar_lengths = env->NewIntArray(cigar_length); + if(java_cigar_lengths == NULL) return NULL; + + if(alignment.cigar) { + for(unsigned cigar_idx = 0; cigar_idx < (unsigned)alignment.n_cigar; ++cigar_idx) { + jchar cigar_operator = "MIDS"[alignment.cigar[cigar_idx]>>14]; + jint cigar_length = alignment.cigar[cigar_idx]&0x3fff; + + env->SetCharArrayRegion(java_cigar_operators,cigar_idx,1,&cigar_operator); + if(env->ExceptionCheck()) return NULL; + env->SetIntArrayRegion(java_cigar_lengths,cigar_idx,1,&cigar_length); + if(env->ExceptionCheck()) return NULL; + } + } + else { + if(alignment.type != BWA_TYPE_NO_MATCH) { + jchar cigar_operator = 'M'; + env->SetCharArrayRegion(java_cigar_operators,0,1,&cigar_operator); + if(env->ExceptionCheck()) return NULL; + env->SetIntArrayRegion(java_cigar_lengths,0,1,&read_length); + if(env->ExceptionCheck()) return NULL; + } + } + delete[] alignment.cigar; + + jclass java_alignment_class = env->FindClass("org/broadinstitute/sting/alignment/Alignment"); + if(java_alignment_class == NULL) return NULL; + + jmethodID java_alignment_constructor = env->GetMethodID(java_alignment_class, "", "(IIZI[C[IILjava/lang/String;IIIII)V"); + if(java_alignment_constructor == NULL) return NULL; + + jstring java_md = env->NewStringUTF(alignment.md); + if(java_md == NULL) return NULL; + delete[] alignment.md; + + jobject java_alignment = env->NewObject(java_alignment_class, + java_alignment_constructor, + alignment.contig, + alignment.pos, + alignment.negative_strand, + alignment.mapping_quality, + java_cigar_operators, + java_cigar_lengths, + alignment.edit_distance, + java_md, + alignment.num_mismatches, + alignment.num_gap_opens, + alignment.num_gap_extensions, + alignment.num_best, + alignment.num_second_best); + if(java_alignment == NULL) return NULL; + + env->DeleteLocalRef(java_alignment_class); + if(env->ExceptionCheck()) return NULL; + + return java_alignment; +} + +static jstring get_configuration_file(JNIEnv* env, jobject configuration, const char* field_name) { + jclass configuration_class = env->GetObjectClass(configuration); + if(configuration_class == NULL) return NULL; + + jfieldID configuration_field = env->GetFieldID(configuration_class, field_name, "Ljava/io/File;"); + if(configuration_field == NULL) return NULL; + + jobject configuration_file = (jobject)env->GetObjectField(configuration,configuration_field); + + jclass file_class = env->FindClass("java/io/File"); + if(file_class == NULL) return NULL; + + jmethodID path_extractor = env->GetMethodID(file_class,"getAbsolutePath", "()Ljava/lang/String;"); + if(path_extractor == NULL) return NULL; + + jstring path = (jstring)env->CallObjectMethod(configuration_file,path_extractor); + if(path == NULL) return NULL; + + env->DeleteLocalRef(configuration_class); + env->DeleteLocalRef(file_class); + env->DeleteLocalRef(configuration_file); + + return path; +} + +static void set_int_configuration_param(JNIEnv* env, jobject configuration, const char* field_name, BWA* bwa, int_setter setter) { + jclass configuration_class = env->GetObjectClass(configuration); + if(configuration_class == NULL) return; + + jfieldID configuration_field = env->GetFieldID(configuration_class, field_name, "Ljava/lang/Integer;"); + if(configuration_field == NULL) return; + + jobject boxed_value = env->GetObjectField(configuration,configuration_field); + if(env->ExceptionCheck()) return; + + if(boxed_value != NULL) { + jclass int_box_class = env->FindClass("java/lang/Integer"); + if(int_box_class == NULL) return; + + jmethodID int_extractor = env->GetMethodID(int_box_class,"intValue", "()I"); + if(int_extractor == NULL) return; + + jint value = env->CallIntMethod(boxed_value,int_extractor); + if(env->ExceptionCheck()) return; + + if(value < 0) + { + throw_config_value_exception(env,field_name,"cannot be set to a negative value"); + return; + } + + (bwa->*setter)(value); + + env->DeleteLocalRef(int_box_class); + } + + env->DeleteLocalRef(boxed_value); + env->DeleteLocalRef(configuration_class); +} + +static void set_float_configuration_param(JNIEnv* env, jobject configuration, const char* field_name, BWA* bwa, float_setter setter) +{ + jclass configuration_class = env->GetObjectClass(configuration); + if(configuration_class == NULL) return; + + jfieldID configuration_field = env->GetFieldID(configuration_class, field_name, "Ljava/lang/Float;"); + if(configuration_field == NULL) return; + + jobject boxed_value = env->GetObjectField(configuration,configuration_field); + if(boxed_value != NULL) { + jclass float_box_class = env->FindClass("java/lang/Float"); + if(float_box_class == NULL) return; + + jmethodID float_extractor = env->GetMethodID(float_box_class,"floatValue", "()F"); + if(float_extractor == NULL) return; + + jfloat value = env->CallFloatMethod(boxed_value,float_extractor); + if(env->ExceptionCheck()) return; + + if(value < 0) + { + throw_config_value_exception(env,field_name,"cannot be set to a negative value"); + return; + } + + (bwa->*setter)(value); + + env->DeleteLocalRef(float_box_class); + } + + env->DeleteLocalRef(boxed_value); + env->DeleteLocalRef(configuration_class); +} + +static void throw_config_value_exception(JNIEnv* env, const char* field_name, const char* message) +{ + char* buffer = new char[strlen(field_name)+1+strlen(message)+1]; + sprintf(buffer,"%s %s",field_name,message); + jclass sting_exception_class = env->FindClass("org/broadinstitute/sting/utils/StingException"); + if(sting_exception_class == NULL) return; + env->ThrowNew(sting_exception_class, buffer); + delete[] buffer; +} diff --git a/public/c/bwa/org_broadinstitute_sting_alignment_bwa_c_BWACAligner.h b/public/c/bwa/org_broadinstitute_sting_alignment_bwa_c_BWACAligner.h new file mode 100644 index 000000000..0c44e430a --- /dev/null +++ b/public/c/bwa/org_broadinstitute_sting_alignment_bwa_c_BWACAligner.h @@ -0,0 +1,61 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class org_broadinstitute_sting_alignment_bwa_c_BWACAligner */ + +#ifndef _Included_org_broadinstitute_sting_alignment_bwa_c_BWACAligner +#define _Included_org_broadinstitute_sting_alignment_bwa_c_BWACAligner +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: org_broadinstitute_sting_alignment_bwa_c_BWACAligner + * Method: create + * Signature: (Lorg/broadinstitute/sting/alignment/bwa/BWTFiles;Lorg/broadinstitute/sting/alignment/bwa/BWAConfiguration;)J + */ +JNIEXPORT jlong JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_create + (JNIEnv *, jobject, jobject, jobject); + +/* + * Class: org_broadinstitute_sting_alignment_bwa_c_BWACAligner + * Method: updateConfiguration + * Signature: (JLorg/broadinstitute/sting/alignment/bwa/BWAConfiguration;)V + */ +JNIEXPORT void JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_updateConfiguration + (JNIEnv *, jobject, jlong, jobject); + +/* + * Class: org_broadinstitute_sting_alignment_bwa_c_BWACAligner + * Method: destroy + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_destroy + (JNIEnv *, jobject, jlong); + +/* + * Class: org_broadinstitute_sting_alignment_bwa_c_BWACAligner + * Method: getPaths + * Signature: (J[B)[Lorg/broadinstitute/sting/alignment/bwa/c/BWAPath; + */ +JNIEXPORT jobjectArray JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_getPaths + (JNIEnv *, jobject, jlong, jbyteArray); + +/* + * Class: org_broadinstitute_sting_alignment_bwa_c_BWACAligner + * Method: convertPathsToAlignments + * Signature: (J[B[Lorg/broadinstitute/sting/alignment/bwa/c/BWAPath;)[Lorg/broadinstitute/sting/alignment/Alignment; + */ +JNIEXPORT jobjectArray JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_convertPathsToAlignments + (JNIEnv *, jobject, jlong, jbyteArray, jobjectArray); + +/* + * Class: org_broadinstitute_sting_alignment_bwa_c_BWACAligner + * Method: getBestAlignment + * Signature: (J[B)Lorg/broadinstitute/sting/alignment/Alignment; + */ +JNIEXPORT jobject JNICALL Java_org_broadinstitute_sting_alignment_bwa_c_BWACAligner_getBestAlignment + (JNIEnv *, jobject, jlong, jbyteArray); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/public/c/libenvironhack/Makefile b/public/c/libenvironhack/Makefile new file mode 100644 index 000000000..302ff8e31 --- /dev/null +++ b/public/c/libenvironhack/Makefile @@ -0,0 +1,10 @@ +CC=gcc +CCFLAGS=-Wall -dynamiclib -arch i386 -arch x86_64 + +libenvironhack.dylib: libenvironhack.c + $(CC) $(CCFLAGS) -init _init_environ $< -o $@ + +all: libenvironhack.dylib + +clean: + rm -f libenvironhack.dylib diff --git a/public/c/libenvironhack/libenvironhack.c b/public/c/libenvironhack/libenvironhack.c new file mode 100644 index 000000000..8b2a2640e --- /dev/null +++ b/public/c/libenvironhack/libenvironhack.c @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010, The Broad Institute + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/* +LSF 7.0.6 on the mac is missing the unsatisfied exported symbol for environ which was removed on MacOS X 10.5+. +nm $LSF_LIBDIR/liblsf.dylib | grep environ +See "man environ" for more info, along with http://lists.apple.com/archives/java-dev/2007/Dec/msg00096.html +*/ + +#include + +char **environ = (char **)0; + +void init_environ(void) { + environ = (*_NSGetEnviron()); +} diff --git a/public/c/libenvironhack/libenvironhack.dylib b/public/c/libenvironhack/libenvironhack.dylib new file mode 100755 index 0000000000000000000000000000000000000000..a45e038b49f238f1ce5138fed4d11dbebccb03b1 GIT binary patch literal 28904 zcmeI4O-!6c7{_OEp=)i+8ZkkwWiJ@x2T0OHZbAbVv{nK%HX%CQg)O^w7YHm0wTY`l z10mI3y{bnKJsHyw)1+M5gL?B~43{RD)WjGM+QhE^|2yyO7YM1nDE^;h_L*lso_F5g z>?S*(H||{d+8EP-un*xrW8zR;_*MdUd!$FM$lUKh$hL1ItsaeRDoPV++Yt zF752C^~sv`*!Wxema|rkMU9!jlL;q91pX$B;>%Fa28F|NHu%`JflazNUAp`N)N1iKK1&-eZMFD(V4YgIIkI#s8YGTw#xV$5jbB`P z6nQK0rS<68^=pVaiEn%yL-3}w2#A0Ph=2%)fCz|y2#A0Ph=2%)z}_M78)fgjj^0EB zL_h>YKmYKm53ko34J=_#u z&RMme$ZJ6T2Kb94u){r{;b#48%4X8ZiS(@D&V4&qm`~YMc4{I!HHnPe3^LrNetUL) zYTV|gr_asLxUx2%9MAaLHr(fKY<4PJs95&o$nkWc-)(|7b8wqWC-T!1^KO%VZ4+gQ zfCz|y2#A0Ph=2%)fCz|y2#CPHj=;C>{C|XFZ;sJ<_PU5cHqZZ49zj^fcz)YE09e2k zW6Vit^E@}l`FwWKbw}JQL-O%=R`IvgKHkb5WF;n0)OKtmj z=7bV+%FwRn#`fX0%Cg)24Xt(lXz$cMawFp{cOfLgM~41PXa`4SkX7HVyS*EMXKeV~ zkHUUN^dE`8JE8lNmW}22Z@+Qr^FOW}Yk2W{GUWXQtqJ$LEkb8tSKpoB`<-R^Tdd{` zvqlyYwf#aKEMLAm0X9+b``?XlH+LtX_?CLQKY>&vA|L`HAOa#F0wN#+{|5rSBPT9C zzVu6})H_mYe+O^e+-6Gc=MgzIuyS*gf3XKv&A>|I+lXDiR%)MwhM?B#P(Q7eN+qt# zQzGYwO=KY+i67V(yM#aN&miqE!j~vRd61%0GQ~{ z0r+)#e->alT;J?LnPx1S`t{jC+HmaGKVO6G&jKJCtVeQ9GJ%!Zd)dQZ*`vSHj(ubP E0>D0Q!T getAllAlignments(final byte[] bases); + + /** + * Get a iterator of aligned reads, batched by mapping quality. + * @param read Read to align. + * @param newHeader Optional new header to use when aligning the read. If present, it must be null. + * @return Iterator to alignments. + */ + public Iterable alignAll(final SAMRecord read, final SAMFileHeader newHeader); +} + + diff --git a/public/java/src/org/broadinstitute/sting/alignment/Alignment.java b/public/java/src/org/broadinstitute/sting/alignment/Alignment.java new file mode 100644 index 000000000..ebbc8c1b8 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/Alignment.java @@ -0,0 +1,221 @@ +package org.broadinstitute.sting.alignment; + +import net.sf.samtools.*; +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.utils.BaseUtils; +import org.broadinstitute.sting.utils.Utils; + +/** + * Represents an alignment of a read to a site in the reference genome. + * + * @author mhanna + * @version 0.1 + */ +public class Alignment { + protected int contigIndex; + protected long alignmentStart; + protected boolean negativeStrand; + protected int mappingQuality; + + protected char[] cigarOperators; + protected int[] cigarLengths; + + protected int editDistance; + protected String mismatchingPositions; + + protected int numMismatches; + protected int numGapOpens; + protected int numGapExtensions; + protected int bestCount; + protected int secondBestCount; + + /** + * Gets the index of the given contig. + * @return the inde + */ + public int getContigIndex() { return contigIndex; } + + /** + * Gets the starting position for the given alignment. + * @return Starting position. + */ + public long getAlignmentStart() { return alignmentStart; } + + /** + * Is the given alignment on the reverse strand? + * @return True if the alignment is on the reverse strand. + */ + public boolean isNegativeStrand() { return negativeStrand; } + + /** + * Gets the score of this alignment. + * @return The score. + */ + public int getMappingQuality() { return mappingQuality; } + + /** + * Gets the edit distance; will eventually end up in the NM SAM tag + * if this alignment makes it that far. + * @return The edit distance. + */ + public int getEditDistance() { return editDistance; } + + /** + * A string representation of which positions mismatch; contents of MD tag. + * @return String representation of mismatching positions. + */ + public String getMismatchingPositions() { return mismatchingPositions; } + + /** + * Gets the number of mismatches in the read. + * @return Number of mismatches. + */ + public int getNumMismatches() { return numMismatches; } + + /** + * Get the number of gap opens. + * @return Number of gap opens. + */ + public int getNumGapOpens() { return numGapOpens; } + + /** + * Get the number of gap extensions. + * @return Number of gap extensions. + */ + public int getNumGapExtensions() { return numGapExtensions; } + + /** + * Get the number of best alignments. + * @return Number of top scoring alignments. + */ + public int getBestCount() { return bestCount; } + + /** + * Get the number of second best alignments. + * @return Number of second best scoring alignments. + */ + public int getSecondBestCount() { return secondBestCount; } + + /** + * Gets the cigar for this alignment. + * @return sam-jdk formatted alignment. + */ + public Cigar getCigar() { + Cigar cigar = new Cigar(); + for(int i = 0; i < cigarOperators.length; i++) { + CigarOperator operator = CigarOperator.characterToEnum(cigarOperators[i]); + cigar.add(new CigarElement(cigarLengths[i],operator)); + } + return cigar; + } + + /** + * Temporarily implement getCigarString() for debugging; the TextCigarCodec is unfortunately + * package-protected. + * @return + */ + public String getCigarString() { + Cigar cigar = getCigar(); + if(cigar.isEmpty()) return "*"; + + StringBuilder cigarString = new StringBuilder(); + for(CigarElement element: cigar.getCigarElements()) { + cigarString.append(element.getLength()); + cigarString.append(element.getOperator()); + } + return cigarString.toString(); + } + + /** + * Stub for inheritance. + */ + public Alignment() {} + + /** + * Create a new alignment object. + * @param contigIndex The contig to which this read aligned. + * @param alignmentStart The point within the contig to which this read aligned. + * @param negativeStrand Forward or reverse alignment of the given read. + * @param mappingQuality How good does BWA think this mapping is? + * @param cigarOperators The ordered operators in the cigar string. + * @param cigarLengths The lengths to which each operator applies. + * @param editDistance The edit distance (cumulative) of the read. + * @param mismatchingPositions String representation of which bases in the read mismatch. + * @param numMismatches Number of total mismatches in the read. + * @param numGapOpens Number of gap opens in the read. + * @param numGapExtensions Number of gap extensions in the read. + * @param bestCount Number of best alignments in the read. + * @param secondBestCount Number of second best alignments in the read. + */ + public Alignment(int contigIndex, + int alignmentStart, + boolean negativeStrand, + int mappingQuality, + char[] cigarOperators, + int[] cigarLengths, + int editDistance, + String mismatchingPositions, + int numMismatches, + int numGapOpens, + int numGapExtensions, + int bestCount, + int secondBestCount) { + this.contigIndex = contigIndex; + this.alignmentStart = alignmentStart; + this.negativeStrand = negativeStrand; + this.mappingQuality = mappingQuality; + this.cigarOperators = cigarOperators; + this.cigarLengths = cigarLengths; + this.editDistance = editDistance; + this.mismatchingPositions = mismatchingPositions; + this.numMismatches = numMismatches; + this.numGapOpens = numGapOpens; + this.numGapExtensions = numGapExtensions; + this.bestCount = bestCount; + this.secondBestCount = secondBestCount; + } + + /** + * Creates a read directly from an alignment. + * @param alignment The alignment to convert to a read. + * @param unmappedRead Source of the unmapped read. Should have bases, quality scores, and flags. + * @param newSAMHeader The new SAM header to use in creating this read. Can be null, but if so, the sequence + * dictionary in the + * @return A mapped alignment. + */ + public static SAMRecord convertToRead(Alignment alignment, SAMRecord unmappedRead, SAMFileHeader newSAMHeader) { + SAMRecord read; + try { + read = (SAMRecord)unmappedRead.clone(); + } + catch(CloneNotSupportedException ex) { + throw new ReviewedStingException("Unable to create aligned read from template."); + } + + if(newSAMHeader != null) + read.setHeader(newSAMHeader); + + // If we're realigning a previously aligned record, strip out the placement of the alignment. + read.setReferenceName(SAMRecord.NO_ALIGNMENT_REFERENCE_NAME); + read.setAlignmentStart(SAMRecord.NO_ALIGNMENT_START); + read.setMateReferenceName(SAMRecord.NO_ALIGNMENT_REFERENCE_NAME); + read.setMateAlignmentStart(SAMRecord.NO_ALIGNMENT_START); + + if(alignment != null) { + read.setReadUnmappedFlag(false); + read.setReferenceIndex(alignment.getContigIndex()); + read.setAlignmentStart((int)alignment.getAlignmentStart()); + read.setReadNegativeStrandFlag(alignment.isNegativeStrand()); + read.setMappingQuality(alignment.getMappingQuality()); + read.setCigar(alignment.getCigar()); + if(alignment.isNegativeStrand()) { + read.setReadBases(BaseUtils.simpleReverseComplement(read.getReadBases())); + read.setBaseQualities(Utils.reverse(read.getBaseQualities())); + } + read.setAttribute("NM",alignment.getEditDistance()); + read.setAttribute("MD",alignment.getMismatchingPositions()); + } + + return read; + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/AlignmentValidationWalker.java b/public/java/src/org/broadinstitute/sting/alignment/AlignmentValidationWalker.java new file mode 100644 index 000000000..16e713bf6 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/AlignmentValidationWalker.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2010 The Broad Institute + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR + * THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.broadinstitute.sting.alignment; + +import org.broadinstitute.sting.gatk.refdata.ReadMetaDataTracker; +import org.broadinstitute.sting.gatk.walkers.ReadWalker; +import org.broadinstitute.sting.gatk.contexts.ReferenceContext; +import org.broadinstitute.sting.utils.BaseUtils; +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.commandline.Argument; +import org.broadinstitute.sting.alignment.bwa.c.BWACAligner; +import org.broadinstitute.sting.alignment.bwa.BWAConfiguration; +import org.broadinstitute.sting.alignment.bwa.BWTFiles; +import net.sf.samtools.SAMRecord; + +import java.util.Iterator; + +/** + * Validates consistency of the aligner interface by taking reads already aligned by BWA in a BAM file, stripping them + * of their alignment data, realigning them, and making sure one of the best resulting realignments matches the original + * alignment from the input file. + * + * @author mhanna + * @version 0.1 + */ +public class AlignmentValidationWalker extends ReadWalker { + /** + * The supporting BWT index generated using BWT. + */ + @Argument(fullName="BWTPrefix",shortName="BWT",doc="Index files generated by bwa index -d bwtsw",required=false) + private String prefix = null; + + /** + * The instance used to generate alignments. + */ + private BWACAligner aligner = null; + + /** + * Create an aligner object. The aligner object will load and hold the BWT until close() is called. + */ + @Override + public void initialize() { + if(prefix == null) + prefix = getToolkit().getArguments().referenceFile.getAbsolutePath(); + BWTFiles bwtFiles = new BWTFiles(prefix); + BWAConfiguration configuration = new BWAConfiguration(); + aligner = new BWACAligner(bwtFiles,configuration); + } + + /** + * Aligns a read to the given reference. + * @param ref Reference over the read. Read will most likely be unmapped, so ref will be null. + * @param read Read to align. + * @return Number of reads aligned by this map (aka 1). + */ + @Override + public Integer map(ReferenceContext ref, SAMRecord read, ReadMetaDataTracker metaDataTracker) { + //logger.info(String.format("examining read %s", read.getReadName())); + + byte[] bases = read.getReadBases(); + if(read.getReadNegativeStrandFlag()) bases = BaseUtils.simpleReverseComplement(bases); + + boolean matches = true; + Iterable alignments = aligner.getAllAlignments(bases); + Iterator alignmentIterator = alignments.iterator(); + + if(!alignmentIterator.hasNext()) { + matches = read.getReadUnmappedFlag(); + } + else { + Alignment[] alignmentsOfBestQuality = alignmentIterator.next(); + for(Alignment alignment: alignmentsOfBestQuality) { + matches = (alignment.getContigIndex() == read.getReferenceIndex()); + matches &= (alignment.getAlignmentStart() == read.getAlignmentStart()); + matches &= (alignment.isNegativeStrand() == read.getReadNegativeStrandFlag()); + matches &= (alignment.getCigar().equals(read.getCigar())); + matches &= (alignment.getMappingQuality() == read.getMappingQuality()); + if(matches) break; + } + } + + if(!matches) { + logger.error("Found mismatch!"); + logger.error(String.format("Read %s:",read.getReadName())); + logger.error(String.format(" Contig index: %d",read.getReferenceIndex())); + logger.error(String.format(" Alignment start: %d", read.getAlignmentStart())); + logger.error(String.format(" Negative strand: %b", read.getReadNegativeStrandFlag())); + logger.error(String.format(" Cigar: %s%n", read.getCigarString())); + logger.error(String.format(" Mapping quality: %s%n", read.getMappingQuality())); + for(Alignment[] alignmentsByScore: alignments) { + for(int i = 0; i < alignmentsByScore.length; i++) { + logger.error(String.format("Alignment %d:",i)); + logger.error(String.format(" Contig index: %d",alignmentsByScore[i].getContigIndex())); + logger.error(String.format(" Alignment start: %d", alignmentsByScore[i].getAlignmentStart())); + logger.error(String.format(" Negative strand: %b", alignmentsByScore[i].isNegativeStrand())); + logger.error(String.format(" Cigar: %s", alignmentsByScore[i].getCigarString())); + logger.error(String.format(" Mapping quality: %s%n", alignmentsByScore[i].getMappingQuality())); + } + } + throw new ReviewedStingException(String.format("Read %s mismatches!", read.getReadName())); + } + + return 1; + } + + /** + * Initial value for reduce. In this case, validated reads will be counted. + * @return 0, indicating no reads yet validated. + */ + @Override + public Integer reduceInit() { return 0; } + + /** + * Calculates the number of reads processed. + * @param value Number of reads processed by this map. + * @param sum Number of reads processed before this map. + * @return Number of reads processed up to and including this map. + */ + @Override + public Integer reduce(Integer value, Integer sum) { + return value + sum; + } + + /** + * Cleanup. + * @param result Number of reads processed. + */ + @Override + public void onTraversalDone(Integer result) { + aligner.close(); + super.onTraversalDone(result); + } + +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/AlignmentWalker.java b/public/java/src/org/broadinstitute/sting/alignment/AlignmentWalker.java new file mode 100644 index 000000000..e97d7a56f --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/AlignmentWalker.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2010 The Broad Institute + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR + * THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.broadinstitute.sting.alignment; + +import org.broadinstitute.sting.gatk.io.StingSAMFileWriter; +import org.broadinstitute.sting.gatk.refdata.ReadMetaDataTracker; +import org.broadinstitute.sting.commandline.Argument; +import org.broadinstitute.sting.commandline.Output; +import org.broadinstitute.sting.gatk.walkers.ReadWalker; +import org.broadinstitute.sting.gatk.walkers.WalkerName; +import org.broadinstitute.sting.gatk.contexts.ReferenceContext; +import org.broadinstitute.sting.alignment.bwa.c.BWACAligner; +import org.broadinstitute.sting.alignment.bwa.BWAConfiguration; +import org.broadinstitute.sting.alignment.bwa.BWTFiles; +import net.sf.samtools.*; +import net.sf.picard.reference.ReferenceSequenceFileFactory; + +import java.io.File; +import java.io.PrintStream; + +/** + * Aligns reads to a given reference using Heng Li's BWA aligner, presenting the resulting alignments in SAM or BAM format. + * Mimics the steps 'bwa aln' followed by 'bwa samse' using the BWA/C implementation. + * + * @author mhanna + * @version 0.1 + */ +@WalkerName("Align") +public class AlignmentWalker extends ReadWalker { + @Argument(fullName="target_reference",shortName="target_ref",doc="The reference to which reads in the source file should be aligned. Alongside this reference should sit index files " + + "generated by bwa index -d bwtsw. If unspecified, will default " + + "to the reference specified via the -R argument.",required=false) + private File targetReferenceFile = null; + + @Output + private StingSAMFileWriter out = null; + + /** + * The actual aligner. + */ + private BWACAligner aligner = null; + + /** + * New header to use, if desired. + */ + private SAMFileHeader header; + + /** + * Create an aligner object. The aligner object will load and hold the BWT until close() is called. + */ + @Override + public void initialize() { + if(targetReferenceFile == null) + targetReferenceFile = getToolkit().getArguments().referenceFile; + BWTFiles bwtFiles = new BWTFiles(targetReferenceFile.getAbsolutePath()); + BWAConfiguration configuration = new BWAConfiguration(); + aligner = new BWACAligner(bwtFiles,configuration); + + // Take the header of the SAM file, tweak it by adding in the reference dictionary and specifying that the target file is unsorted. + header = getToolkit().getSAMFileHeader().clone(); + SAMSequenceDictionary referenceDictionary = + ReferenceSequenceFileFactory.getReferenceSequenceFile(targetReferenceFile).getSequenceDictionary(); + header.setSequenceDictionary(referenceDictionary); + header.setSortOrder(SAMFileHeader.SortOrder.unsorted); + + out.writeHeader(header); + } + + /** + * Aligns a read to the given reference. + * @param ref Reference over the read. Read will most likely be unmapped, so ref will be null. + * @param read Read to align. + * @return Number of alignments found for this read. + */ + @Override + public Integer map(ReferenceContext ref, SAMRecord read, ReadMetaDataTracker metaDataTracker) { + SAMRecord alignedRead = aligner.align(read,header); + out.addAlignment(alignedRead); + return 1; + } + + /** + * Initial value for reduce. In this case, alignments will be counted. + * @return 0, indicating no alignments yet found. + */ + @Override + public Integer reduceInit() { return 0; } + + /** + * Calculates the number of alignments found. + * @param value Number of alignments found by this map. + * @param sum Number of alignments found before this map. + * @return Number of alignments found up to and including this map. + */ + @Override + public Integer reduce(Integer value, Integer sum) { + return value + sum; + } + + /** + * Cleanup. + * @param result Number of reads processed. + */ + @Override + public void onTraversalDone(Integer result) { + aligner.close(); + super.onTraversalDone(result); + } + +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/CountBestAlignmentsWalker.java b/public/java/src/org/broadinstitute/sting/alignment/CountBestAlignmentsWalker.java new file mode 100644 index 000000000..1a1e1197d --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/CountBestAlignmentsWalker.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010 The Broad Institute + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR + * THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.broadinstitute.sting.alignment; + +import org.broadinstitute.sting.gatk.refdata.ReadMetaDataTracker; +import org.broadinstitute.sting.gatk.walkers.ReadWalker; +import org.broadinstitute.sting.gatk.contexts.ReferenceContext; +import org.broadinstitute.sting.alignment.bwa.BWTFiles; +import org.broadinstitute.sting.alignment.bwa.BWAConfiguration; +import org.broadinstitute.sting.alignment.bwa.c.BWACAligner; +import org.broadinstitute.sting.commandline.Argument; +import org.broadinstitute.sting.commandline.Output; +import net.sf.samtools.SAMRecord; + +import java.util.*; +import java.io.PrintStream; + +/** + * Counts the number of best alignments as presented by BWA and outputs a histogram of number of placements vs. the + * frequency of that number of placements. + * + * @author mhanna + * @version 0.1 + */ +public class CountBestAlignmentsWalker extends ReadWalker { + /** + * The supporting BWT index generated using BWT. + */ + @Argument(fullName="BWTPrefix",shortName="BWT",doc="Index files generated by bwa index -d bwtsw",required=false) + private String prefix = null; + + @Output + private PrintStream out = null; + + /** + * The actual aligner. + */ + private Aligner aligner = null; + + private SortedMap alignmentFrequencies = new TreeMap(); + + /** + * Create an aligner object. The aligner object will load and hold the BWT until close() is called. + */ + @Override + public void initialize() { + if(prefix == null) + prefix = getToolkit().getArguments().referenceFile.getAbsolutePath(); + BWTFiles bwtFiles = new BWTFiles(prefix); + BWAConfiguration configuration = new BWAConfiguration(); + aligner = new BWACAligner(bwtFiles,configuration); + } + + /** + * Aligns a read to the given reference. + * @param ref Reference over the read. Read will most likely be unmapped, so ref will be null. + * @param read Read to align. + * @return Number of alignments found for this read. + */ + @Override + public Integer map(ReferenceContext ref, SAMRecord read, ReadMetaDataTracker metaDataTracker) { + Iterator alignmentIterator = aligner.getAllAlignments(read.getReadBases()).iterator(); + if(alignmentIterator.hasNext()) { + int numAlignments = alignmentIterator.next().length; + if(alignmentFrequencies.containsKey(numAlignments)) + alignmentFrequencies.put(numAlignments,alignmentFrequencies.get(numAlignments)+1); + else + alignmentFrequencies.put(numAlignments,1); + } + return 1; + } + + /** + * Initial value for reduce. In this case, validated reads will be counted. + * @return 0, indicating no reads yet validated. + */ + @Override + public Integer reduceInit() { return 0; } + + /** + * Calculates the number of reads processed. + * @param value Number of reads processed by this map. + * @param sum Number of reads processed before this map. + * @return Number of reads processed up to and including this map. + */ + @Override + public Integer reduce(Integer value, Integer sum) { + return value + sum; + } + + /** + * Cleanup. + * @param result Number of reads processed. + */ + @Override + public void onTraversalDone(Integer result) { + aligner.close(); + for(Map.Entry alignmentFrequency: alignmentFrequencies.entrySet()) + out.printf("%d\t%d%n", alignmentFrequency.getKey(), alignmentFrequency.getValue()); + super.onTraversalDone(result); + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/BWAAligner.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/BWAAligner.java new file mode 100644 index 000000000..ddbf784f5 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/BWAAligner.java @@ -0,0 +1,38 @@ +package org.broadinstitute.sting.alignment.bwa; + +import org.broadinstitute.sting.alignment.Aligner; + +/** + * Align reads using BWA. + * + * @author mhanna + * @version 0.1 + */ +public abstract class BWAAligner implements Aligner { + /** + * The supporting files used by BWA. + */ + protected BWTFiles bwtFiles; + + /** + * The current configuration for the BWA aligner. + */ + protected BWAConfiguration configuration; + + /** + * Create a new BWAAligner. Purpose of this call is to ensure that all BWA constructors accept the correct + * parameters. + * @param bwtFiles The many files representing BWTs persisted to disk. + * @param configuration Configuration parameters for the alignment. + */ + public BWAAligner(BWTFiles bwtFiles, BWAConfiguration configuration) { + this.bwtFiles = bwtFiles; + this.configuration = configuration; + } + + /** + * Update the configuration passed to the BWA aligner. + * @param configuration New configuration to set. + */ + public abstract void updateConfiguration(BWAConfiguration configuration); +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/BWAConfiguration.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/BWAConfiguration.java new file mode 100644 index 000000000..73441cb6a --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/BWAConfiguration.java @@ -0,0 +1,44 @@ +package org.broadinstitute.sting.alignment.bwa; + +/** + * Configuration for the BWA/C aligner. + * + * @author mhanna + * @version 0.1 + */ +public class BWAConfiguration { + /** + * The maximum edit distance used by BWA. + */ + public Float maximumEditDistance = null; + + /** + * How many gap opens are acceptable within this alignment? + */ + public Integer maximumGapOpens = null; + + /** + * How many gap extensions are acceptable within this alignment? + */ + public Integer maximumGapExtensions = null; + + /** + * Do we disallow indels within a certain range from the start / end? + */ + public Integer disallowIndelWithinRange = null; + + /** + * What is the scoring penalty for a mismatch? + */ + public Integer mismatchPenalty = null; + + /** + * What is the scoring penalty for a gap open? + */ + public Integer gapOpenPenalty = null; + + /** + * What is the scoring penalty for a gap extension? + */ + public Integer gapExtensionPenalty = null; +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/BWTFiles.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/BWTFiles.java new file mode 100644 index 000000000..cd7800900 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/BWTFiles.java @@ -0,0 +1,240 @@ +package org.broadinstitute.sting.alignment.bwa; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.utils.Utils; +import org.broadinstitute.sting.alignment.reference.packing.PackUtils; +import org.broadinstitute.sting.alignment.reference.bwt.BWT; +import org.broadinstitute.sting.alignment.reference.bwt.BWTWriter; +import org.broadinstitute.sting.alignment.reference.bwt.SuffixArray; +import org.broadinstitute.sting.alignment.reference.bwt.SuffixArrayWriter; +import org.broadinstitute.sting.alignment.reference.bwt.ANNWriter; +import org.broadinstitute.sting.alignment.reference.bwt.AMBWriter; + +import java.io.File; +import java.io.IOException; + +import net.sf.samtools.SAMSequenceDictionary; +import net.sf.samtools.SAMSequenceRecord; +import net.sf.samtools.util.StringUtil; + +/** + * Support files for BWT. + * + * @author mhanna + * @version 0.1 + */ +public class BWTFiles { + /** + * ANN (?) file name. + */ + public final File annFile; + + /** + * AMB (?) file name. + */ + public final File ambFile; + + /** + * Packed reference sequence file. + */ + public final File pacFile; + + /** + * Reverse of packed reference sequence file. + */ + public final File rpacFile; + + /** + * Forward BWT file. + */ + public final File forwardBWTFile; + + /** + * Forward suffix array file. + */ + public final File forwardSAFile; + + /** + * Reverse BWT file. + */ + public final File reverseBWTFile; + + /** + * Reverse suffix array file. + */ + public final File reverseSAFile; + + /** + * Where these files autogenerated on the fly? + */ + public final boolean autogenerated; + + /** + * Create a new BWA configuration file using the given prefix. + * @param prefix Prefix to use when creating the configuration. Must not be null. + */ + public BWTFiles(String prefix) { + if(prefix == null) + throw new ReviewedStingException("Prefix must not be null."); + annFile = new File(prefix + ".ann"); + ambFile = new File(prefix + ".amb"); + pacFile = new File(prefix + ".pac"); + rpacFile = new File(prefix + ".rpac"); + forwardBWTFile = new File(prefix + ".bwt"); + forwardSAFile = new File(prefix + ".sa"); + reverseBWTFile = new File(prefix + ".rbwt"); + reverseSAFile = new File(prefix + ".rsa"); + autogenerated = false; + } + + /** + * Hand-create a new BWTFiles object, specifying a unique file object for each type. + * @param annFile ANN (alternate dictionary) file. + * @param ambFile AMB (holes) files. + * @param pacFile Packed representation of the forward reference sequence. + * @param forwardBWTFile BWT representation of the forward reference sequence. + * @param forwardSAFile SA representation of the forward reference sequence. + * @param rpacFile Packed representation of the reversed reference sequence. + * @param reverseBWTFile BWT representation of the reversed reference sequence. + * @param reverseSAFile SA representation of the reversed reference sequence. + */ + private BWTFiles(File annFile, + File ambFile, + File pacFile, + File forwardBWTFile, + File forwardSAFile, + File rpacFile, + File reverseBWTFile, + File reverseSAFile) { + this.annFile = annFile; + this.ambFile = ambFile; + this.pacFile = pacFile; + this.forwardBWTFile = forwardBWTFile; + this.forwardSAFile = forwardSAFile; + this.rpacFile = rpacFile; + this.reverseBWTFile = reverseBWTFile; + this.reverseSAFile = reverseSAFile; + autogenerated = true; + } + + /** + * Close out this files object, in the process deleting any temporary filse + * that were created. + */ + public void close() { + if(autogenerated) { + boolean success = true; + success = annFile.delete(); + success &= ambFile.delete(); + success &= pacFile.delete(); + success &= forwardBWTFile.delete(); + success &= forwardSAFile.delete(); + success &= rpacFile.delete(); + success &= reverseBWTFile.delete(); + success &= reverseSAFile.delete(); + + if(!success) + throw new ReviewedStingException("Unable to clean up autogenerated representation"); + } + } + + /** + * Create a new set of BWT files from the given reference sequence. + * @param referenceSequence Sequence from which to build metadata. + * @return A new object representing encoded representations of each sequence. + */ + public static BWTFiles createFromReferenceSequence(byte[] referenceSequence) { + byte[] normalizedReferenceSequence = new byte[referenceSequence.length]; + System.arraycopy(referenceSequence,0,normalizedReferenceSequence,0,referenceSequence.length); + normalizeReferenceSequence(normalizedReferenceSequence); + + File annFile,ambFile,pacFile,bwtFile,saFile,rpacFile,rbwtFile,rsaFile; + try { + // Write the ann and amb for this reference sequence. + annFile = File.createTempFile("bwt",".ann"); + ambFile = File.createTempFile("bwt",".amb"); + + SAMSequenceDictionary dictionary = new SAMSequenceDictionary(); + dictionary.addSequence(new SAMSequenceRecord("autogenerated",normalizedReferenceSequence.length)); + + ANNWriter annWriter = new ANNWriter(annFile); + annWriter.write(dictionary); + annWriter.close(); + + AMBWriter ambWriter = new AMBWriter(ambFile); + ambWriter.writeEmpty(dictionary); + ambWriter.close(); + + // Write the encoded files for the forward version of this reference sequence. + pacFile = File.createTempFile("bwt",".pac"); + bwtFile = File.createTempFile("bwt",".bwt"); + saFile = File.createTempFile("bwt",".sa"); + + writeEncodedReferenceSequence(normalizedReferenceSequence,pacFile,bwtFile,saFile); + + // Write the encoded files for the reverse version of this reference sequence. + byte[] reverseReferenceSequence = Utils.reverse(normalizedReferenceSequence); + + rpacFile = File.createTempFile("bwt",".rpac"); + rbwtFile = File.createTempFile("bwt",".rbwt"); + rsaFile = File.createTempFile("bwt",".rsa"); + + writeEncodedReferenceSequence(reverseReferenceSequence,rpacFile,rbwtFile,rsaFile); + } + catch(IOException ex) { + throw new ReviewedStingException("Unable to write autogenerated reference sequence to temporary files"); + } + + // Make sure that, at the very least, all temporary files are deleted on exit. + annFile.deleteOnExit(); + ambFile.deleteOnExit(); + pacFile.deleteOnExit(); + bwtFile.deleteOnExit(); + saFile.deleteOnExit(); + rpacFile.deleteOnExit(); + rbwtFile.deleteOnExit(); + rsaFile.deleteOnExit(); + + return new BWTFiles(annFile,ambFile,pacFile,bwtFile,saFile,rpacFile,rbwtFile,rsaFile); + } + + /** + * Write the encoded form of the reference sequence. In the case of BWA, the encoded reference + * sequence is the reference itself in PAC format, the BWT, and the suffix array. + * @param referenceSequence The reference sequence to encode. + * @param pacFile Target for the PAC-encoded reference. + * @param bwtFile Target for the BWT representation of the reference. + * @param suffixArrayFile Target for the suffix array encoding of the reference. + * @throws java.io.IOException In case of issues writing to the file. + */ + private static void writeEncodedReferenceSequence(byte[] referenceSequence, + File pacFile, + File bwtFile, + File suffixArrayFile) throws IOException { + PackUtils.writeReferenceSequence(pacFile,referenceSequence); + + BWT bwt = BWT.createFromReferenceSequence(referenceSequence); + BWTWriter bwtWriter = new BWTWriter(bwtFile); + bwtWriter.write(bwt); + bwtWriter.close(); + + SuffixArray suffixArray = SuffixArray.createFromReferenceSequence(referenceSequence); + SuffixArrayWriter suffixArrayWriter = new SuffixArrayWriter(suffixArrayFile); + suffixArrayWriter.write(suffixArray); + suffixArrayWriter.close(); + } + + /** + * Convert the given reference sequence into a form suitable for building into + * on-the-fly sequences. + * @param referenceSequence The reference sequence to normalize. + * @throws org.broadinstitute.sting.utils.exceptions.ReviewedStingException if normalized sequence cannot be generated. + */ + private static void normalizeReferenceSequence(byte[] referenceSequence) { + StringUtil.toUpperCase(referenceSequence); + for(byte base: referenceSequence) { + if(base != 'A' && base != 'C' && base != 'G' && base != 'T') + throw new ReviewedStingException(String.format("Base type %c is not supported when building references on-the-fly",(char)base)); + } + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/c/BWACAligner.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/c/BWACAligner.java new file mode 100644 index 000000000..8631c42d8 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/c/BWACAligner.java @@ -0,0 +1,258 @@ +package org.broadinstitute.sting.alignment.bwa.c; + +import net.sf.samtools.SAMRecord; +import net.sf.samtools.SAMFileHeader; +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.alignment.Alignment; +import org.broadinstitute.sting.alignment.bwa.BWAConfiguration; +import org.broadinstitute.sting.alignment.bwa.BWTFiles; +import org.broadinstitute.sting.alignment.bwa.BWAAligner; + +import java.util.*; + +/** + * An aligner using the BWA/C implementation. + * + * @author mhanna + * @version 0.1 + */ +public class BWACAligner extends BWAAligner { + static { + System.loadLibrary("bwa"); + } + + /** + * A pointer to the C++ object representing the BWA engine. + */ + private long thunkPointer = 0; + + public BWACAligner(BWTFiles bwtFiles, BWAConfiguration configuration) { + super(bwtFiles,configuration); + if(thunkPointer != 0) + throw new ReviewedStingException("BWA/C attempting to reinitialize."); + + if(!bwtFiles.annFile.exists()) throw new ReviewedStingException("ANN file is missing; please rerun 'bwa aln' to regenerate it."); + if(!bwtFiles.ambFile.exists()) throw new ReviewedStingException("AMB file is missing; please rerun 'bwa aln' to regenerate it."); + if(!bwtFiles.pacFile.exists()) throw new ReviewedStingException("PAC file is missing; please rerun 'bwa aln' to regenerate it."); + if(!bwtFiles.forwardBWTFile.exists()) throw new ReviewedStingException("Forward BWT file is missing; please rerun 'bwa aln' to regenerate it."); + if(!bwtFiles.forwardSAFile.exists()) throw new ReviewedStingException("Forward SA file is missing; please rerun 'bwa aln' to regenerate it."); + if(!bwtFiles.reverseBWTFile.exists()) throw new ReviewedStingException("Reverse BWT file is missing; please rerun 'bwa aln' to regenerate it."); + if(!bwtFiles.reverseSAFile.exists()) throw new ReviewedStingException("Reverse SA file is missing; please rerun 'bwa aln' to regenerate it."); + + thunkPointer = create(bwtFiles,configuration); + } + + /** + * Create an aligner object using an array of bytes as a reference. + * @param referenceSequence Reference sequence to encode ad-hoc. + * @param configuration Configuration for the given aligner. + */ + public BWACAligner(byte[] referenceSequence, BWAConfiguration configuration) { + this(BWTFiles.createFromReferenceSequence(referenceSequence),configuration); + // Now that the temporary files are created, the temporary files can be destroyed. + bwtFiles.close(); + } + + /** + * Update the configuration passed to the BWA aligner. + * @param configuration New configuration to set. + */ + @Override + public void updateConfiguration(BWAConfiguration configuration) { + if(thunkPointer == 0) + throw new ReviewedStingException("BWA/C: attempting to update configuration of uninitialized aligner."); + updateConfiguration(thunkPointer,configuration); + } + + /** + * Close this instance of the BWA pointer and delete its resources. + */ + @Override + public void close() { + if(thunkPointer == 0) + throw new ReviewedStingException("BWA/C close attempted, but BWA/C is not properly initialized."); + destroy(thunkPointer); + } + + /** + * Allow the aligner to choose one alignment randomly from the pile of best alignments. + * @param bases Bases to align. + * @return An align + */ + @Override + public Alignment getBestAlignment(final byte[] bases) { + if(thunkPointer == 0) + throw new ReviewedStingException("BWA/C getBestAlignment attempted, but BWA/C is not properly initialized."); + return getBestAlignment(thunkPointer,bases); + } + + /** + * Get the best aligned read, chosen randomly from the pile of best alignments. + * @param read Read to align. + * @param newHeader New header to apply to this SAM file. Can be null, but if so, read header must be valid. + * @return Read with injected alignment data. + */ + @Override + public SAMRecord align(final SAMRecord read, final SAMFileHeader newHeader) { + if(bwtFiles.autogenerated) + throw new UnsupportedOperationException("Cannot create target alignment; source contig was generated ad-hoc and is not reliable"); + return Alignment.convertToRead(getBestAlignment(read.getReadBases()),read,newHeader); + } + + /** + * Get a iterator of alignments, batched by mapping quality. + * @param bases List of bases. + * @return Iterator to alignments. + */ + @Override + public Iterable getAllAlignments(final byte[] bases) { + final BWAPath[] paths = getPaths(bases); + return new Iterable() { + public Iterator iterator() { + return new Iterator() { + /** + * The last position accessed. + */ + private int position = 0; + + /** + * Whether all alignments have been seen based on the current position. + * @return True if any more alignments are pending. False otherwise. + */ + public boolean hasNext() { return position < paths.length; } + + /** + * Return the next cross-section of alignments, based on mapping quality. + * @return Array of the next set of alignments of a given mapping quality. + */ + public Alignment[] next() { + if(position >= paths.length) + throw new UnsupportedOperationException("Out of alignments to return."); + int score = paths[position].score; + int startingPosition = position; + while(position < paths.length && paths[position].score == score) position++; + return convertPathsToAlignments(bases,Arrays.copyOfRange(paths,startingPosition,position)); + } + + /** + * Unsupported. + */ + public void remove() { throw new UnsupportedOperationException("Cannot remove from an alignment iterator"); } + }; + } + }; + } + + /** + * Get a iterator of aligned reads, batched by mapping quality. + * @param read Read to align. + * @param newHeader Optional new header to use when aligning the read. If present, it must be null. + * @return Iterator to alignments. + */ + @Override + public Iterable alignAll(final SAMRecord read, final SAMFileHeader newHeader) { + if(bwtFiles.autogenerated) + throw new UnsupportedOperationException("Cannot create target alignment; source contig was generated ad-hoc and is not reliable"); + final Iterable alignments = getAllAlignments(read.getReadBases()); + return new Iterable() { + public Iterator iterator() { + final Iterator alignmentIterator = alignments.iterator(); + return new Iterator() { + /** + * Whether all alignments have been seen based on the current position. + * @return True if any more alignments are pending. False otherwise. + */ + public boolean hasNext() { return alignmentIterator.hasNext(); } + + /** + * Return the next cross-section of alignments, based on mapping quality. + * @return Array of the next set of alignments of a given mapping quality. + */ + public SAMRecord[] next() { + Alignment[] alignmentsOfQuality = alignmentIterator.next(); + SAMRecord[] reads = new SAMRecord[alignmentsOfQuality.length]; + for(int i = 0; i < alignmentsOfQuality.length; i++) { + reads[i] = Alignment.convertToRead(alignmentsOfQuality[i],read,newHeader); + } + return reads; + } + + /** + * Unsupported. + */ + public void remove() { throw new UnsupportedOperationException("Cannot remove from an alignment iterator"); } + }; + } + }; + } + + /** + * Get the paths associated with the given base string. + * @param bases List of bases. + * @return A set of paths through the BWA. + */ + public BWAPath[] getPaths(byte[] bases) { + if(thunkPointer == 0) + throw new ReviewedStingException("BWA/C getPaths attempted, but BWA/C is not properly initialized."); + return getPaths(thunkPointer,bases); + } + + /** + * Create a pointer to the BWA/C thunk. + * @param files BWT source files. + * @param configuration Configuration of the aligner. + * @return Pointer to the BWA/C thunk. + */ + protected native long create(BWTFiles files, BWAConfiguration configuration); + + /** + * Update the configuration passed to the BWA aligner. For internal use only. + * @param thunkPointer pointer to BWA object. + * @param configuration New configuration to set. + */ + protected native void updateConfiguration(long thunkPointer, BWAConfiguration configuration); + + /** + * Destroy the BWA/C thunk. + * @param thunkPointer Pointer to the allocated thunk. + */ + protected native void destroy(long thunkPointer); + + /** + * Do the extra steps involved in converting a local alignment to a global alignment. + * @param bases ASCII representation of byte array. + * @param paths Paths through the current BWT. + * @return A list of alignments. + */ + protected Alignment[] convertPathsToAlignments(byte[] bases, BWAPath[] paths) { + if(thunkPointer == 0) + throw new ReviewedStingException("BWA/C convertPathsToAlignments attempted, but BWA/C is not properly initialized."); + return convertPathsToAlignments(thunkPointer,bases,paths); + } + + /** + * Caller to the path generation functionality within BWA/C. Call this method's getPaths() wrapper (above) instead. + * @param thunkPointer pointer to the C++ object managing BWA/C. + * @param bases ASCII representation of byte array. + * @return A list of paths through the specified BWT. + */ + protected native BWAPath[] getPaths(long thunkPointer, byte[] bases); + + /** + * Do the extra steps involved in converting a local alignment to a global alignment. + * Call this method's convertPathsToAlignments() wrapper (above) instead. + * @param thunkPointer pointer to the C++ object managing BWA/C. + * @param bases ASCII representation of byte array. + * @param paths Paths through the current BWT. + * @return A list of alignments. + */ + protected native Alignment[] convertPathsToAlignments(long thunkPointer, byte[] bases, BWAPath[] paths); + + /** + * Gets the best alignment from BWA/C, randomly selected from all best-aligned reads. + * @param thunkPointer Pointer to BWA thunk. + * @param bases bases to align. + * @return The best alignment from BWA/C. + */ + protected native Alignment getBestAlignment(long thunkPointer, byte[] bases); +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/c/BWAPath.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/c/BWAPath.java new file mode 100755 index 000000000..347d4344f --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/c/BWAPath.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2009 The Broad Institute + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.broadinstitute.sting.alignment.bwa.c; + +/** + * Models a BWA path. + * + * @author mhanna + * @version 0.1 + */ +public class BWAPath { + /** + * Number of mismatches encountered along this path. + */ + public final int numMismatches; + + /** + * Number of gap opens encountered along this path. + */ + public final int numGapOpens; + + /** + * Number of gap extensions along this path. + */ + public final int numGapExtensions; + + /** + * Whether this alignment was found on the positive or negative strand. + */ + public final boolean negativeStrand; + + /** + * Starting coordinate in the BWT. + */ + public final long k; + + /** + * Ending coordinate in the BWT. + */ + public final long l; + + /** + * The score of this path. + */ + public final int score; + + /** + * The number of best alignments seen along this path. + */ + public final int bestCount; + + /** + * The number of second best alignments seen along this path. + */ + public final int secondBestCount; + + /** + * Create a new path with the given attributes. + * @param numMismatches Number of mismatches along path. + * @param numGapOpens Number of gap opens along path. + * @param numGapExtensions Number of gap extensions along path. + * @param k Index to first coordinate within BWT. + * @param l Index to last coordinate within BWT. + * @param score Score of this alignment. Not the mapping quality. + */ + public BWAPath(int numMismatches, int numGapOpens, int numGapExtensions, boolean negativeStrand, long k, long l, int score, int bestCount, int secondBestCount) { + this.numMismatches = numMismatches; + this.numGapOpens = numGapOpens; + this.numGapExtensions = numGapExtensions; + this.negativeStrand = negativeStrand; + this.k = k; + this.l = l; + this.score = score; + this.bestCount = bestCount; + this.secondBestCount = secondBestCount; + } + +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignerTestHarness.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignerTestHarness.java new file mode 100644 index 000000000..ae6e22221 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignerTestHarness.java @@ -0,0 +1,165 @@ +package org.broadinstitute.sting.alignment.bwa.java; + +import org.broadinstitute.sting.alignment.Aligner; +import org.broadinstitute.sting.alignment.Alignment; +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.utils.BaseUtils; + +import java.io.File; +import java.io.FileNotFoundException; + +import net.sf.samtools.*; +import net.sf.picard.reference.IndexedFastaSequenceFile; + +/** + * A test harness to ensure that the perfect aligner works. + * + * @author mhanna + * @version 0.1 + */ +public class AlignerTestHarness { + public static void main( String argv[] ) throws FileNotFoundException { + if( argv.length != 6 ) { + System.out.println("PerfectAlignerTestHarness "); + System.exit(1); + } + + File referenceFile = new File(argv[0]); + File bwtFile = new File(argv[1]); + File rbwtFile = new File(argv[2]); + File suffixArrayFile = new File(argv[3]); + File reverseSuffixArrayFile = new File(argv[4]); + File bamFile = new File(argv[5]); + + align(referenceFile,bwtFile,rbwtFile,suffixArrayFile,reverseSuffixArrayFile,bamFile); + } + + private static void align(File referenceFile, File bwtFile, File rbwtFile, File suffixArrayFile, File reverseSuffixArrayFile, File bamFile) throws FileNotFoundException { + Aligner aligner = new BWAJavaAligner(bwtFile,rbwtFile,suffixArrayFile,reverseSuffixArrayFile); + int count = 0; + + SAMFileReader reader = new SAMFileReader(bamFile); + reader.setValidationStringency(SAMFileReader.ValidationStringency.SILENT); + + int mismatches = 0; + int failures = 0; + + for(SAMRecord read: reader) { + count++; + if( count > 200000 ) break; + //if( count < 366000 ) continue; + //if( count > 2 ) break; + //if( !read.getReadName().endsWith("SL-XBC:1:82:506:404#0") ) + // continue; + //if( !read.getReadName().endsWith("SL-XBC:1:36:30:1926#0") ) + // continue; + //if( !read.getReadName().endsWith("SL-XBC:1:60:1342:1340#0") ) + // continue; + + SAMRecord alignmentCleaned = null; + try { + alignmentCleaned = (SAMRecord)read.clone(); + } + catch( CloneNotSupportedException ex ) { + throw new ReviewedStingException("SAMRecord clone not supported", ex); + } + + if( alignmentCleaned.getReadNegativeStrandFlag() ) + alignmentCleaned.setReadBases(BaseUtils.simpleReverseComplement(alignmentCleaned.getReadBases())); + + alignmentCleaned.setReferenceIndex(SAMRecord.NO_ALIGNMENT_REFERENCE_INDEX); + alignmentCleaned.setAlignmentStart(SAMRecord.NO_ALIGNMENT_START); + alignmentCleaned.setMappingQuality(SAMRecord.NO_MAPPING_QUALITY); + alignmentCleaned.setCigarString(SAMRecord.NO_ALIGNMENT_CIGAR); + + // Clear everything except flags pertaining to pairing and set 'unmapped' status to true. + alignmentCleaned.setFlags(alignmentCleaned.getFlags() & 0x00A1 | 0x000C); + + Iterable alignments = aligner.getAllAlignments(alignmentCleaned.getReadBases()); + if(!alignments.iterator().hasNext() ) { + //throw new StingException(String.format("Unable to align read %s to reference; count = %d",read.getReadName(),count)); + System.out.printf("Unable to align read %s to reference; count = %d%n",read.getReadName(),count); + failures++; + } + + Alignment foundAlignment = null; + for(Alignment[] alignmentsOfQuality: alignments) { + for(Alignment alignment: alignmentsOfQuality) { + if( read.getReadNegativeStrandFlag() != alignment.isNegativeStrand() ) + continue; + if( read.getAlignmentStart() != alignment.getAlignmentStart() ) + continue; + + foundAlignment = alignment; + } + } + + if( foundAlignment != null ) { + //System.out.printf("%s: Aligned read to reference at position %d with %d mismatches, %d gap opens, and %d gap extensions.%n", read.getReadName(), foundAlignment.getAlignmentStart(), foundAlignment.getMismatches(), foundAlignment.getGapOpens(), foundAlignment.getGapExtensions()); + } + else { + System.out.printf("Error aligning read %s%n", read.getReadName()); + + mismatches++; + + IndexedFastaSequenceFile reference = new IndexedFastaSequenceFile(referenceFile); + + System.out.printf("read = %s, position = %d, negative strand = %b%n", formatBasesBasedOnCigar(read.getReadString(),read.getCigar(),CigarOperator.DELETION), + read.getAlignmentStart(), + read.getReadNegativeStrandFlag()); + int numDeletions = numDeletionsInCigar(read.getCigar()); + String expectedRef = new String(reference.getSubsequenceAt(reference.getSequenceDictionary().getSequences().get(0).getSequenceName(),read.getAlignmentStart(),read.getAlignmentStart()+read.getReadLength()+numDeletions-1).getBases()); + System.out.printf("expected ref = %s%n", formatBasesBasedOnCigar(expectedRef,read.getCigar(),CigarOperator.INSERTION)); + + for(Alignment[] alignmentsOfQuality: alignments) { + for(Alignment alignment: alignmentsOfQuality) { + System.out.println(); + + Cigar cigar = ((BWAAlignment)alignment).getCigar(); + + System.out.printf("read = %s%n", formatBasesBasedOnCigar(read.getReadString(),cigar,CigarOperator.DELETION)); + + int deletionCount = ((BWAAlignment)alignment).getNumberOfBasesMatchingState(AlignmentState.DELETION); + String alignedRef = new String(reference.getSubsequenceAt(reference.getSequenceDictionary().getSequences().get(0).getSequenceName(),alignment.getAlignmentStart(),alignment.getAlignmentStart()+read.getReadLength()+deletionCount-1).getBases()); + System.out.printf("actual ref = %s, position = %d, negative strand = %b%n", formatBasesBasedOnCigar(alignedRef,cigar,CigarOperator.INSERTION), + alignment.getAlignmentStart(), + alignment.isNegativeStrand()); + } + } + + //throw new StingException(String.format("Read %s was placed at incorrect location; count = %d%n",read.getReadName(),count)); + } + + + if( count % 1000 == 0 ) + System.out.printf("%d reads examined.%n",count); + } + + System.out.printf("%d reads examined; %d mismatches; %d failures.%n",count,mismatches,failures); + } + + private static String formatBasesBasedOnCigar( String bases, Cigar cigar, CigarOperator toBlank ) { + StringBuilder formatted = new StringBuilder(); + int readIndex = 0; + for(CigarElement cigarElement: cigar.getCigarElements()) { + if(cigarElement.getOperator() == toBlank) { + int number = cigarElement.getLength(); + while( number-- > 0 ) formatted.append(' '); + } + else { + int number = cigarElement.getLength(); + while( number-- > 0 ) formatted.append(bases.charAt(readIndex++)); + } + } + return formatted.toString(); + } + + private static int numDeletionsInCigar( Cigar cigar ) { + int numDeletions = 0; + for(CigarElement cigarElement: cigar.getCigarElements()) { + if(cigarElement.getOperator() == CigarOperator.DELETION) + numDeletions += cigarElement.getLength(); + } + return numDeletions; + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignmentMatchSequence.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignmentMatchSequence.java new file mode 100644 index 000000000..879ecb5fb --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignmentMatchSequence.java @@ -0,0 +1,151 @@ +package org.broadinstitute.sting.alignment.bwa.java; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; + +import java.util.Deque; +import java.util.ArrayDeque; +import java.util.Iterator; + +import net.sf.samtools.Cigar; +import net.sf.samtools.CigarElement; +import net.sf.samtools.CigarOperator; + +/** + * Represents a sequence of matches. + * + * @author mhanna + * @version 0.1 + */ +public class AlignmentMatchSequence implements Cloneable { + /** + * Stores the particular match entries in the order they occur. + */ + private Deque entries = new ArrayDeque(); + + /** + * Clone the given match sequence. + * @return A deep copy of the current match sequence. + */ + public AlignmentMatchSequence clone() { + AlignmentMatchSequence copy = null; + try { + copy = (AlignmentMatchSequence)super.clone(); + } + catch( CloneNotSupportedException ex ) { + throw new ReviewedStingException("Unable to clone AlignmentMatchSequence."); + } + + copy.entries = new ArrayDeque(); + for( AlignmentMatchSequenceEntry entry: entries ) + copy.entries.add(entry.clone()); + + return copy; + } + + public Cigar convertToCigar(boolean negativeStrand) { + Cigar cigar = new Cigar(); + Iterator iterator = negativeStrand ? entries.descendingIterator() : entries.iterator(); + while( iterator.hasNext() ) { + AlignmentMatchSequenceEntry entry = iterator.next(); + CigarOperator operator; + switch( entry.getAlignmentState() ) { + case MATCH_MISMATCH: operator = CigarOperator.MATCH_OR_MISMATCH; break; + case INSERTION: operator = CigarOperator.INSERTION; break; + case DELETION: operator = CigarOperator.DELETION; break; + default: throw new ReviewedStingException("convertToCigar: cannot process state: " + entry.getAlignmentState()); + } + cigar.add( new CigarElement(entry.count,operator) ); + } + return cigar; + } + + /** + * All a new alignment of the given state. + * @param state State to add to the sequence. + */ + public void addNext( AlignmentState state ) { + AlignmentMatchSequenceEntry last = entries.peekLast(); + // If the last entry is the same as this one, increment it. Otherwise, add a new entry. + if( last != null && last.alignmentState == state ) + last.increment(); + else + entries.add(new AlignmentMatchSequenceEntry(state)); + } + + /** + * Gets the current state of this alignment (what's the state of the last base?) + * @return State of the most recently aligned base. + */ + public AlignmentState getCurrentState() { + if( entries.size() == 0 ) + return AlignmentState.MATCH_MISMATCH; + return entries.peekLast().getAlignmentState(); + } + + /** + * How many bases in the read match the given state. + * @param state State to test. + * @return number of bases which match that state. + */ + public int getNumberOfBasesMatchingState(AlignmentState state) { + int matches = 0; + for( AlignmentMatchSequenceEntry entry: entries ) { + if( entry.getAlignmentState() == state ) + matches += entry.count; + } + return matches; + } + + /** + * Stores an individual match sequence entry. + */ + private class AlignmentMatchSequenceEntry implements Cloneable { + /** + * The state of the alignment throughout a given point in the sequence. + */ + private final AlignmentState alignmentState; + + /** + * The number of bases having this particular state. + */ + private int count; + + /** + * Create a new sequence entry with the given state. + * @param alignmentState The state that this sequence should contain. + */ + AlignmentMatchSequenceEntry( AlignmentState alignmentState ) { + this.alignmentState = alignmentState; + this.count = 1; + } + + /** + * Clone the given match sequence entry. + * @return A deep copy of the current match sequence entry. + */ + public AlignmentMatchSequenceEntry clone() { + try { + return (AlignmentMatchSequenceEntry)super.clone(); + } + catch( CloneNotSupportedException ex ) { + throw new ReviewedStingException("Unable to clone AlignmentMatchSequenceEntry."); + } + } + + /** + * Retrieves the current state of the alignment. + * @return The state of the current sequence. + */ + AlignmentState getAlignmentState() { + return alignmentState; + } + + /** + * Increment the count of alignments having this particular state. + */ + void increment() { + count++; + } + } +} + diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignmentState.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignmentState.java new file mode 100644 index 000000000..92c603335 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/AlignmentState.java @@ -0,0 +1,13 @@ +package org.broadinstitute.sting.alignment.bwa.java; + +/** + * The state of a given base in the alignment. + * + * @author mhanna + * @version 0.1 + */ +public enum AlignmentState { + MATCH_MISMATCH, + INSERTION, + DELETION +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/java/BWAAlignment.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/BWAAlignment.java new file mode 100644 index 000000000..c59546bbb --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/BWAAlignment.java @@ -0,0 +1,190 @@ +package org.broadinstitute.sting.alignment.bwa.java; + +import org.broadinstitute.sting.alignment.Alignment; +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import net.sf.samtools.Cigar; + +/** + * An alignment object to be used incrementally as the BWA aligner + * inspects the read. + * + * @author mhanna + * @version 0.1 + */ +public class BWAAlignment extends Alignment implements Cloneable { + /** + * Track the number of alignments that have been created. + */ + private static long numCreated; + + /** + * Which number alignment is this? + */ + private long creationNumber; + + /** + * The aligner performing the alignments. + */ + protected BWAJavaAligner aligner; + + /** + * The sequence of matches/mismatches/insertions/deletions. + */ + private AlignmentMatchSequence alignmentMatchSequence = new AlignmentMatchSequence(); + + /** + * Working variable. How many bases have been matched at this point. + */ + protected int position; + + /** + * Working variable. How many mismatches have been encountered at this point. + */ + private int mismatches; + + /** + * Number of gap opens in alignment. + */ + private int gapOpens; + + /** + * Number of gap extensions in alignment. + */ + private int gapExtensions; + + /** + * Working variable. The lower bound of the alignment within the BWT. + */ + protected long loBound; + + /** + * Working variable. The upper bound of the alignment within the BWT. + */ + protected long hiBound; + + protected void setAlignmentStart(long position) { + this.alignmentStart = position; + } + + protected void setNegativeStrand(boolean negativeStrand) { + this.negativeStrand = negativeStrand; + } + + /** + * Cache the score. + */ + private int score; + + public Cigar getCigar() { + return alignmentMatchSequence.convertToCigar(isNegativeStrand()); + } + + /** + * Gets the current state of this alignment (state of the last base viewed).. + * @return Current state of the alignment. + */ + public AlignmentState getCurrentState() { + return alignmentMatchSequence.getCurrentState(); + } + + /** + * Adds the given state to the current alignment. + * @param state State to add to the given alignment. + */ + public void addState( AlignmentState state ) { + alignmentMatchSequence.addNext(state); + } + + /** + * Gets the BWA score of this alignment. + * @return BWA-style scores. 0 is best. + */ + public int getScore() { + return score; + } + + public int getMismatches() { return mismatches; } + public int getGapOpens() { return gapOpens; } + public int getGapExtensions() { return gapExtensions; } + + public void incrementMismatches() { + this.mismatches++; + updateScore(); + } + + public void incrementGapOpens() { + this.gapOpens++; + updateScore(); + } + + public void incrementGapExtensions() { + this.gapExtensions++; + updateScore(); + } + + /** + * Updates the score based on new information about matches / mismatches. + */ + private void updateScore() { + score = mismatches*aligner.MISMATCH_PENALTY + gapOpens*aligner.GAP_OPEN_PENALTY + gapExtensions*aligner.GAP_EXTENSION_PENALTY; + } + + /** + * Create a new alignment with the given parent aligner. + * @param aligner Aligner being used. + */ + public BWAAlignment( BWAJavaAligner aligner ) { + this.aligner = aligner; + this.creationNumber = numCreated++; + } + + /** + * Clone the alignment. + * @return New instance of the alignment. + */ + public BWAAlignment clone() { + BWAAlignment newAlignment = null; + try { + newAlignment = (BWAAlignment)super.clone(); + } + catch( CloneNotSupportedException ex ) { + throw new ReviewedStingException("Unable to clone BWAAlignment."); + } + newAlignment.creationNumber = numCreated++; + newAlignment.alignmentMatchSequence = alignmentMatchSequence.clone(); + + return newAlignment; + } + + /** + * How many bases in the read match the given state. + * @param state State to test. + * @return number of bases which match that state. + */ + public int getNumberOfBasesMatchingState(AlignmentState state) { + return alignmentMatchSequence.getNumberOfBasesMatchingState(state); + } + + /** + * Compare this alignment to another alignment. + * @param rhs Other alignment to which to compare. + * @return < 0 if this < other, == 0 if this == other, > 0 if this > other + */ + public int compareTo(Alignment rhs) { + BWAAlignment other = (BWAAlignment)rhs; + + // If the scores are different, disambiguate using the score. + if(score != other.score) + return score > other.score ? 1 : -1; + + // Otherwise, use the order in which the elements were created. + if(creationNumber != other.creationNumber) + return creationNumber > other.creationNumber ? -1 : 1; + + return 0; + } + + public String toString() { + return String.format("position: %d, strand: %b, state: %s, mismatches: %d, gap opens: %d, gap extensions: %d, loBound: %d, hiBound: %d, score: %d, creationNumber: %d", position, negativeStrand, alignmentMatchSequence.getCurrentState(), mismatches, gapOpens, gapExtensions, loBound, hiBound, getScore(), creationNumber); + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/java/BWAJavaAligner.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/BWAJavaAligner.java new file mode 100644 index 000000000..81186c53e --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/BWAJavaAligner.java @@ -0,0 +1,392 @@ +package org.broadinstitute.sting.alignment.bwa.java; + +import org.broadinstitute.sting.alignment.reference.bwt.*; +import org.broadinstitute.sting.alignment.bwa.BWAAligner; +import org.broadinstitute.sting.alignment.bwa.BWAConfiguration; +import org.broadinstitute.sting.alignment.Alignment; +import org.broadinstitute.sting.utils.BaseUtils; +import org.broadinstitute.sting.utils.Utils; + +import java.io.File; +import java.util.*; + +import net.sf.samtools.SAMRecord; +import net.sf.samtools.SAMFileHeader; + +/** + * Create imperfect alignments from the read to the genome represented by the given BWT / suffix array. + * + * @author mhanna + * @version 0.1 + */ +public class BWAJavaAligner extends BWAAligner { + /** + * BWT in the forward direction. + */ + private BWT forwardBWT; + + /** + * BWT in the reverse direction. + */ + private BWT reverseBWT; + + /** + * Suffix array in the forward direction. + */ + private SuffixArray forwardSuffixArray; + + /** + * Suffix array in the reverse direction. + */ + private SuffixArray reverseSuffixArray; + + /** + * Maximum edit distance (-n option from original BWA). + */ + private final int MAXIMUM_EDIT_DISTANCE = 4; + + /** + * Maximum number of gap opens (-o option from original BWA). + */ + private final int MAXIMUM_GAP_OPENS = 1; + + /** + * Maximum number of gap extensions (-e option from original BWA). + */ + private final int MAXIMUM_GAP_EXTENSIONS = 6; + + /** + * Penalty for straight mismatches (-M option from original BWA). + */ + public final int MISMATCH_PENALTY = 3; + + /** + * Penalty for gap opens (-O option from original BWA). + */ + public final int GAP_OPEN_PENALTY = 11; + + /** + * Penalty for gap extensions (-E option from original BWA). + */ + public final int GAP_EXTENSION_PENALTY = 4; + + /** + * Skip the ends of indels. + */ + public final int INDEL_END_SKIP = 5; + + public BWAJavaAligner( File forwardBWTFile, File reverseBWTFile, File forwardSuffixArrayFile, File reverseSuffixArrayFile ) { + super(null,null); + forwardBWT = new BWTReader(forwardBWTFile).read(); + reverseBWT = new BWTReader(reverseBWTFile).read(); + forwardSuffixArray = new SuffixArrayReader(forwardSuffixArrayFile,forwardBWT).read(); + reverseSuffixArray = new SuffixArrayReader(reverseSuffixArrayFile,reverseBWT).read(); + } + + /** + * Close this instance of the BWA pointer and delete its resources. + */ + @Override + public void close() { + throw new UnsupportedOperationException("BWA aligner can't currently be closed."); + } + + /** + * Update the current parameters of this aligner. + * @param configuration New configuration to set. + */ + public void updateConfiguration(BWAConfiguration configuration) { + throw new UnsupportedOperationException("Configuration of the BWA aligner can't currently be changed."); + } + + /** + * Allow the aligner to choose one alignment randomly from the pile of best alignments. + * @param bases Bases to align. + * @return An align + */ + public Alignment getBestAlignment(final byte[] bases) { throw new UnsupportedOperationException("BWAJavaAligner does not yet support the standard Aligner interface."); } + + /** + * Align the read to the reference. + * @param read Read to align. + * @param header Optional header to drop in place. + * @return A list of the alignments. + */ + public SAMRecord align(final SAMRecord read, final SAMFileHeader header) { throw new UnsupportedOperationException("BWAJavaAligner does not yet support the standard Aligner interface."); } + + /** + * Get a iterator of alignments, batched by mapping quality. + * @param bases List of bases. + * @return Iterator to alignments. + */ + public Iterable getAllAlignments(final byte[] bases) { throw new UnsupportedOperationException("BWAJavaAligner does not yet support the standard Aligner interface."); } + + /** + * Get a iterator of aligned reads, batched by mapping quality. + * @param read Read to align. + * @param newHeader Optional new header to use when aligning the read. If present, it must be null. + * @return Iterator to alignments. + */ + public Iterable alignAll(final SAMRecord read, final SAMFileHeader newHeader) { throw new UnsupportedOperationException("BWAJavaAligner does not yet support the standard Aligner interface."); } + + + public List align( SAMRecord read ) { + List successfulMatches = new ArrayList(); + + Byte[] uncomplementedBases = normalizeBases(read.getReadBases()); + Byte[] complementedBases = normalizeBases(Utils.reverse(BaseUtils.simpleReverseComplement(read.getReadBases()))); + + List forwardLowerBounds = LowerBound.create(uncomplementedBases,forwardBWT); + List reverseLowerBounds = LowerBound.create(complementedBases,reverseBWT); + + // Seed the best score with any score that won't overflow on comparison. + int bestScore = Integer.MAX_VALUE - MISMATCH_PENALTY; + int bestDiff = MAXIMUM_EDIT_DISTANCE+1; + int maxDiff = MAXIMUM_EDIT_DISTANCE; + + PriorityQueue alignments = new PriorityQueue(); + + // Create a fictional initial alignment, with the position just off the end of the read, and the limits + // set as the entire BWT. + alignments.add(createSeedAlignment(reverseBWT)); + alignments.add(createSeedAlignment(forwardBWT)); + + while(!alignments.isEmpty()) { + BWAAlignment alignment = alignments.remove(); + + // From bwtgap.c in the original BWT; if the rank is worse than the best score + the mismatch PENALTY, move on. + if( alignment.getScore() > bestScore + MISMATCH_PENALTY ) + break; + + Byte[] bases = alignment.isNegativeStrand() ? complementedBases : uncomplementedBases; + BWT bwt = alignment.isNegativeStrand() ? forwardBWT : reverseBWT; + List lowerBounds = alignment.isNegativeStrand() ? reverseLowerBounds : forwardLowerBounds; + + // if z < D(i) then return {} + int mismatches = maxDiff - alignment.getMismatches() - alignment.getGapOpens() - alignment.getGapExtensions(); + if( alignment.position < lowerBounds.size()-1 && mismatches < lowerBounds.get(alignment.position+1).value ) + continue; + + if(mismatches == 0) { + exactMatch(alignment,bases,bwt); + if(alignment.loBound > alignment.hiBound) + continue; + } + + // Found a valid alignment; store it and move on. + if(alignment.position >= read.getReadLength()-1) { + for(long bwtIndex = alignment.loBound; bwtIndex <= alignment.hiBound; bwtIndex++) { + BWAAlignment finalAlignment = alignment.clone(); + + if( finalAlignment.isNegativeStrand() ) + finalAlignment.setAlignmentStart(forwardSuffixArray.get(bwtIndex) + 1); + else { + int sizeAlongReference = read.getReadLength() - + finalAlignment.getNumberOfBasesMatchingState(AlignmentState.INSERTION) + + finalAlignment.getNumberOfBasesMatchingState(AlignmentState.DELETION); + finalAlignment.setAlignmentStart(reverseBWT.length() - reverseSuffixArray.get(bwtIndex) - sizeAlongReference + 1); + } + + successfulMatches.add(finalAlignment); + + bestScore = Math.min(finalAlignment.getScore(),bestScore); + bestDiff = Math.min(finalAlignment.getMismatches()+finalAlignment.getGapOpens()+finalAlignment.getGapExtensions(),bestDiff); + maxDiff = bestDiff + 1; + } + + continue; + } + + //System.out.printf("Processing alignments; queue size = %d, alignment = %s, bound = %d, base = %s%n", alignments.size(), alignment, lowerBounds.get(alignment.position+1).value, alignment.position >= 0 ? (char)bases[alignment.position].byteValue() : ""); + /* + System.out.printf("#1\t[%d,%d,%d,%c]\t[%d,%d,%d]\t[%d,%d]\t[%d,%d]%n",alignments.size(), + alignment.negativeStrand?1:0, + bases.length-alignment.position-1, + alignment.getCurrentState().toString().charAt(0), + alignment.getMismatches(), + alignment.getGapOpens(), + alignment.getGapExtensions(), + lowerBounds.get(alignment.position+1).value, + lowerBounds.get(alignment.position+1).width, + alignment.loBound, + alignment.hiBound); + */ + + // Temporary -- look ahead to see if the next alignment is bounded. + boolean allowDifferences = mismatches > 0; + boolean allowMismatches = mismatches > 0; + + if( allowDifferences && + alignment.position+1 >= INDEL_END_SKIP-1+alignment.getGapOpens()+alignment.getGapExtensions() && + read.getReadLength()-1-(alignment.position+1) >= INDEL_END_SKIP+alignment.getGapOpens()+alignment.getGapExtensions() ) { + if( alignment.getCurrentState() == AlignmentState.MATCH_MISMATCH ) { + if( alignment.getGapOpens() < MAXIMUM_GAP_OPENS ) { + // Add a potential insertion extension. + BWAAlignment insertionAlignment = createInsertionAlignment(alignment); + insertionAlignment.incrementGapOpens(); + alignments.add(insertionAlignment); + + // Add a potential deletion by marking a deletion and augmenting the position. + List deletionAlignments = createDeletionAlignments(bwt,alignment); + for( BWAAlignment deletionAlignment: deletionAlignments ) + deletionAlignment.incrementGapOpens(); + alignments.addAll(deletionAlignments); + } + } + else if( alignment.getCurrentState() == AlignmentState.INSERTION ) { + if( alignment.getGapExtensions() < MAXIMUM_GAP_EXTENSIONS && mismatches > 0 ) { + // Add a potential insertion extension. + BWAAlignment insertionAlignment = createInsertionAlignment(alignment); + insertionAlignment.incrementGapExtensions(); + alignments.add(insertionAlignment); + } + } + else if( alignment.getCurrentState() == AlignmentState.DELETION ) { + if( alignment.getGapExtensions() < MAXIMUM_GAP_EXTENSIONS && mismatches > 0 ) { + // Add a potential deletion by marking a deletion and augmenting the position. + List deletionAlignments = createDeletionAlignments(bwt,alignment); + for( BWAAlignment deletionAlignment: deletionAlignments ) + deletionAlignment.incrementGapExtensions(); + alignments.addAll(deletionAlignments); + } + } + } + + // Mismatches + alignments.addAll(createMatchedAlignments(bwt,alignment,bases,allowDifferences&&allowMismatches)); + } + + return successfulMatches; + } + + /** + * Create an seeding alignment to use as a starting point when traversing. + * @param bwt source BWT. + * @return Seed alignment. + */ + private BWAAlignment createSeedAlignment(BWT bwt) { + BWAAlignment seed = new BWAAlignment(this); + seed.setNegativeStrand(bwt == forwardBWT); + seed.position = -1; + seed.loBound = 0; + seed.hiBound = bwt.length(); + return seed; + } + + /** + * Creates a new alignments representing direct matches / mismatches. + * @param bwt Source BWT with which to work. + * @param alignment Alignment for the previous position. + * @param bases The bases in the read. + * @param allowMismatch Should mismatching bases be allowed? + * @return New alignment representing this position if valid; null otherwise. + */ + private List createMatchedAlignments( BWT bwt, BWAAlignment alignment, Byte[] bases, boolean allowMismatch ) { + List newAlignments = new ArrayList(); + + List baseChoices = new ArrayList(); + Byte thisBase = bases[alignment.position+1]; + + if( allowMismatch ) + baseChoices.addAll(Bases.allOf()); + else + baseChoices.add(thisBase); + + if( thisBase != null ) { + // Keep rotating the current base to the last position until we've hit the current base. + for( ;; ) { + baseChoices.add(baseChoices.remove(0)); + if( thisBase.equals(baseChoices.get(baseChoices.size()-1)) ) + break; + + } + } + + for(byte base: baseChoices) { + BWAAlignment newAlignment = alignment.clone(); + + newAlignment.loBound = bwt.counts(base) + bwt.occurrences(base,alignment.loBound-1) + 1; + newAlignment.hiBound = bwt.counts(base) + bwt.occurrences(base,alignment.hiBound); + + // If this alignment is valid, skip it. + if( newAlignment.loBound > newAlignment.hiBound ) + continue; + + newAlignment.position++; + newAlignment.addState(AlignmentState.MATCH_MISMATCH); + if( bases[newAlignment.position] == null || base != bases[newAlignment.position] ) + newAlignment.incrementMismatches(); + + newAlignments.add(newAlignment); + } + + return newAlignments; + } + + /** + * Create a new alignment representing an insertion at this point in the read. + * @param alignment Alignment from which to derive the insertion. + * @return New alignment reflecting the insertion. + */ + private BWAAlignment createInsertionAlignment( BWAAlignment alignment ) { + // Add a potential insertion extension. + BWAAlignment newAlignment = alignment.clone(); + newAlignment.position++; + newAlignment.addState(AlignmentState.INSERTION); + return newAlignment; + } + + /** + * Create new alignments representing a deletion at this point in the read. + * @param bwt source BWT for inferring deletion info. + * @param alignment Alignment from which to derive the deletion. + * @return New alignments reflecting all possible deletions. + */ + private List createDeletionAlignments( BWT bwt, BWAAlignment alignment) { + List newAlignments = new ArrayList(); + for(byte base: Bases.instance) { + BWAAlignment newAlignment = alignment.clone(); + + newAlignment.loBound = bwt.counts(base) + bwt.occurrences(base,alignment.loBound-1) + 1; + newAlignment.hiBound = bwt.counts(base) + bwt.occurrences(base,alignment.hiBound); + + // If this alignment is valid, skip it. + if( newAlignment.loBound > newAlignment.hiBound ) + continue; + + newAlignment.addState(AlignmentState.DELETION); + + newAlignments.add(newAlignment); + } + + return newAlignments; + } + + /** + * Exactly match the given alignment against the given BWT. + * @param alignment Alignment to match. + * @param bases Bases to use. + * @param bwt BWT to use. + */ + private void exactMatch( BWAAlignment alignment, Byte[] bases, BWT bwt ) { + while( ++alignment.position < bases.length ) { + byte base = bases[alignment.position]; + alignment.loBound = bwt.counts(base) + bwt.occurrences(base,alignment.loBound-1) + 1; + alignment.hiBound = bwt.counts(base) + bwt.occurrences(base,alignment.hiBound); + if( alignment.loBound > alignment.hiBound ) + return; + } + } + + /** + * Make each base into A/C/G/T or null if unknown. + * @param bases Base string to normalize. + * @return Array of normalized bases. + */ + private Byte[] normalizeBases( byte[] bases ) { + Byte[] normalBases = new Byte[bases.length]; + for(int i = 0; i < bases.length; i++) + normalBases[i] = Bases.fromASCII(bases[i]); + return normalBases; + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/bwa/java/LowerBound.java b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/LowerBound.java new file mode 100644 index 000000000..3784643c0 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/bwa/java/LowerBound.java @@ -0,0 +1,88 @@ +package org.broadinstitute.sting.alignment.bwa.java; + +import java.util.List; +import java.util.ArrayList; + +import org.broadinstitute.sting.alignment.reference.bwt.BWT; + +/** + * At any point along the given read, what is a good lower bound for the + * total number of differences? + * + * @author mhanna + * @version 0.1 + */ +public class LowerBound { + /** + * Lower bound of the suffix array. + */ + public final long loIndex; + + /** + * Upper bound of the suffix array. + */ + public final long hiIndex; + + /** + * Width of the bwt from loIndex -> hiIndex, inclusive. + */ + public final long width; + + /** + * The lower bound at the given point. + */ + public final int value; + + /** + * Create a new lower bound with the given value. + * @param loIndex The lower bound of the BWT. + * @param hiIndex The upper bound of the BWT. + * @param value Value for the lower bound at this site. + */ + private LowerBound(long loIndex, long hiIndex, int value) { + this.loIndex = loIndex; + this.hiIndex = hiIndex; + this.width = hiIndex - loIndex + 1; + this.value = value; + } + + /** + * Create a non-optimal bound according to the algorithm specified in Figure 3 of the BWA paper. + * @param bases Bases of the read to use when creating a new BWT. + * @param bwt BWT to check against. + * @return A list of lower bounds at every point in the reference. + * + */ + public static List create(Byte[] bases, BWT bwt) { + List bounds = new ArrayList(); + + long loIndex = 0, hiIndex = bwt.length(); + int mismatches = 0; + for( int i = bases.length-1; i >= 0; i-- ) { + Byte base = bases[i]; + + // Ignore non-ACGT bases. + if( base != null ) { + loIndex = bwt.counts(base) + bwt.occurrences(base,loIndex-1) + 1; + hiIndex = bwt.counts(base) + bwt.occurrences(base,hiIndex); + } + + if( base == null || loIndex > hiIndex ) { + loIndex = 0; + hiIndex = bwt.length(); + mismatches++; + } + bounds.add(0,new LowerBound(loIndex,hiIndex,mismatches)); + } + + return bounds; + } + + /** + * Create a string representation of this bound. + * @return String version of this bound. + */ + public String toString() { + return String.format("LowerBound: w = %d, value = %d",width,value); + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/package-info.java b/public/java/src/org/broadinstitute/sting/alignment/package-info.java new file mode 100644 index 000000000..60cf1e425 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/package-info.java @@ -0,0 +1,4 @@ +/** + * Analyses used to validate the correctness and performance the BWA Java bindings. + */ +package org.broadinstitute.sting.alignment; \ No newline at end of file diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/AMBWriter.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/AMBWriter.java new file mode 100644 index 000000000..1d97fec79 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/AMBWriter.java @@ -0,0 +1,68 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import net.sf.samtools.SAMSequenceDictionary; +import net.sf.samtools.SAMSequenceRecord; + +import java.io.PrintStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Writes .amb files - a file indicating where 'holes' (indeterminant bases) + * exist in the contig. Currently, only empty, placeholder AMBs are supported. + * + * @author mhanna + * @version 0.1 + */ +public class AMBWriter { + /** + * Number of holes is fixed at zero. + */ + private static final int NUM_HOLES = 0; + + /** + * Input stream from which to read BWT data. + */ + private final PrintStream out; + + /** + * Create a new ANNWriter targeting the given file. + * @param file file into which ANN data should be written. + * @throws java.io.IOException if there is a problem opening the output file. + */ + public AMBWriter(File file) throws IOException { + out = new PrintStream(file); + } + + /** + * Create a new ANNWriter targeting the given OutputStream. + * @param stream Stream into which ANN data should be written. + */ + public AMBWriter(OutputStream stream) { + out = new PrintStream(stream); + } + + /** + * Write the contents of the given dictionary into the AMB file. + * Assumes that there are no holes in the dictionary. + * @param dictionary Dictionary to write. + */ + public void writeEmpty(SAMSequenceDictionary dictionary) { + long genomeLength = 0L; + for(SAMSequenceRecord sequence: dictionary.getSequences()) + genomeLength += sequence.getSequenceLength(); + + int sequences = dictionary.getSequences().size(); + + // Write the header + out.printf("%d %d %d%n",genomeLength,sequences,NUM_HOLES); + } + + /** + * Close the given output stream. + */ + public void close() { + out.close(); + } +} \ No newline at end of file diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/ANNWriter.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/ANNWriter.java new file mode 100644 index 000000000..17296c31c --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/ANNWriter.java @@ -0,0 +1,95 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import net.sf.samtools.SAMSequenceDictionary; +import net.sf.samtools.SAMSequenceRecord; + +import java.io.PrintStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Writes .ann files - an alternate sequence dictionary format + * used by BWA/C. For best results, the input sequence dictionary + * should be created with Picard's CreateSequenceDictionary.jar, + * TRUNCATE_NAMES_AT_WHITESPACE=false. + * + * @author mhanna + * @version 0.1 + */ +public class ANNWriter { + /** + * BWA uses a fixed seed of 11, written into every file. + */ + private static final int BNS_SEED = 11; + + /** + * A seemingly unused value that appears in every contig in the ANN. + */ + private static final int GI = 0; + + /** + * Input stream from which to read BWT data. + */ + private final PrintStream out; + + /** + * Create a new ANNWriter targeting the given file. + * @param file file into which ANN data should be written. + * @throws IOException if there is a problem opening the output file. + */ + public ANNWriter(File file) throws IOException { + out = new PrintStream(file); + } + + /** + * Create a new ANNWriter targeting the given OutputStream. + * @param stream Stream into which ANN data should be written. + */ + public ANNWriter(OutputStream stream) { + out = new PrintStream(stream); + } + + /** + * Write the contents of the given dictionary into the ANN file. + * Assumes that no ambs (blocks of indeterminate base) are present in the dictionary. + * @param dictionary Dictionary to write. + */ + public void write(SAMSequenceDictionary dictionary) { + long genomeLength = 0L; + for(SAMSequenceRecord sequence: dictionary.getSequences()) + genomeLength += sequence.getSequenceLength(); + + int sequences = dictionary.getSequences().size(); + + // Write the header + out.printf("%d %d %d%n",genomeLength,sequences,BNS_SEED); + + for(SAMSequenceRecord sequence: dictionary.getSequences()) { + String fullSequenceName = sequence.getSequenceName(); + String trimmedSequenceName = fullSequenceName; + String sequenceComment = "(null)"; + + long offset = 0; + + // Separate the sequence name from the sequence comment, based on BWA's definition. + // BWA's definition appears to accept a zero-length contig name, so mimic that behavior. + if(fullSequenceName.indexOf(' ') >= 0) { + trimmedSequenceName = fullSequenceName.substring(0,fullSequenceName.indexOf(' ')); + sequenceComment = fullSequenceName.substring(fullSequenceName.indexOf(' ')+1); + } + + // Write the sequence GI (?), name, and comment. + out.printf("%d %s %s%n",GI,trimmedSequenceName,sequenceComment); + // Write the sequence offset, length, and ambs (currently fixed at 0). + out.printf("%d %d %d%n",offset,sequence.getSequenceLength(),0); + } + } + + /** + * Close the given output stream. + */ + public void close() { + out.close(); + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWT.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWT.java new file mode 100644 index 000000000..7f8c48253 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWT.java @@ -0,0 +1,172 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import org.broadinstitute.sting.alignment.reference.packing.PackUtils; +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; + +/** + * Represents the Burrows-Wheeler Transform of a reference sequence. + * + * @author mhanna + * @version 0.1 + */ +public class BWT { + /** + * Write an occurrence table after every SEQUENCE_BLOCK_SIZE bases. + * For this implementation to behave correctly, SEQUENCE_BLOCK_SIZE % 8 == 0 + */ + public static final int SEQUENCE_BLOCK_SIZE = 128; + + /** + * The inverse SA, used as a placeholder for determining where the special EOL character sits. + */ + protected final long inverseSA0; + + /** + * Cumulative counts for the entire BWT. + */ + protected final Counts counts; + + /** + * The individual sequence blocks, modelling how they appear on disk. + */ + protected final SequenceBlock[] sequenceBlocks; + + /** + * Creates a new BWT with the given inverse SA, counts, and sequence (in ASCII). + * @param inverseSA0 Inverse SA entry for the first element. Will be missing from the BWT sequence. + * @param counts Cumulative count of bases, in A,C,G,T order. + * @param sequenceBlocks The full BWT sequence, sans the '$'. + */ + public BWT( long inverseSA0, Counts counts, SequenceBlock[] sequenceBlocks ) { + this.inverseSA0 = inverseSA0; + this.counts = counts; + this.sequenceBlocks = sequenceBlocks; + } + + /** + * Creates a new BWT with the given inverse SA, occurrences, and sequence (in ASCII). + * @param inverseSA0 Inverse SA entry for the first element. Will be missing from the BWT sequence. + * @param counts Count of bases, in A,C,G,T order. + * @param sequence The full BWT sequence, sans the '$'. + */ + public BWT( long inverseSA0, Counts counts, byte[] sequence ) { + this(inverseSA0,counts,generateSequenceBlocks(sequence)); + } + + /** + * Extract the full sequence from the list of block. + * @return The full BWT string as a byte array. + */ + public byte[] getSequence() { + byte[] sequence = new byte[(int)counts.getTotal()]; + for( SequenceBlock block: sequenceBlocks ) + System.arraycopy(block.sequence,0,sequence,block.sequenceStart,block.sequenceLength); + return sequence; + } + + /** + * Get the total counts of bases lexicographically smaller than the given base, for Ferragina and Manzini's search. + * @param base The base. + * @return Total counts for all bases lexicographically smaller than this base. + */ + public long counts(byte base) { + return counts.getCumulative(base); + } + + /** + * Get the total counts of bases lexicographically smaller than the given base, for Ferragina and Manzini's search. + * @param base The base. + * @param index The position to search within the BWT. + * @return Total counts for all bases lexicographically smaller than this base. + */ + public long occurrences(byte base,long index) { + SequenceBlock block = getSequenceBlock(index); + int position = getSequencePosition(index); + long accumulator = block.occurrences.get(base); + for(int i = 0; i <= position; i++) { + if(base == block.sequence[i]) + accumulator++; + } + return accumulator; + } + + /** + * The number of bases in the BWT as a whole. + * @return Number of bases. + */ + public long length() { + return counts.getTotal(); + } + + /** + * Create a new BWT from the given reference sequence. + * @param referenceSequence Sequence from which to derive the BWT. + * @return reference sequence-derived BWT. + */ + public static BWT createFromReferenceSequence(byte[] referenceSequence) { + SuffixArray suffixArray = SuffixArray.createFromReferenceSequence(referenceSequence); + + byte[] bwt = new byte[(int)suffixArray.length()-1]; + int bwtIndex = 0; + for(long suffixArrayIndex = 0; suffixArrayIndex < suffixArray.length(); suffixArrayIndex++) { + if(suffixArray.get(suffixArrayIndex) == 0) + continue; + bwt[bwtIndex++] = referenceSequence[(int)suffixArray.get(suffixArrayIndex)-1]; + } + + return new BWT(suffixArray.inverseSA0,suffixArray.occurrences,bwt); + } + + /** + * Gets the base at a given position in the BWT. + * @param index The index to use. + * @return The base at that location. + */ + protected byte getBase(long index) { + if(index == inverseSA0) + throw new ReviewedStingException(String.format("Base at index %d does not have a text representation",index)); + + SequenceBlock block = getSequenceBlock(index); + int position = getSequencePosition(index); + return block.sequence[position]; + } + + private SequenceBlock getSequenceBlock(long index) { + // If the index is above the SA-1[0], remap it to the appropriate coordinate space. + if(index > inverseSA0) index--; + return sequenceBlocks[(int)(index/SEQUENCE_BLOCK_SIZE)]; + } + + private int getSequencePosition(long index) { + // If the index is above the SA-1[0], remap it to the appropriate coordinate space. + if(index > inverseSA0) index--; + return (int)(index%SEQUENCE_BLOCK_SIZE); + } + + /** + * Create a set of sequence blocks from one long sequence. + * @param sequence Sequence from which to derive blocks. + * @return Array of sequence blocks containing data from the sequence. + */ + private static SequenceBlock[] generateSequenceBlocks( byte[] sequence ) { + Counts occurrences = new Counts(); + + int numSequenceBlocks = PackUtils.numberOfPartitions(sequence.length,SEQUENCE_BLOCK_SIZE); + SequenceBlock[] sequenceBlocks = new SequenceBlock[numSequenceBlocks]; + + for( int block = 0; block < numSequenceBlocks; block++ ) { + int blockStart = block*SEQUENCE_BLOCK_SIZE; + int blockLength = Math.min(SEQUENCE_BLOCK_SIZE, sequence.length-blockStart); + byte[] subsequence = new byte[blockLength]; + + System.arraycopy(sequence,blockStart,subsequence,0,blockLength); + + sequenceBlocks[block] = new SequenceBlock(blockStart,blockLength,occurrences.clone(),subsequence); + + for( byte base: subsequence ) + occurrences.increment(base); + } + + return sequenceBlocks; + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTReader.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTReader.java new file mode 100644 index 000000000..64a595419 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTReader.java @@ -0,0 +1,86 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.alignment.reference.packing.UnsignedIntPackedInputStream; +import org.broadinstitute.sting.alignment.reference.packing.BasePackedInputStream; +import org.broadinstitute.sting.alignment.reference.packing.PackUtils; + +import java.io.*; +import java.nio.ByteOrder; +/** + * Reads a BWT from a given file. + * + * @author mhanna + * @version 0.1 + */ +public class BWTReader { + /** + * Input stream from which to read BWT data. + */ + private FileInputStream inputStream; + + /** + * Create a new BWT reader. + * @param inputFile File in which the BWT is stored. + */ + public BWTReader( File inputFile ) { + try { + this.inputStream = new FileInputStream(inputFile); + } + catch( FileNotFoundException ex ) { + throw new ReviewedStingException("Unable to open input file", ex); + } + } + + /** + * Read a BWT from the input stream. + * @return The BWT stored in the input stream. + */ + public BWT read() { + UnsignedIntPackedInputStream uintPackedInputStream = new UnsignedIntPackedInputStream(inputStream, ByteOrder.LITTLE_ENDIAN); + BasePackedInputStream basePackedInputStream = new BasePackedInputStream(Integer.class, inputStream, ByteOrder.LITTLE_ENDIAN); + + long inverseSA0; + long[] count; + SequenceBlock[] sequenceBlocks; + + try { + inverseSA0 = uintPackedInputStream.read(); + count = new long[PackUtils.ALPHABET_SIZE]; + uintPackedInputStream.read(count); + + long bwtSize = count[PackUtils.ALPHABET_SIZE-1]; + sequenceBlocks = new SequenceBlock[PackUtils.numberOfPartitions(bwtSize,BWT.SEQUENCE_BLOCK_SIZE)]; + + for( int block = 0; block < sequenceBlocks.length; block++ ) { + int sequenceStart = block* BWT.SEQUENCE_BLOCK_SIZE; + int sequenceLength = (int)Math.min(BWT.SEQUENCE_BLOCK_SIZE,bwtSize-sequenceStart); + + long[] occurrences = new long[PackUtils.ALPHABET_SIZE]; + byte[] bwt = new byte[sequenceLength]; + + uintPackedInputStream.read(occurrences); + basePackedInputStream.read(bwt); + + sequenceBlocks[block] = new SequenceBlock(sequenceStart,sequenceLength,new Counts(occurrences,false),bwt); + } + } + catch( IOException ex ) { + throw new ReviewedStingException("Unable to read BWT from input stream.", ex); + } + + return new BWT(inverseSA0, new Counts(count,true), sequenceBlocks); + } + + /** + * Close the input stream. + */ + public void close() { + try { + inputStream.close(); + } + catch( IOException ex ) { + throw new ReviewedStingException("Unable to close input file", ex); + } + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTSupplementaryFileGenerator.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTSupplementaryFileGenerator.java new file mode 100644 index 000000000..f24baf766 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTSupplementaryFileGenerator.java @@ -0,0 +1,60 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import net.sf.picard.reference.ReferenceSequenceFileFactory; +import net.sf.picard.reference.ReferenceSequenceFile; +import net.sf.samtools.SAMSequenceDictionary; + +import java.io.File; +import java.io.IOException; + +/** + * Generate BWA supplementary files (.ann, .amb) from the command line. + * + * @author mhanna + * @version 0.1 + */ +public class BWTSupplementaryFileGenerator { + enum SupplementaryFileType { ANN, AMB } + + public static void main(String[] args) throws IOException { + if(args.length < 3) + usage("Incorrect number of arguments supplied"); + + File fastaFile = new File(args[0]); + File outputFile = new File(args[1]); + SupplementaryFileType outputType = null; + try { + outputType = Enum.valueOf(SupplementaryFileType.class,args[2]); + } + catch(IllegalArgumentException ex) { + usage("Invalid output type: " + args[2]); + } + + ReferenceSequenceFile sequenceFile = ReferenceSequenceFileFactory.getReferenceSequenceFile(fastaFile); + SAMSequenceDictionary dictionary = sequenceFile.getSequenceDictionary(); + + switch(outputType) { + case ANN: + ANNWriter annWriter = new ANNWriter(outputFile); + annWriter.write(dictionary); + annWriter.close(); + break; + case AMB: + AMBWriter ambWriter = new AMBWriter(outputFile); + ambWriter.writeEmpty(dictionary); + ambWriter.close(); + break; + default: + usage("Unsupported output type: " + outputType); + } + } + + /** + * Print usage information and exit. + */ + private static void usage(String message) { + System.err.println(message); + System.err.println("Usage: BWTSupplementaryFileGenerator "); + System.exit(1); + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTWriter.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTWriter.java new file mode 100644 index 000000000..b3867ebfe --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/BWTWriter.java @@ -0,0 +1,71 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.alignment.reference.packing.UnsignedIntPackedOutputStream; +import org.broadinstitute.sting.alignment.reference.packing.BasePackedOutputStream; + +import java.io.*; +import java.nio.ByteOrder; + +/** + * Writes an in-memory BWT to an outputstream. + * + * @author mhanna + * @version 0.1 + */ +public class BWTWriter { + /** + * Input stream from which to read BWT data. + */ + private final OutputStream outputStream; + + /** + * Create a new BWT writer. + * @param outputFile File in which the BWT is stored. + */ + public BWTWriter( File outputFile ) { + try { + this.outputStream = new BufferedOutputStream(new FileOutputStream(outputFile)); + } + catch( FileNotFoundException ex ) { + throw new ReviewedStingException("Unable to open output file", ex); + } + } + + /** + * Write a BWT to the output stream. + * @param bwt Transform to be written to the output stream. + */ + public void write( BWT bwt ) { + UnsignedIntPackedOutputStream intPackedOutputStream = new UnsignedIntPackedOutputStream(outputStream, ByteOrder.LITTLE_ENDIAN); + BasePackedOutputStream basePackedOutputStream = new BasePackedOutputStream(Integer.class, outputStream, ByteOrder.LITTLE_ENDIAN); + + try { + intPackedOutputStream.write(bwt.inverseSA0); + intPackedOutputStream.write(bwt.counts.toArray(true)); + + for( SequenceBlock block: bwt.sequenceBlocks ) { + intPackedOutputStream.write(block.occurrences.toArray(false)); + basePackedOutputStream.write(block.sequence); + } + + // The last block is the last set of counts in the structure. + intPackedOutputStream.write(bwt.counts.toArray(false)); + } + catch( IOException ex ) { + throw new ReviewedStingException("Unable to read BWT from input stream.", ex); + } + } + + /** + * Close the input stream. + */ + public void close() { + try { + outputStream.close(); + } + catch( IOException ex ) { + throw new ReviewedStingException("Unable to close input file", ex); + } + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/Bases.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/Bases.java new file mode 100644 index 000000000..bc0a5b63d --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/Bases.java @@ -0,0 +1,108 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; + +import java.util.*; + +/** + * Enhanced enum representation of a base. + * + * @author mhanna + * @version 0.1 + */ +public class Bases implements Iterable +{ + public static byte A = 'A'; + public static byte C = 'C'; + public static byte G = 'G'; + public static byte T = 'T'; + + public static final Bases instance = new Bases(); + + private static final List allBases; + + /** + * Representation of the base broken down by packed value. + */ + private static final Map basesByPack = new HashMap(); + + static { + List bases = new ArrayList(); + bases.add(A); + bases.add(C); + bases.add(G); + bases.add(T); + allBases = Collections.unmodifiableList(bases); + + for(int i = 0; i < allBases.size(); i++) + basesByPack.put(i,allBases.get(i)); + } + + /** + * Create a new base with the given ascii representation and + * pack value. + */ + private Bases() { + } + + /** + * Return all possible bases. + * @return Byte representation of all bases. + */ + public static Collection allOf() { + return allBases; + } + + /** + * Gets the number of known bases. + * @return The number of known bases. + */ + public static int size() { + return allBases.size(); + } + + /** + * Gets an iterator over the total number of known base types. + * @return Iterator over all known bases. + */ + public Iterator iterator() { + return basesByPack.values().iterator(); + } + + /** + * Get the given base from the packed representation. + * @param pack Packed representation. + * @return base. + */ + public static byte fromPack( int pack ) { return basesByPack.get(pack); } + + /** + * Convert the given base to its packed value. + * @param ascii ASCII representation of the base. + * @return Packed value. + */ + public static int toPack( byte ascii ) + { + for( Map.Entry entry: basesByPack.entrySet() ) { + if( entry.getValue().equals(ascii) ) + return entry.getKey(); + } + throw new ReviewedStingException(String.format("Base %c is an invalid base to pack", (char)ascii)); + } + + /** + * Convert the ASCII representation of a base to its 'normalized' representation. + * @param base The base itself. + * @return The byte, if present. Null if unknown. + */ + public static Byte fromASCII( byte base ) { + Byte found = null; + for( Byte normalized: allBases ) { + if( normalized.equals(base) ) { + found = normalized; + break; + } + } + return found; + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/Counts.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/Counts.java new file mode 100644 index 000000000..268b11ac4 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/Counts.java @@ -0,0 +1,151 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; + +import java.util.HashMap; +import java.util.Map; + +/** + * Counts of how many bases of each type have been seen. + * + * @author mhanna + * @version 0.1 + */ +public class Counts implements Cloneable { + /** + * Internal representation of counts, broken down by ASCII value. + */ + private Map counts = new HashMap(); + + /** + * Internal representation of cumulative counts, broken down by ASCII value. + */ + private Map cumulativeCounts = new HashMap(); + + /** + * Create an empty Counts object with values A=0,C=0,G=0,T=0. + */ + public Counts() + { + for(byte base: Bases.instance) { + counts.put(base,0L); + cumulativeCounts.put(base,0L); + } + } + + /** + * Create a counts data structure with the given initial values. + * @param data Count data, broken down by base. + * @param cumulative Whether the counts are cumulative, (count_G=numA+numC+numG,for example). + */ + public Counts( long[] data, boolean cumulative ) { + if(cumulative) { + long priorCount = 0; + for(byte base: Bases.instance) { + long count = data[Bases.toPack(base)]; + counts.put(base,count-priorCount); + cumulativeCounts.put(base,priorCount); + priorCount = count; + } + } + else { + long priorCount = 0; + for(byte base: Bases.instance) { + long count = data[Bases.toPack(base)]; + counts.put(base,count); + cumulativeCounts.put(base,priorCount); + priorCount += count; + } + } + } + + /** + * Convert to an array for persistence. + * @param cumulative Use a cumulative representation. + * @return Array of count values. + */ + public long[] toArray(boolean cumulative) { + long[] countArray = new long[counts.size()]; + if(cumulative) { + int index = 0; + boolean first = true; + for(byte base: Bases.instance) { + if(first) { + first = false; + continue; + } + countArray[index++] = getCumulative(base); + } + countArray[countArray.length-1] = getTotal(); + } + else { + int index = 0; + for(byte base: Bases.instance) + countArray[index++] = counts.get(base); + } + return countArray; + } + + /** + * Create a unique copy of the current object. + * @return A duplicate of this object. + */ + public Counts clone() { + Counts other; + try { + other = (Counts)super.clone(); + } + catch(CloneNotSupportedException ex) { + throw new ReviewedStingException("Unable to clone counts object", ex); + } + other.counts = new HashMap(counts); + other.cumulativeCounts = new HashMap(cumulativeCounts); + return other; + } + + /** + * Increment the number of bases seen at the given location. + * @param base Base to increment. + */ + public void increment(byte base) { + counts.put(base,counts.get(base)+1); + boolean increment = false; + for(byte cumulative: Bases.instance) { + if(increment) cumulativeCounts.put(cumulative,cumulativeCounts.get(cumulative)+1); + increment |= (cumulative == base); + } + } + + /** + * Gets a count of the number of bases seen at a given location. + * Note that counts in this case are not cumulative (counts for A,C,G,T + * are independent). + * @param base Base for which to query counts. + * @return Number of bases of this type seen. + */ + public long get(byte base) { + return counts.get(base); + } + + /** + * Gets a count of the number of bases seen before this base. + * Note that counts in this case are cumulative. + * @param base Base for which to query counts. + * @return Number of bases of this type seen. + */ + public long getCumulative(byte base) { + return cumulativeCounts.get(base); + } + + /** + * How many total bases are represented by this count structure? + * @return Total bases represented. + */ + public long getTotal() { + int accumulator = 0; + for(byte base: Bases.instance) { + accumulator += get(base); + } + return accumulator; + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/CreateBWTFromReference.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/CreateBWTFromReference.java new file mode 100755 index 000000000..92bb713f0 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/CreateBWTFromReference.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2009 The Broad Institute + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITHoc THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.broadinstitute.sting.alignment.reference.bwt; + +import net.sf.picard.reference.ReferenceSequenceFile; +import net.sf.picard.reference.ReferenceSequenceFileFactory; +import net.sf.picard.reference.ReferenceSequence; + +import java.io.*; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.alignment.reference.packing.PackUtils; + +/** + * Create a suffix array data structure. + * + * @author mhanna + * @version 0.1 + */ +public class CreateBWTFromReference { + private byte[] loadReference( File inputFile ) { + // Read in the first sequence in the input file + ReferenceSequenceFile reference = ReferenceSequenceFileFactory.getReferenceSequenceFile(inputFile); + ReferenceSequence sequence = reference.nextSequence(); + return sequence.getBases(); + } + + private byte[] loadReverseReference( File inputFile ) { + ReferenceSequenceFile reference = ReferenceSequenceFileFactory.getReferenceSequenceFile(inputFile); + ReferenceSequence sequence = reference.nextSequence(); + PackUtils.reverse(sequence.getBases()); + return sequence.getBases(); + } + + private Counts countOccurrences( byte[] sequence ) { + Counts occurrences = new Counts(); + for( byte base: sequence ) + occurrences.increment(base); + return occurrences; + } + + private long[] createSuffixArray( byte[] sequence ) { + return SuffixArray.createFromReferenceSequence(sequence).sequence; + } + + private long[] invertSuffixArray( long[] suffixArray ) { + long[] inverseSuffixArray = new long[suffixArray.length]; + for( int i = 0; i < suffixArray.length; i++ ) + inverseSuffixArray[(int)suffixArray[i]] = i; + return inverseSuffixArray; + } + + private long[] createCompressedSuffixArray( int[] suffixArray, int[] inverseSuffixArray ) { + long[] compressedSuffixArray = new long[suffixArray.length]; + compressedSuffixArray[0] = inverseSuffixArray[0]; + for( int i = 1; i < suffixArray.length; i++ ) + compressedSuffixArray[i] = inverseSuffixArray[suffixArray[i]+1]; + return compressedSuffixArray; + } + + private long[] createInversedCompressedSuffixArray( int[] compressedSuffixArray ) { + long[] inverseCompressedSuffixArray = new long[compressedSuffixArray.length]; + for( int i = 0; i < compressedSuffixArray.length; i++ ) + inverseCompressedSuffixArray[compressedSuffixArray[i]] = i; + return inverseCompressedSuffixArray; + } + + public static void main( String argv[] ) throws IOException { + if( argv.length != 5 ) { + System.out.println("USAGE: CreateBWTFromReference .fasta "); + return; + } + + String inputFileName = argv[0]; + File inputFile = new File(inputFileName); + + String bwtFileName = argv[1]; + File bwtFile = new File(bwtFileName); + + String rbwtFileName = argv[2]; + File rbwtFile = new File(rbwtFileName); + + String saFileName = argv[3]; + File saFile = new File(saFileName); + + String rsaFileName = argv[4]; + File rsaFile = new File(rsaFileName); + + CreateBWTFromReference creator = new CreateBWTFromReference(); + + byte[] sequence = creator.loadReference(inputFile); + byte[] reverseSequence = creator.loadReverseReference(inputFile); + + // Count the occurences of each given base. + Counts occurrences = creator.countOccurrences(sequence); + System.out.printf("Occurrences: a=%d, c=%d, g=%d, t=%d%n",occurrences.getCumulative(Bases.A), + occurrences.getCumulative(Bases.C), + occurrences.getCumulative(Bases.G), + occurrences.getCumulative(Bases.T)); + + // Generate the suffix array and print diagnostics. + long[] suffixArrayData = creator.createSuffixArray(sequence); + long[] reverseSuffixArrayData = creator.createSuffixArray(reverseSequence); + + // Invert the suffix array and print diagnostics. + long[] inverseSuffixArray = creator.invertSuffixArray(suffixArrayData); + long[] reverseInverseSuffixArray = creator.invertSuffixArray(reverseSuffixArrayData); + + SuffixArray suffixArray = new SuffixArray( inverseSuffixArray[0], occurrences, suffixArrayData ); + SuffixArray reverseSuffixArray = new SuffixArray( reverseInverseSuffixArray[0], occurrences, reverseSuffixArrayData ); + + /* + // Create the data structure for the compressed suffix array and print diagnostics. + int[] compressedSuffixArray = creator.createCompressedSuffixArray(suffixArray.sequence,inverseSuffixArray); + int reconstructedInverseSA = compressedSuffixArray[0]; + for( int i = 0; i < 8; i++ ) { + System.out.printf("compressedSuffixArray[%d] = %d (SA-1[%d] = %d)%n", i, compressedSuffixArray[i], i, reconstructedInverseSA); + reconstructedInverseSA = compressedSuffixArray[reconstructedInverseSA]; + } + + // Create the data structure for the inverse compressed suffix array and print diagnostics. + int[] inverseCompressedSuffixArray = creator.createInversedCompressedSuffixArray(compressedSuffixArray); + for( int i = 0; i < 8; i++ ) { + System.out.printf("inverseCompressedSuffixArray[%d] = %d%n", i, inverseCompressedSuffixArray[i]); + } + */ + + // Create the BWT. + BWT bwt = BWT.createFromReferenceSequence(sequence); + BWT reverseBWT = BWT.createFromReferenceSequence(reverseSequence); + + byte[] bwtSequence = bwt.getSequence(); + System.out.printf("BWT: %s... (length = %d)%n", new String(bwtSequence,0,80),bwt.length()); + + BWTWriter bwtWriter = new BWTWriter(bwtFile); + bwtWriter.write(bwt); + bwtWriter.close(); + + BWTWriter reverseBWTWriter = new BWTWriter(rbwtFile); + reverseBWTWriter.write(reverseBWT); + reverseBWTWriter.close(); + + /* + SuffixArrayWriter saWriter = new SuffixArrayWriter(saFile); + saWriter.write(suffixArray); + saWriter.close(); + + SuffixArrayWriter reverseSAWriter = new SuffixArrayWriter(rsaFile); + reverseSAWriter.write(reverseSuffixArray); + reverseSAWriter.close(); + */ + + File existingBWTFile = new File(inputFileName+".bwt"); + BWTReader existingBWTReader = new BWTReader(existingBWTFile); + BWT existingBWT = existingBWTReader.read(); + + byte[] existingBWTSequence = existingBWT.getSequence(); + System.out.printf("Existing BWT: %s... (length = %d)%n",new String(existingBWTSequence,0,80),existingBWT.length()); + + for( int i = 0; i < bwt.length(); i++ ) { + if( bwtSequence[i] != existingBWTSequence[i] ) + throw new ReviewedStingException("BWT mismatch at " + i); + } + + File existingSAFile = new File(inputFileName+".sa"); + SuffixArrayReader existingSuffixArrayReader = new SuffixArrayReader(existingSAFile,existingBWT); + SuffixArray existingSuffixArray = existingSuffixArrayReader.read(); + + for(int i = 0; i < suffixArray.length(); i++) { + if( i % 10000 == 0 ) + System.out.printf("Validating suffix array entry %d%n", i); + if( suffixArray.get(i) != existingSuffixArray.get(i) ) + throw new ReviewedStingException(String.format("Suffix array mismatch at %d; SA is %d; should be %d",i,existingSuffixArray.get(i),suffixArray.get(i))); + } + } + +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SequenceBlock.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SequenceBlock.java new file mode 100644 index 000000000..13714de1e --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SequenceBlock.java @@ -0,0 +1,41 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +/** + * Models a block of bases within the BWT. + */ +public class SequenceBlock { + /** + * Start position of this sequence within the BWT. + */ + public final int sequenceStart; + + /** + * Length of this sequence within the BWT. + */ + public final int sequenceLength; + + + /** + * Occurrences of each letter up to this sequence block. + */ + public final Counts occurrences; + + /** + * Sequence for this segment. + */ + public final byte[] sequence; + + /** + * Create a new block within this BWT. + * @param sequenceStart Starting position of this sequence within the BWT. + * @param sequenceLength Length of this sequence. + * @param occurrences How many of each base has been seen before this sequence began. + * @param sequence The actual sequence from the BWT. + */ + public SequenceBlock( int sequenceStart, int sequenceLength, Counts occurrences, byte[] sequence ) { + this.sequenceStart = sequenceStart; + this.sequenceLength = sequenceLength; + this.occurrences = occurrences; + this.sequence = sequence; + } +} \ No newline at end of file diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArray.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArray.java new file mode 100644 index 000000000..dba3633d1 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArray.java @@ -0,0 +1,159 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; + +import java.util.Comparator; +import java.util.TreeSet; + +import net.sf.samtools.util.StringUtil; + +/** + * An in-memory representation of a suffix array. + * + * @author mhanna + * @version 0.1 + */ +public class SuffixArray { + public final long inverseSA0; + public final Counts occurrences; + + /** + * The elements of the sequence actually stored in memory. + */ + protected final long[] sequence; + + /** + * How often are individual elements in the sequence actually stored + * in memory, as opposed to being calculated on the fly? + */ + protected final int sequenceInterval; + + /** + * The BWT used to calculate missing portions of the sequence. + */ + protected final BWT bwt; + + public SuffixArray(long inverseSA0, Counts occurrences, long[] sequence) { + this(inverseSA0,occurrences,sequence,1,null); + } + + /** + * Creates a new sequence array with the given inverse SA, occurrences, and values. + * @param inverseSA0 Inverse SA entry for the first element. + * @param occurrences Cumulative number of occurrences of A,C,G,T, in order. + * @param sequence The full suffix array. + * @param sequenceInterval How frequently is the sequence interval stored. + * @param bwt bwt used to infer the remaining entries in the BWT. + */ + public SuffixArray(long inverseSA0, Counts occurrences, long[] sequence, int sequenceInterval, BWT bwt) { + this.inverseSA0 = inverseSA0; + this.occurrences = occurrences; + this.sequence = sequence; + this.sequenceInterval = sequenceInterval; + this.bwt = bwt; + + if(sequenceInterval != 1 && bwt == null) + throw new ReviewedStingException("A BWT must be provided if the sequence interval is not 1"); + } + + /** + * Retrieves the length of the sequence array. + * @return Length of the suffix array. + */ + public long length() { + if( bwt != null ) + return bwt.length()+1; + else + return sequence.length; + } + + /** + * Get the suffix array value at a given sequence. + * @param index Index at which to retrieve the suffix array vaule. + * @return The suffix array value at that entry. + */ + public long get(long index) { + int iterations = 0; + while(index%sequenceInterval != 0) { + // The inverseSA0 ('$') doesn't have a usable ASCII representation; it must be treated as a special case. + if(index == inverseSA0) + index = 0; + else { + byte base = bwt.getBase(index); + index = bwt.counts(base) + bwt.occurrences(base,index); + } + iterations++; + } + return (sequence[(int)(index/sequenceInterval)]+iterations) % length(); + } + + /** + * Create a suffix array from a given reference sequence. + * @param sequence The reference sequence to use when building the suffix array. + * @return a constructed suffix array. + */ + public static SuffixArray createFromReferenceSequence(byte[] sequence) { + // The builder for the suffix array. Use an integer in this case because + // Java arrays can only hold an integer. + TreeSet suffixArrayBuilder = new TreeSet(new SuffixArrayComparator(sequence)); + + Counts occurrences = new Counts(); + for( byte base: sequence ) + occurrences.increment(base); + + // Build out the suffix array using a custom comparator. + for( int i = 0; i <= sequence.length; i++ ) + suffixArrayBuilder.add(i); + + // Copy the suffix array into an array. + long[] suffixArray = new long[suffixArrayBuilder.size()]; + int i = 0; + for( Integer element: suffixArrayBuilder ) + suffixArray[i++] = element; + + // Find the first element in the inverse suffix array. + long inverseSA0 = -1; + for(i = 0; i < suffixArray.length; i++) { + if(suffixArray[i] == 0) + inverseSA0 = i; + } + if(inverseSA0 < 0) + throw new ReviewedStingException("Unable to find first inverse SA entry in generated suffix array."); + + return new SuffixArray(inverseSA0,occurrences,suffixArray); + } + + /** + * Compares two suffix arrays of the given sequence. Will return whichever string appears + * first in lexicographic order. + */ + private static class SuffixArrayComparator implements Comparator { + /** + * The data source for all suffix arrays. + */ + private final String sequence; + + /** + * Create a new comparator. + * @param sequence Reference sequence to use as basis for comparison. + */ + public SuffixArrayComparator( byte[] sequence ) { + // Processing the suffix array tends to be easier as a string. + this.sequence = StringUtil.bytesToString(sequence); + } + + /** + * Compare the two given suffix arrays. Criteria for comparison is the lexicographic order of + * the two substrings sequence[lhs:], sequence[rhs:]. + * @param lhs Left-hand side of comparison. + * @param rhs Right-hand side of comparison. + * @return How the suffix arrays represented by lhs, rhs compare. + */ + public int compare( Integer lhs, Integer rhs ) { + String lhsSuffixArray = sequence.substring(lhs); + String rhsSuffixArray = sequence.substring(rhs); + return lhsSuffixArray.compareTo(rhsSuffixArray); + } + } + +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArrayReader.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArrayReader.java new file mode 100644 index 000000000..c10984145 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArrayReader.java @@ -0,0 +1,82 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.alignment.reference.packing.UnsignedIntPackedInputStream; +import org.broadinstitute.sting.alignment.reference.packing.PackUtils; + +import java.io.*; +import java.nio.ByteOrder; + +/** + * A reader for suffix arrays in permanent storage. + * + * @author mhanna + * @version 0.1 + */ +public class SuffixArrayReader { + /** + * Input stream from which to read suffix array data. + */ + private FileInputStream inputStream; + + /** + * BWT to use to fill in missing data. + */ + private BWT bwt; + + /** + * Create a new suffix array reader. + * @param inputFile File in which the suffix array is stored. + * @param bwt BWT to use when filling in missing data. + */ + public SuffixArrayReader(File inputFile, BWT bwt) { + try { + this.inputStream = new FileInputStream(inputFile); + this.bwt = bwt; + } + catch( FileNotFoundException ex ) { + throw new ReviewedStingException("Unable to open input file", ex); + } + } + + /** + * Read a suffix array from the input stream. + * @return The suffix array stored in the input stream. + */ + public SuffixArray read() { + UnsignedIntPackedInputStream uintPackedInputStream = new UnsignedIntPackedInputStream(inputStream, ByteOrder.LITTLE_ENDIAN); + + long inverseSA0; + long[] occurrences; + long[] suffixArray; + int suffixArrayInterval; + + try { + inverseSA0 = uintPackedInputStream.read(); + occurrences = new long[PackUtils.ALPHABET_SIZE]; + uintPackedInputStream.read(occurrences); + // Throw away the suffix array size in bytes and use the occurrences table directly. + suffixArrayInterval = (int)uintPackedInputStream.read(); + suffixArray = new long[(int)((occurrences[occurrences.length-1]+suffixArrayInterval-1)/suffixArrayInterval)]; + uintPackedInputStream.read(suffixArray); + } + catch( IOException ex ) { + throw new ReviewedStingException("Unable to read BWT from input stream.", ex); + } + + return new SuffixArray(inverseSA0, new Counts(occurrences,true), suffixArray, suffixArrayInterval, bwt); + } + + + /** + * Close the input stream. + */ + public void close() { + try { + inputStream.close(); + } + catch( IOException ex ) { + throw new ReviewedStingException("Unable to close input file", ex); + } + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArrayWriter.java b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArrayWriter.java new file mode 100644 index 000000000..972fc2a15 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/bwt/SuffixArrayWriter.java @@ -0,0 +1,67 @@ +package org.broadinstitute.sting.alignment.reference.bwt; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; +import org.broadinstitute.sting.alignment.reference.packing.UnsignedIntPackedOutputStream; + +import java.io.*; +import java.nio.ByteOrder; + +/** + * Javadoc goes here. + * + * @author mhanna + * @version 0.1 + */ +public class SuffixArrayWriter { + /** + * Input stream from which to read suffix array data. + */ + private OutputStream outputStream; + + /** + * Create a new suffix array reader. + * @param outputFile File in which the suffix array is stored. + */ + public SuffixArrayWriter( File outputFile ) { + try { + this.outputStream = new BufferedOutputStream(new FileOutputStream(outputFile)); + } + catch( FileNotFoundException ex ) { + throw new ReviewedStingException("Unable to open input file", ex); + } + } + + /** + * Write a suffix array to the output stream. + * @param suffixArray suffix array to write. + */ + public void write(SuffixArray suffixArray) { + UnsignedIntPackedOutputStream uintPackedOutputStream = new UnsignedIntPackedOutputStream(outputStream, ByteOrder.LITTLE_ENDIAN); + + try { + uintPackedOutputStream.write(suffixArray.inverseSA0); + uintPackedOutputStream.write(suffixArray.occurrences.toArray(true)); + // How frequently the suffix array entry is placed. + uintPackedOutputStream.write(1); + // Length of the suffix array. + uintPackedOutputStream.write(suffixArray.length()-1); + uintPackedOutputStream.write(suffixArray.sequence,1,suffixArray.sequence.length-1); + } + catch( IOException ex ) { + throw new ReviewedStingException("Unable to read BWT from input stream.", ex); + } + } + + + /** + * Close the input stream. + */ + public void close() { + try { + outputStream.close(); + } + catch( IOException ex ) { + throw new ReviewedStingException("Unable to close input file", ex); + } + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/packing/BasePackedInputStream.java b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/BasePackedInputStream.java new file mode 100644 index 000000000..6681e37ec --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/BasePackedInputStream.java @@ -0,0 +1,92 @@ +package org.broadinstitute.sting.alignment.reference.packing; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; + +import java.io.*; +import java.nio.ByteOrder; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Reads a packed version of the input stream. + * + * @author mhanna + * @version 0.1 + */ +public class BasePackedInputStream { + /** + * Type of object to unpack. + */ + private final Class type; + + /** + * Ultimate source for packed bases. + */ + private final FileInputStream targetInputStream; + + /** + * Channel source for packed bases. + */ + private final FileChannel targetInputChannel; + + /** + * A fixed-size buffer for word-packed data. + */ + private final ByteOrder byteOrder; + + /** + * How many bases are in a given packed word. + */ + private final int basesPerPackedWord = PackUtils.bitsInType(Integer.class)/PackUtils.BITS_PER_BASE; + + /** + * How many bytes in an integer? + */ + private final int bytesPerInteger = PackUtils.bitsInType(Integer.class)/PackUtils.BITS_PER_BYTE; + + + public BasePackedInputStream( Class type, File inputFile, ByteOrder byteOrder ) throws FileNotFoundException { + this(type,new FileInputStream(inputFile),byteOrder); + } + + public BasePackedInputStream( Class type, FileInputStream inputStream, ByteOrder byteOrder ) { + if( type != Integer.class ) + throw new ReviewedStingException("Only bases packed into 32-bit words are currently supported by this input stream. Type specified: " + type.getName()); + this.type = type; + this.targetInputStream = inputStream; + this.targetInputChannel = inputStream.getChannel(); + this.byteOrder = byteOrder; + } + + /** + * Read the entire contents of the input stream. + * @param bwt array into which bases should be read. + * @throws IOException if an I/O error occurs. + */ + public void read(byte[] bwt) throws IOException { + read(bwt,0,bwt.length); + } + + /** + * Read the next length bases into the bwt array, starting at the given offset. + * @param bwt array holding the given data. + * @param offset target position in the bases array into which bytes should be written. + * @param length number of bases to read from the stream. + * @throws IOException if an I/O error occurs. + */ + public void read(byte[] bwt, int offset, int length) throws IOException { + int bufferWidth = ((bwt.length+basesPerPackedWord-1)/basesPerPackedWord)*bytesPerInteger; + ByteBuffer buffer = ByteBuffer.allocate(bufferWidth).order(byteOrder); + targetInputChannel.read(buffer); + targetInputChannel.position(targetInputChannel.position()+buffer.remaining()); + buffer.flip(); + + int packedWord = 0; + int i = 0; + while(i < length) { + if(i % basesPerPackedWord == 0) packedWord = buffer.getInt(); + int position = basesPerPackedWord - i%basesPerPackedWord - 1; + bwt[offset+i++] = PackUtils.unpackBase((byte)((packedWord >> position*PackUtils.BITS_PER_BASE) & 0x3)); + } + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/packing/BasePackedOutputStream.java b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/BasePackedOutputStream.java new file mode 100644 index 000000000..c62f40e51 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/BasePackedOutputStream.java @@ -0,0 +1,140 @@ +package org.broadinstitute.sting.alignment.reference.packing; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * A general-purpose stream for writing packed bases. + * + * @author mhanna + * @version 0.1 + */ +public class BasePackedOutputStream { + /** + * Type of object to pack. + */ + private final Class type; + + /** + * How many bases can be stored in the given data structure? + */ + private final int basesPerType; + + /** + * Ultimate target for the packed bases. + */ + private final OutputStream targetOutputStream; + + /** + * A fixed-size buffer for word-packed data. + */ + private final ByteBuffer buffer; + + public BasePackedOutputStream( Class type, File outputFile, ByteOrder byteOrder ) throws FileNotFoundException { + this(type,new BufferedOutputStream(new FileOutputStream(outputFile)),byteOrder); + } + + /** + * Write packed bases to the given output stream. + * @param type Type of data to pack bases into. + * @param outputStream Output stream to which to write packed bases. + * @param byteOrder Switch between big endian / little endian when reading / writing files. + */ + public BasePackedOutputStream( Class type, OutputStream outputStream, ByteOrder byteOrder) { + this.targetOutputStream = outputStream; + this.type = type; + basesPerType = PackUtils.bitsInType(type)/PackUtils.BITS_PER_BASE; + this.buffer = ByteBuffer.allocate(basesPerType/PackUtils.ALPHABET_SIZE).order(byteOrder); + } + + /** + * Writes the given base to the output stream. Will write only this base; no packing will be performed. + * @param base List of bases to write. + * @throws IOException if an I/O error occurs. + */ + public void write( int base ) throws IOException { + write( new byte[] { (byte)base } ); + } + + /** + * Writes an array of bases to the target output stream. + * @param bases List of bases to write. + * @throws IOException if an I/O error occurs. + */ + public void write( byte[] bases ) throws IOException { + write(bases,0,bases.length); + } + + /** + * Writes a subset of the array of bases to the output stream. + * @param bases List of bases to write. + * @param offset site at which to start writing. + * @param length number of bases to write. + * @throws IOException if an I/O error occurs. + */ + public void write( byte[] bases, int offset, int length ) throws IOException { + int packedBases = 0; + int positionInPack = 0; + + for( int base = offset; base < offset+length; base++ ) { + packedBases = packBase(bases[base], packedBases, positionInPack); + + // Increment the packed counter. If all possible bases have been squeezed into this byte, write it out. + positionInPack = ++positionInPack % basesPerType; + if( positionInPack == 0 ) { + writePackedBases(packedBases); + packedBases = 0; + } + } + + if( positionInPack > 0 ) + writePackedBases(packedBases); + } + + /** + * Flush the contents of the OutputStream to disk. + * @throws IOException if an I/O error occurs. + */ + public void flush() throws IOException { + targetOutputStream.flush(); + } + + /** + * Closes the given output stream. + * @throws IOException if an I/O error occurs. + */ + public void close() throws IOException { + targetOutputStream.close(); + } + + /** + * Pack the given base into the basepack. + * @param base The base to pack. + * @param basePack Target for the pack operation. + * @param position Position within the pack to which to add the base. + * @return The packed integer. + */ + private int packBase( byte base, int basePack, int position ) { + basePack |= (PackUtils.packBase(base) << 2*(basesPerType-position-1)); + return basePack; + } + + /** + * Write the given packed base structure to the output file. + * @param packedBases Packed bases to write. + * @throws IOException on error writing to the file. + */ + private void writePackedBases(int packedBases) throws IOException { + buffer.rewind(); + if( type == Integer.class ) + buffer.putInt(packedBases); + else if( type == Byte.class ) + buffer.put((byte)packedBases); + else + throw new ReviewedStingException("Cannot pack bases into type " + type.getName()); + targetOutputStream.write(buffer.array()); + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/packing/CreatePACFromReference.java b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/CreatePACFromReference.java new file mode 100755 index 000000000..8211c97d8 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/CreatePACFromReference.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2009 The Broad Institute + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.broadinstitute.sting.alignment.reference.packing; + +import net.sf.picard.reference.ReferenceSequenceFile; +import net.sf.picard.reference.ReferenceSequenceFileFactory; +import net.sf.picard.reference.ReferenceSequence; + +import java.io.*; +import java.nio.ByteOrder; + +/** + * Generate a .PAC file from a given reference. + * + * @author hanna + * @version 0.1 + */ + +public class CreatePACFromReference { + public static void main( String argv[] ) throws IOException { + if( argv.length != 3 ) { + System.out.println("USAGE: CreatePACFromReference .fasta "); + return; + } + + // Read in the first sequence in the input file + String inputFileName = argv[0]; + File inputFile = new File(inputFileName); + ReferenceSequenceFile reference = ReferenceSequenceFileFactory.getReferenceSequenceFile(inputFile); + ReferenceSequence sequence = reference.nextSequence(); + + // Target file for output + PackUtils.writeReferenceSequence( new File(argv[1]), sequence.getBases() ); + + // Reverse the bases in the reference + PackUtils.reverse(sequence.getBases()); + + // Target file for output + PackUtils.writeReferenceSequence( new File(argv[2]), sequence.getBases() ); + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/packing/PackUtils.java b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/PackUtils.java new file mode 100644 index 000000000..beed21b49 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/PackUtils.java @@ -0,0 +1,135 @@ +package org.broadinstitute.sting.alignment.reference.packing; + +import org.broadinstitute.sting.utils.exceptions.ReviewedStingException; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.FileOutputStream; +import java.nio.ByteOrder; + +/** + * Utilities designed for packing / unpacking bases. + * + * @author mhanna + * @version 0.1 + */ +public class PackUtils { + /** + * How many possible bases can be encoded? + */ + public static final int ALPHABET_SIZE = 4; + + /** + * How many bits does it take to store a single base? + */ + public static final int BITS_PER_BASE = (int)(Math.log(ALPHABET_SIZE)/Math.log(2)); + + /** + * How many bits fit into a single byte? + */ + public static final int BITS_PER_BYTE = 8; + + /** + * Writes a reference sequence to a PAC file. + * @param outputFile Filename for the PAC file. + * @param referenceSequence Reference sequence to write. + * @throws IOException If there's a problem writing to the output file. + */ + public static void writeReferenceSequence( File outputFile, byte[] referenceSequence ) throws IOException { + OutputStream outputStream = new FileOutputStream(outputFile); + + BasePackedOutputStream basePackedOutputStream = new BasePackedOutputStream(Byte.class, outputStream, ByteOrder.BIG_ENDIAN); + basePackedOutputStream.write(referenceSequence); + + outputStream.write(referenceSequence.length%PackUtils.ALPHABET_SIZE); + + outputStream.close(); + } + + + /** + * How many bits can a given type hold? + * @param type Type to test. + * @return Number of bits that the given type can hold. + */ + public static int bitsInType( Class type ) { + try { + long typeSize = type.getField("MAX_VALUE").getLong(null) - type.getField("MIN_VALUE").getLong(null)+1; + long intTypeSize = (long)Integer.MAX_VALUE - (long)Integer.MIN_VALUE + 1; + if( typeSize > intTypeSize ) + throw new ReviewedStingException("Cannot determine number of bits available in type: " + type.getName()); + return (int)(Math.log(typeSize)/Math.log(2)); + } + catch( NoSuchFieldException ex ) { + throw new ReviewedStingException("Cannot determine number of bits available in type: " + type.getName(),ex); + } + catch( IllegalAccessException ex ) { + throw new ReviewedStingException("Cannot determine number of bits available in type: " + type.getName(),ex); + } + } + + /** + * Gets the two-bit representation of a base. A=00b, C=01b, G=10b, T=11b. + * @param base ASCII value for the base to pack. + * @return A byte from 0-3 indicating the base's packed value. + */ + public static byte packBase(byte base) { + switch( base ) { + case 'A': + return 0; + case 'C': + return 1; + case 'G': + return 2; + case 'T': + return 3; + default: + throw new ReviewedStingException("Unknown base type: " + base); + } + } + + /** + * Converts a two-bit representation of a base into an ASCII representation of a base. + * @param pack Byte from 0-3 indicating which base is represented. + * @return An ASCII value representing the packed base. + */ + public static byte unpackBase(byte pack) { + switch( pack ) { + case 0: + return 'A'; + case 1: + return 'C'; + case 2: + return 'G'; + case 3: + return 'T'; + default: + throw new ReviewedStingException("Unknown pack type: " + pack); + } + } + + /** + * Reverses an unpacked sequence of bases. + * @param bases bases to reverse. + */ + public static void reverse( byte[] bases ) { + for( int i = 0, j = bases.length-1; i < j; i++, j-- ) { + byte temp = bases[j]; + bases[j] = bases[i]; + bases[i] = temp; + } + } + + /** + * Given a structure of size size that should be split + * into partitionSize partitions, how many partitions should + * be created? Size of last partition will be <= partitionSize. + * @param size Total size of the data structure. + * @param partitionSize Size of an individual partition. + * @return Number of partitions that would be created. + */ + public static int numberOfPartitions( long size, long partitionSize ) { + return (int)((size+partitionSize-1) / partitionSize); + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/packing/UnsignedIntPackedInputStream.java b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/UnsignedIntPackedInputStream.java new file mode 100644 index 000000000..c07766ee1 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/UnsignedIntPackedInputStream.java @@ -0,0 +1,102 @@ +package org.broadinstitute.sting.alignment.reference.packing; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; + +/** + * Read a set of integers packed into + * + * @author mhanna + * @version 0.1 + */ +public class UnsignedIntPackedInputStream { + /** + * Ultimate target for the occurrence array. + */ + private final FileInputStream targetInputStream; + + /** + * Target channel from which to pull file data. + */ + private final FileChannel targetInputChannel; + + /** + * The byte order in which integer input data appears. + */ + private final ByteOrder byteOrder; + + /** + * How many bytes are required to store an integer? + */ + private final int bytesPerInteger = PackUtils.bitsInType(Integer.class)/PackUtils.BITS_PER_BYTE; + + /** + * Create a new PackedIntInputStream, writing to the given target file. + * @param inputFile target input file. + * @param byteOrder Endianness to use when writing a list of integers. + * @throws java.io.IOException if an I/O error occurs. + */ + public UnsignedIntPackedInputStream(File inputFile, ByteOrder byteOrder) throws IOException { + this(new FileInputStream(inputFile),byteOrder); + } + + /** + * Read ints from the given InputStream. + * @param inputStream Input stream from which to read ints. + * @param byteOrder Endianness to use when writing a list of integers. + */ + public UnsignedIntPackedInputStream(FileInputStream inputStream, ByteOrder byteOrder) { + this.targetInputStream = inputStream; + this.targetInputChannel = inputStream.getChannel(); + this.byteOrder = byteOrder; + } + + /** + * Read a datum from the input stream. + * @return The next input datum in the stream. + * @throws IOException if an I/O error occurs. + */ + public long read() throws IOException { + long[] data = new long[1]; + read(data); + return data[0]; + } + + /** + * Read the data from the input stream. + * @param data placeholder for input data. + * @throws IOException if an I/O error occurs. + */ + public void read( long[] data ) throws IOException { + read( data, 0, data.length ); + } + + /** + * Read the data from the input stream, starting at the given offset. + * @param data placeholder for input data. + * @param offset place in the array to start reading in data. + * @param length number of ints to read in. + * @throws IOException if an I/O error occurs. + */ + public void read( long[] data, int offset, int length ) throws IOException { + ByteBuffer readBuffer = ByteBuffer.allocate(bytesPerInteger*length).order(byteOrder); + + targetInputChannel.read(readBuffer,targetInputChannel.position()); + readBuffer.flip(); + targetInputChannel.position(targetInputChannel.position()+readBuffer.remaining()); + + int i = 0; + while(i < length) + data[offset+i++] = readBuffer.getInt() & 0xFFFFFFFFL; + } + + /** + * Closes the given output stream. + * @throws IOException if an I/O error occurs. + */ + public void close() throws IOException { + targetInputStream.close(); + } +} diff --git a/public/java/src/org/broadinstitute/sting/alignment/reference/packing/UnsignedIntPackedOutputStream.java b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/UnsignedIntPackedOutputStream.java new file mode 100755 index 000000000..9d7853695 --- /dev/null +++ b/public/java/src/org/broadinstitute/sting/alignment/reference/packing/UnsignedIntPackedOutputStream.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2009 The Broad Institute + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.broadinstitute.sting.alignment.reference.packing; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Writes an list of integers to the output file. + * + * @author mhanna + * @version 0.1 + */ +public class UnsignedIntPackedOutputStream { + /** + * Ultimate target for the occurrence array. + */ + private final OutputStream targetOutputStream; + + /** + * A fixed-size buffer for int-packed data. + */ + private final ByteBuffer buffer; + + /** + * Create a new PackedIntOutputStream, writing to the given target file. + * @param outputFile target output file. + * @param byteOrder Endianness to use when writing a list of integers. + * @throws IOException if an I/O error occurs. + */ + public UnsignedIntPackedOutputStream(File outputFile, ByteOrder byteOrder) throws IOException { + this(new FileOutputStream(outputFile),byteOrder); + } + + /** + * Write packed ints to the given OutputStream. + * @param outputStream Output stream to which to write packed ints. + * @param byteOrder Endianness to use when writing a list of integers. + */ + public UnsignedIntPackedOutputStream(OutputStream outputStream, ByteOrder byteOrder) { + this.targetOutputStream = outputStream; + buffer = ByteBuffer.allocate(PackUtils.bitsInType(Integer.class)/PackUtils.BITS_PER_BYTE).order(byteOrder); + } + + /** + * Write the data to the output stream. + * @param datum datum to write. + * @throws IOException if an I/O error occurs. + */ + public void write( long datum ) throws IOException { + buffer.rewind(); + buffer.putInt((int)datum); + targetOutputStream.write(buffer.array()); + } + + /** + * Write the data to the output stream. + * @param data data to write. occurrences.length must match alphabet size. + * @throws IOException if an I/O error occurs. + */ + public void write( long[] data ) throws IOException { + for(long datum: data) + write(datum); + } + + /** + * Write the given chunk of data to the input stream. + * @param data data to write. + * @param offset position at which to start. + * @param length number of ints to write. + * @throws IOException if an I/O error occurs. + */ + public void write( long[] data, int offset, int length ) throws IOException { + for( int i = offset; i < offset+length; i++ ) + write(data[i]); + } + + /** + * Flush the contents of the OutputStream to disk. + * @throws IOException if an I/O error occurs. + */ + public void flush() throws IOException { + targetOutputStream.flush(); + } + + /** + * Closes the given output stream. + * @throws IOException if an I/O error occurs. + */ + public void close() throws IOException { + targetOutputStream.close(); + } + +} diff --git a/public/java/src/org/broadinstitute/sting/gatk/walkers/sequenom/CreateSequenomMask.java b/public/java/src/org/broadinstitute/sting/gatk/walkers/sequenom/CreateSequenomMask.java deleted file mode 100755 index c1c17bda5..000000000 --- a/public/java/src/org/broadinstitute/sting/gatk/walkers/sequenom/CreateSequenomMask.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.broadinstitute.sting.gatk.walkers.sequenom; - -import org.broadinstitute.sting.utils.variantcontext.VariantContext; -import org.broadinstitute.sting.gatk.contexts.AlignmentContext; -import org.broadinstitute.sting.gatk.contexts.ReferenceContext; -import org.broadinstitute.sting.gatk.refdata.RefMetaDataTracker; -import org.broadinstitute.sting.gatk.walkers.RodWalker; -import org.broadinstitute.sting.commandline.Output; -import org.broadinstitute.sting.utils.GenomeLoc; - -import java.io.PrintStream; - -/** - * Create a mask for use with the PickSequenomProbes walker. - */ -public class CreateSequenomMask extends RodWalker { - @Output - PrintStream out; - - public void initialize() {} - - public Integer map(RefMetaDataTracker tracker, ReferenceContext ref, AlignmentContext context) { - if ( tracker == null ) - return 0; - - int result = 0; - for ( VariantContext vc : tracker.getAllVariantContexts(ref) ) { - if ( vc.isSNP() ) { - GenomeLoc loc = context.getLocation(); - out.println(loc.getContig() + "\t" + (loc.getStart()-1) + "\t" + loc.getStop()); - result = 1; - break; - } - } - - return result; - } - - public Integer reduceInit() { - return 0; - } - - public Integer reduce(Integer value, Integer sum) { - return value + sum; - } - - public void onTraversalDone(Integer sum) { - logger.info("Found " + sum + " masking sites."); - } -} \ No newline at end of file diff --git a/public/java/src/org/broadinstitute/sting/gatk/walkers/sequenom/PickSequenomProbes.java b/public/java/src/org/broadinstitute/sting/gatk/walkers/sequenom/PickSequenomProbes.java deleted file mode 100755 index fde233b5d..000000000 --- a/public/java/src/org/broadinstitute/sting/gatk/walkers/sequenom/PickSequenomProbes.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright (c) 2010 The Broad Institute - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -package org.broadinstitute.sting.gatk.walkers.sequenom; - -import net.sf.samtools.util.CloseableIterator; -import org.broad.tribble.bed.BEDCodec; -import org.broad.tribble.dbsnp.DbSNPCodec; -import org.broadinstitute.sting.utils.variantcontext.Allele; -import org.broadinstitute.sting.utils.variantcontext.VariantContext; -import org.broadinstitute.sting.gatk.contexts.AlignmentContext; -import org.broadinstitute.sting.gatk.contexts.ReferenceContext; -import org.broadinstitute.sting.gatk.refdata.*; -import org.broadinstitute.sting.gatk.refdata.tracks.RMDTrack; -import org.broadinstitute.sting.gatk.refdata.tracks.builders.RMDTrackBuilder; -import org.broadinstitute.sting.gatk.refdata.utils.GATKFeature; -import org.broadinstitute.sting.gatk.refdata.utils.LocationAwareSeekableRODIterator; -import org.broadinstitute.sting.gatk.refdata.utils.RODRecordList; -import org.broadinstitute.sting.gatk.refdata.utils.helpers.DbSNPHelper; -import org.broadinstitute.sting.gatk.walkers.*; -import org.broadinstitute.sting.utils.GenomeLoc; -import org.broadinstitute.sting.utils.GenomeLocParser; -import org.broadinstitute.sting.commandline.Argument; -import org.broadinstitute.sting.commandline.Output; - -import java.io.File; -import java.util.*; -import java.io.PrintStream; - - -/** - * Generates Sequenom probe information given a single variant track. Emitted is the variant - * along with the 200 reference bases on each side of the variant. - */ -@WalkerName("PickSequenomProbes") -@Requires(value={DataSource.REFERENCE}) -@Reference(window=@Window(start=-200,stop=200)) -public class PickSequenomProbes extends RodWalker { - @Output - PrintStream out; - - @Argument(required=false, shortName="snp_mask", doc="positions to be masked with N's") - protected String SNP_MASK = null; - @Argument(required=false, shortName="project_id",doc="If specified, all probenames will be prepended with 'project_id|'") - String project_id = null; - @Argument(required = false, shortName="omitWindow", doc = "If specified, the window appender will be omitted from the design files (e.g. \"_chr:start-stop\")") - boolean omitWindow = false; - @Argument(required = false, fullName="usePlinkRODNamingConvention", shortName="nameConvention",doc="Use the naming convention defined in PLINKROD") - boolean useNamingConvention = false; - @Argument(required = false, fullName="noMaskWindow",shortName="nmw",doc="Do not mask bases within X bases of an event when designing probes") - int noMaskWindow = 0; - @Argument(required = false, shortName="counter", doc = "If specified, unique count id (ordinal number) is added to the end of each assay name") - boolean addCounter = false; - - private byte [] maskFlags = new byte[401]; - - private LocationAwareSeekableRODIterator snpMaskIterator=null; - - private GenomeLoc positionOfLastVariant = null; - - private int cnt = 0; - private int discarded = 0; - - VariantCollection VCs ; // will keep a set of distinct variants at a given site - private List processedVariantsInScope = new LinkedList(); - - public void initialize() { - if ( SNP_MASK != null ) { - logger.info("Loading SNP mask... "); - ReferenceOrderedData snp_mask; - //if ( SNP_MASK.contains(DbSNPHelper.STANDARD_DBSNP_TRACK_NAME)) { - RMDTrackBuilder builder = new RMDTrackBuilder(getToolkit().getReferenceDataSource().getReference().getSequenceDictionary(),getToolkit().getGenomeLocParser(),getToolkit().getArguments().unsafe); - RMDTrack track = builder.createInstanceOfTrack(BEDCodec.class, new File(SNP_MASK)); - snpMaskIterator = new SeekableRODIterator(track.getHeader(), - track.getSequenceDictionary(), - getToolkit().getReferenceDataSource().getReference().getSequenceDictionary(), - getToolkit().getGenomeLocParser(), - track.getIterator()); - //} else { - // // TODO: fix me when Plink is back - // throw new IllegalArgumentException("We currently do not support other snp_mask tracks (like Plink)"); - //} - - } - VCs = new VariantCollection(); - } - - - public String map(RefMetaDataTracker tracker, ReferenceContext ref, AlignmentContext context) { - if ( tracker == null ) - return ""; - - logger.debug("Probing " + ref.getLocus() + " " + ref.getWindow()); - - VCs.clear(); - VCs.addAll( tracker.getAllVariantContexts(ref), ref.getLocus() ); - - discarded += VCs.discarded(); - - if ( VCs.size() == 0 ) { - logger.debug(" Context empty"); - return ""; - } - - if ( VCs.size() > 1 ) { - logger.debug(" "+VCs.size()+ " variants at the locus"); - } - -// System.out.print("At locus "+ref.getLocus()+": "); -// for ( VariantContext vc : VCs ) { -// System.out.println(vc.toString()); -// } - - // little optimization: since we may have few events at the current site on the reference, - // we are going to make sure we compute the mask and ref bases only once for each location and only if we need to - boolean haveMaskForWindow = false; - boolean haveBasesForWindow = false; - String leading_bases = null; - String trailing_bases = null; - - StringBuilder assaysForLocus = new StringBuilder(""); // all assays for current locus will be collected here (will be multi-line if multiple events are assayed) - - // get all variant contexts!!!! - for ( VariantContext vc : VCs ) { - - // we can only deal with biallelic sites for now - if ( !vc.isBiallelic() ) { - logger.debug(" Not biallelic; skipped"); - continue; - } - - // we don't want to see the same multi-base event (deletion, DNP etc) multiple times. - // All the vcs we are currently seeing are clearly on the same contig as the current reference - // poisiton (or we would not see them at all!). All we need to check is if the vc starts at the - // current reference position (i.e. it is the first time we see it) or not (i.e. we saw it already). - if ( ref.getLocus().getStart() != vc.getStart() ) - continue; - - if ( ! haveMaskForWindow ) { - String contig = context.getLocation().getContig(); - int offset = context.getLocation().getStart(); - int true_offset = offset - 200; - - // we have variant; let's load all the snps falling into the current window and prepare the mask array. - // we need to do it only once per window, regardless of how many vcs we may have at this location! - if ( snpMaskIterator != null ) { - // clear the mask - for ( int i = 0 ; i < 401; i++ ) - maskFlags[i] = 0; - - RODRecordList snpList = snpMaskIterator.seekForward(getToolkit().getGenomeLocParser().createGenomeLoc(contig,offset-200,offset+200)); - if ( snpList != null && snpList.size() != 0 ) { - Iterator snpsInWindow = snpList.iterator(); - int i = 0; - while ( snpsInWindow.hasNext() ) { - GenomeLoc snp = snpsInWindow.next().getLocation(); - // we don't really want to mask out multi-base indels - if ( snp.size() > 1 ) - continue; - logger.debug(" SNP at "+snp.getStart()); - int offsetInWindow = (int)(snp.getStart() - true_offset); - maskFlags[offsetInWindow] = 1; - } - } - } - haveMaskForWindow = true; // if we use masking, we will probably need to recompute the window... - } - - if ( ! haveBasesForWindow ) { - byte[] context_bases = ref.getBases(); - for (int i = 0; i < 401; i++) { - if ( maskFlags[i] == 1 && ( i < 200 - noMaskWindow || i > 200 + getNoMaskWindowRightEnd(vc,noMaskWindow) ) ) { - context_bases[i] = 'N'; - } - } - leading_bases = new String(Arrays.copyOfRange(context_bases, 0, 200)); - trailing_bases = new String(Arrays.copyOfRange(context_bases, 201, 401)); - // masked bases are not gonna change for the current window, unless we use windowed masking; - // in the latter case the bases (N's) will depend on the event we are currently looking at, - // so we better recompute.. - if ( noMaskWindow == 0 ) haveBasesForWindow = true; - } - - - // below, build single assay line for the current VC: - - String assay_sequence; - if ( vc.isSNP() ) - assay_sequence = leading_bases + "[" + (char)ref.getBase() + "/" + vc.getAlternateAllele(0).toString() + "]" + trailing_bases; - else if ( vc.isMNP() ) - assay_sequence = leading_bases + "[" + new String(vc.getReference().getBases()) + "/" + new String(vc.getAlternateAllele(0).getBases())+"]"+trailing_bases.substring(vc.getReference().length()-1); - else if ( vc.isInsertion() ) - assay_sequence = leading_bases + (char)ref.getBase() + "[-/" + vc.getAlternateAllele(0).toString() + "]" + trailing_bases; - else if ( vc.isDeletion() ) - assay_sequence = leading_bases + (char)ref.getBase() + "[" + new String(vc.getReference().getBases()) + "/-]" + trailing_bases.substring(vc.getReference().length()); - else - continue; - - StringBuilder assay_id = new StringBuilder(); - if ( project_id != null ) { - assay_id.append(project_id); - assay_id.append('|'); - } - if ( useNamingConvention ) { - assay_id.append('c'); - assay_id.append(context.getLocation().toString().replace(":","_p")); - } else { - assay_id.append(context.getLocation().toString().replace(':','_')); - } - if ( vc.isInsertion() ) assay_id.append("_gI"); - else if ( vc.isDeletion()) assay_id.append("_gD"); - - if ( ! omitWindow ) { - assay_id.append("_"); - assay_id.append(ref.getWindow().toString().replace(':', '_')); - } - ++cnt; - if ( addCounter ) assay_id.append("_"+cnt); - - assaysForLocus.append(assay_id); - assaysForLocus.append('\t'); - assaysForLocus.append(assay_sequence); - assaysForLocus.append('\n'); - } - return assaysForLocus.toString(); - } - - public String reduceInit() { - return ""; - } - - public String reduce(String data, String sum) { - out.print(data); - return ""; - } - - private int getNoMaskWindowRightEnd(VariantContext vc, int window) { - if ( window == 0 ) { - return 0; - } - - if ( vc.isInsertion() ) { - return window-1; - } - - int max = 0; - for (Allele a : vc.getAlleles() ) { - if ( vc.isInsertion() ) { - logger.debug("Getting length of allele "+a.toString()+" it is "+a.getBases().length+" (ref allele is "+vc.getReference().toString()+")"); - } - if ( a.getBases().length > max ) { - max = a.getBases().length; - } - } - return max+window-1; - } - - public void onTraversalDone(String sum) { - logger.info(cnt+" assay seqences generated"); - logger.info(discarded+" events were found to be duplicates and discarded (no redundant assays generated)"); - } - - static class EventComparator implements Comparator { - - public int compare(VariantContext o1, VariantContext o2) { - // if variants start at different positions, they are different. All we actually - // care about is detecting the variants that are strictly the same; the actual ordering of distinct variants - // (which one we deem less and which one greater) is utterly unimportant. We just need to be consistent. - if ( o1.getStart() < o2.getStart() ) return -1; - if ( o1.getStart() > o2.getStart() ) return 1; - - if ( o1.getType() != o2.getType() ) return o1.getType().compareTo(o2.getType()); - - int refComp = o1.getReference().compareTo(o2.getReference()); - if ( refComp != 0 ) return refComp; - - return o1.getAlternateAllele(0).compareTo(o2.getAlternateAllele(0)); - - } - } - - static class VariantCollection implements Iterable { - TreeSet variants = new TreeSet(new EventComparator()); - int discarded = 0; - - public void add(VariantContext vc, GenomeLoc current) { - if ( vc.getStart() != current.getStart() ) return; // we add only variants that start at current locus - // note that we do not check chr here, since the way this class is used, the mathod is always called with - // VCs coming from the same metadata tracker, so they simply can not be on different chrs! - if ( !vc.isBiallelic() ) { - logger.info(" Non-biallelic variant encountered; skipped"); - return; - } - if ( variants.add(vc) == false ) discarded++; - } - - public void addAll(Collection c, GenomeLoc current) { - for ( VariantContext vc : c ) add(vc,current); - } - - public void clear() { - variants.clear(); - discarded = 0; - } - - public int discarded() { return discarded; } - - public int size() { return variants.size(); } - - public Iterator iterator() { return variants.iterator(); } - } -} diff --git a/public/java/src/org/broadinstitute/sting/gatk/walkers/validation/ValidationAmplicons.java b/public/java/src/org/broadinstitute/sting/gatk/walkers/validation/ValidationAmplicons.java index 3a5213868..466c1aee3 100755 --- a/public/java/src/org/broadinstitute/sting/gatk/walkers/validation/ValidationAmplicons.java +++ b/public/java/src/org/broadinstitute/sting/gatk/walkers/validation/ValidationAmplicons.java @@ -34,7 +34,7 @@ import java.util.List; */ @Requires(value={DataSource.REFERENCE}, referenceMetaData={@RMD(name="ProbeIntervals",type=TableFeature.class), @RMD(name="ValidateAlleles",type=VariantContext.class),@RMD(name="MaskAlleles",type=VariantContext.class)}) -public class PickSequenomProbes2 extends RodWalker { +public class ValidationAmplicons extends RodWalker { @Argument(doc="Lower case SNPs rather than replacing with 'N'",fullName="lowerCaseSNPs",required=false) boolean lowerCaseSNPs = false; @@ -45,6 +45,9 @@ public class PickSequenomProbes2 extends RodWalker { @Argument(doc="Monomorphic sites in the mask file will be treated as filtered",fullName="filterMonomorphic",required=false) boolean filterMonomorphic = false; + @Argument(doc="Do not use BWA, lower-case repeats only",fullName="doNotUseBWA",required=false) + boolean doNotUseBWA = false; + GenomeLoc prevInterval; GenomeLoc allelePos; String probeName; @@ -67,16 +70,18 @@ public class PickSequenomProbes2 extends RodWalker { private SAMFileHeader header = null; public void initialize() { - if(targetReferenceFile == null) - targetReferenceFile = getToolkit().getArguments().referenceFile; - BWTFiles bwtFiles = new BWTFiles(targetReferenceFile.getAbsolutePath()); - BWAConfiguration configuration = new BWAConfiguration(); - aligner = new BWACAligner(bwtFiles,configuration); - header = new SAMFileHeader(); - SAMSequenceDictionary referenceDictionary = - ReferenceSequenceFileFactory.getReferenceSequenceFile(targetReferenceFile).getSequenceDictionary(); - header.setSequenceDictionary(referenceDictionary); - header.setSortOrder(SAMFileHeader.SortOrder.unsorted); + if ( ! doNotUseBWA ) { + if(targetReferenceFile == null) + targetReferenceFile = getToolkit().getArguments().referenceFile; + BWTFiles bwtFiles = new BWTFiles(targetReferenceFile.getAbsolutePath()); + BWAConfiguration configuration = new BWAConfiguration(); + aligner = new BWACAligner(bwtFiles,configuration); + header = new SAMFileHeader(); + SAMSequenceDictionary referenceDictionary = + ReferenceSequenceFileFactory.getReferenceSequenceFile(targetReferenceFile).getSequenceDictionary(); + header.setSequenceDictionary(referenceDictionary); + header.setSortOrder(SAMFileHeader.SortOrder.unsorted); + } } public Integer reduceInit() { @@ -106,8 +111,11 @@ public class PickSequenomProbes2 extends RodWalker { // there was a previous interval validateSequence(); // ensure the sequence in the region is valid // next line removed in favor of the one after - //lowerRepeats(); // change repeats in sequence to lower case - lowerNonUniqueSegments(); + if ( doNotUseBWA ) { + lowerRepeats(); // change repeats in sequence to lower case + } else { + lowerNonUniqueSegments(); + } print(); // print out the fasta sequence } @@ -218,7 +226,11 @@ public class PickSequenomProbes2 extends RodWalker { public void onTraversalDone(Integer fin ) { validateSequence(); - lowerNonUniqueSegments(); + if ( doNotUseBWA ) { + lowerRepeats(); + } else { + lowerNonUniqueSegments(); + } print(); } diff --git a/public/java/test/org/broadinstitute/sting/alignment/AlignerIntegrationTest.java b/public/java/test/org/broadinstitute/sting/alignment/AlignerIntegrationTest.java new file mode 100644 index 000000000..dafaf3ffe --- /dev/null +++ b/public/java/test/org/broadinstitute/sting/alignment/AlignerIntegrationTest.java @@ -0,0 +1,27 @@ +package org.broadinstitute.sting.alignment; + +import org.testng.annotations.Test; +import org.broadinstitute.sting.WalkerTest; + +import java.util.Arrays; + +/** + * Integration tests for the aligner. + * + * @author mhanna + * @version 0.1 + */ +public class AlignerIntegrationTest extends WalkerTest { + @Test + public void testBasicAlignment() { + String md5 = "34eb4323742999d6d250a0aaa803c6d5"; + WalkerTest.WalkerTestSpec spec = new WalkerTest.WalkerTestSpec( + "-R " + GATKDataLocation + "bwa/human_b36_both.fasta" + + " -T Align" + + " -I " + validationDataLocation + "NA12878_Pilot1_20.trimmed.unmapped.bam" + + " -o %s", + 1, // just one output file + Arrays.asList(md5)); + executeTest("testBasicAlignment", spec); + } +} diff --git a/public/java/test/org/broadinstitute/sting/gatk/walkers/sequenom/PickSequenomProbesIntegrationTest.java b/public/java/test/org/broadinstitute/sting/gatk/walkers/sequenom/PickSequenomProbesIntegrationTest.java deleted file mode 100755 index 850a3113e..000000000 --- a/public/java/test/org/broadinstitute/sting/gatk/walkers/sequenom/PickSequenomProbesIntegrationTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.broadinstitute.sting.gatk.walkers.sequenom; - -import org.broadinstitute.sting.WalkerTest; -import org.testng.annotations.Test; - -import java.util.Arrays; - -public class PickSequenomProbesIntegrationTest extends WalkerTest { - @Test - public void testProbes() { - String testVCF = validationDataLocation + "complexExample.vcf4"; - String testArgs = "-R " + b36KGReference + " -T PickSequenomProbes -L 1:10,000,000-11,000,000 -B:input,VCF "+testVCF+" -o %s"; - WalkerTestSpec spec = new WalkerTestSpec(testArgs, 1, - Arrays.asList("6b5409cc78960f1be855536ed89ea9dd")); - executeTest("Test probes", spec); - } - - @Test - public void testProbesUsingDbSNPMask() { - - String md5 = "46d53491af1d3aa0ee1f1e13d68b732d"; - String testVCF = validationDataLocation + "pickSeqIntegrationTest.vcf"; - - String testArgs = "-snp_mask " + validationDataLocation + "pickSeqIntegrationTest.bed -R " - + b36KGReference + " -omitWindow -nameConvention " - + "-project_id 1kgp3_s4_lf -T PickSequenomProbes -B:input,VCF "+testVCF+" -o %s"; - WalkerTestSpec spec1 = new WalkerTestSpec(testArgs, 1, Arrays.asList(md5)); - executeTest("Test probes", spec1); - - testArgs += " -nmw 1"; - WalkerTestSpec spec2 = new WalkerTestSpec(testArgs, 1, Arrays.asList(md5)); - executeTest("Test probes", spec2); - } -} diff --git a/public/java/test/org/broadinstitute/sting/gatk/walkers/validation/ValidationAmpliconsIntegrationTest.java b/public/java/test/org/broadinstitute/sting/gatk/walkers/validation/ValidationAmpliconsIntegrationTest.java new file mode 100755 index 000000000..6528f5795 --- /dev/null +++ b/public/java/test/org/broadinstitute/sting/gatk/walkers/validation/ValidationAmpliconsIntegrationTest.java @@ -0,0 +1,56 @@ +package org.broadinstitute.sting.gatk.walkers.validation; + +import org.broadinstitute.sting.WalkerTest; +import org.testng.annotations.Test; + +import java.util.Arrays; + +/** + * Created by IntelliJ IDEA. + * User: Ghost + * Date: 7/19/11 + * Time: 7:39 PM + * To change this template use File | Settings | File Templates. + */ +public class ValidationAmpliconsIntegrationTest extends WalkerTest { + + @Test + public void testWikiExample() { + String siteVCF = validationDataLocation + "sites_to_validate.vcf"; + String maskVCF = validationDataLocation + "amplicon_mask_sites.vcf"; + String intervalTable = validationDataLocation + "amplicon_interval_table1.table"; + String testArgs = "-R " + b37KGReference + " -T ValidationAmplicons -B:ValidateAlleles,VCF "+siteVCF+" -o %s"; + testArgs += " -B:ProbeIntervals,table "+intervalTable+" -BTI ProbeIntervals -B:MaskAlleles,VCF "+maskVCF; + testArgs += " --virtualPrimerSize 30"; + WalkerTestSpec spec = new WalkerTestSpec(testArgs, 1, + Arrays.asList("27f9450afa132888a8994167f0035fd7")); + executeTest("Test probes", spec); + } + + @Test + public void testWikiExampleNoBWA() { + String siteVCF = validationDataLocation + "sites_to_validate.vcf"; + String maskVCF = validationDataLocation + "amplicon_mask_sites.vcf"; + String intervalTable = validationDataLocation + "amplicon_interval_table1.table"; + String testArgs = "-R " + b37KGReference + " -T ValidationAmplicons -B:ValidateAlleles,VCF "+siteVCF+" -o %s"; + testArgs += " -B:ProbeIntervals,table "+intervalTable+" -BTI ProbeIntervals -B:MaskAlleles,VCF "+maskVCF; + testArgs += " --virtualPrimerSize 30 --doNotUseBWA"; + WalkerTestSpec spec = new WalkerTestSpec(testArgs, 1, + Arrays.asList("f2611ff1d9cd5bedaad003251fed8bc1")); + executeTest("Test probes", spec); + } + + @Test + public void testWikiExampleMonoFilter() { + String siteVCF = validationDataLocation + "sites_to_validate.vcf"; + String maskVCF = validationDataLocation + "amplicon_mask_sites.vcf"; + String intervalTable = validationDataLocation + "amplicon_interval_table1.table"; + String testArgs = "-R " + b37KGReference + " -T ValidationAmplicons -B:ValidateAlleles,VCF "+siteVCF+" -o %s"; + testArgs += " -B:ProbeIntervals,table "+intervalTable+" -BTI ProbeIntervals -B:MaskAlleles,VCF "+maskVCF; + testArgs += " --virtualPrimerSize 30 --filterMonomorphic"; + WalkerTestSpec spec = new WalkerTestSpec(testArgs, 1, + Arrays.asList("77b3f30e38fedad812125bdf6cf3255f")); + executeTest("Test probes", spec); + } + +}