From 2e5837c1420da880d725f422f66966f6d5efd1f2 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Thu, 10 Oct 2024 17:17:23 -0400 Subject: [PATCH 01/13] rewrite PeakIO.py, PairedEndTrack.py; --- MACS3/IO/{PeakIO.pyx => PeakIO.py} | 1010 +++++++++++++++++----------- MACS3/Signal/CallPeakUnit.pyx | 4 +- MACS3/Signal/PairedEndTrack.py | 730 ++++++++++++++++++++ MACS3/Signal/PairedEndTrack.pyx | 584 ---------------- MACS3/Signal/ScoreTrack.pyx | 4 +- setup.py | 4 +- 6 files changed, 1350 insertions(+), 986 deletions(-) rename MACS3/IO/{PeakIO.pyx => PeakIO.py} (60%) create mode 100644 MACS3/Signal/PairedEndTrack.py delete mode 100644 MACS3/Signal/PairedEndTrack.pyx diff --git a/MACS3/IO/PeakIO.pyx b/MACS3/IO/PeakIO.py similarity index 60% rename from MACS3/IO/PeakIO.pyx rename to MACS3/IO/PeakIO.py index e959db25..433dafbf 100644 --- a/MACS3/IO/PeakIO.pyx +++ b/MACS3/IO/PeakIO.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-09-06 14:56:51 Tao Liu> +# Time-stamp: <2024-10-10 17:00:18 Tao Liu> """Module for PeakIO IO classes. @@ -22,25 +22,25 @@ # MACS3 modules # ------------------------------------ -from MACS3.Utilities.Constants import * +# from MACS3.Utilities.Constants import * # ------------------------------------ # Other modules # ------------------------------------ - -from cpython cimport bool +import cython +from cython.cimports.cpython import bool # ------------------------------------ # constants # ------------------------------------ -__version__ = "PeakIO $Revision$" -__author__ = "Tao Liu " -__doc__ = "PeakIO class" # ------------------------------------ # Misc functions # ------------------------------------ -cdef str subpeak_letters( int i): + + +@cython.cfunc +def subpeak_letters(i: cython.int) -> str: if i < 26: return chr(97+i) else: @@ -50,24 +50,32 @@ # Classes # ------------------------------------ -cdef class PeakContent: - cdef: - bytes chrom - int start - int end - int length - int summit - float score - float pileup - float pscore - float fc - float qscore - bytes name - - def __init__ ( self, bytes chrom, int start, int end, int summit, - float peak_score, float pileup, - float pscore, float fold_change, float qscore, - bytes name= b"NA" ): + +@cython.cclass +class PeakContent: + chrom: bytes + start: cython.int + end: cython.int + length: cython.int + summit: cython.int + score: cython.float + pileup: cython.float + pscore: cython.float + fc: cython.float + qscore: cython.float + name: bytes + + def __init__(self, + chrom: bytes, + start: cython.int, + end: cython.int, + summit: cython.int, + peak_score: cython.float, + pileup: cython.float, + pscore: cython.float, + fold_change: cython.float, + qscore: cython.float, + name: bytes = b"NA"): self.chrom = chrom self.start = start self.end = end @@ -80,7 +88,7 @@ def __init__ ( self, bytes chrom, int start, int end, int summit, self.qscore = qscore self.name = name - def __getitem__ ( self, a ): + def __getitem__(self, a: str): if a == "chrom": return self.chrom elif a == "start": @@ -104,7 +112,7 @@ def __getitem__ ( self, a ): elif a == "name": return self.name - def __setitem__ ( self, a, v ): + def __setitem__(self, a: str, v): if a == "chrom": self.chrom = v elif a == "start": @@ -128,27 +136,42 @@ def __setitem__ ( self, a, v ): elif a == "name": self.name = v - def __str__ (self): - return "chrom:%s;start:%d;end:%d;score:%f" % ( self.chrom, self.start, self.end, self.score ) + def __str__(self): + return "chrom:%s;start:%d;end:%d;score:%f" % (self.chrom, + self.start, + self.end, + self.score) + -cdef class PeakIO: +@cython.cclass +class PeakIO: """IO for peak information. """ - cdef: - public dict peaks # dictionary storing peak contents - public bool CO_sorted # whether peaks have been sorted by coordinations - public long total # total number of peaks - - def __init__ (self): + # dictionary storing peak contents + peaks = cython.declare(dict, visibility="public") + # whether peaks have been sorted by coordinations + CO_sorted = cython.declare(bool, visibility="public") + # total number of peaks + total = cython.declare(cython.long, visibility="public") + + def __init__(self): self.peaks = {} self.CO_sorted = False self.total = 0 - cpdef add (self, bytes chromosome, int start, int end, int summit = 0, - float peak_score = 0, float pileup = 0, - float pscore = 0, float fold_change = 0, float qscore = 0, - bytes name = b"NA"): + @cython.ccall + def add(self, + chromosome: bytes, + start: cython.int, + end: cython.int, + summit: cython.int = 0, + peak_score: cython.float = 0, + pileup: cython.float = 0, + pscore: cython.float = 0, + fold_change: cython.float = 0, + qscore: cython.float = 0, + name: bytes = b"NA"): """items: start:start end:end, @@ -161,154 +184,165 @@ def __init__ (self): qscore:qscore """ if not self.peaks.has_key(chromosome): - self.peaks[chromosome]=[] - self.peaks[chromosome].append(PeakContent( chromosome, start, end, summit, peak_score, pileup, pscore, fold_change, qscore, name)) + self.peaks[chromosome] = [] + self.peaks[chromosome].append(PeakContent(chromosome, + start, + end, + summit, + peak_score, + pileup, + pscore, + fold_change, + qscore, + name)) self.total += 1 self.CO_sorted = False - cpdef add_PeakContent ( self, bytes chromosome, object peakcontent ): + @cython.ccall + def add_PeakContent(self, + chromosome: bytes, + peakcontent: PeakContent): if not self.peaks.has_key(chromosome): - self.peaks[chromosome]=[] + self.peaks[chromosome] = [] self.peaks[chromosome].append(peakcontent) self.total += 1 self.CO_sorted = False - cpdef list get_data_from_chrom (self, bytes chrom): - if not self.peaks.has_key( chrom ): - self.peaks[chrom]= [] + @cython.ccall + def get_data_from_chrom(self, chrom: bytes) -> list: + if not self.peaks.has_key(chrom): + self.peaks[chrom] = [] return self.peaks[chrom] - cpdef set get_chr_names (self): + def get_chr_names(self) -> set: return set(sorted(self.peaks.keys())) - def sort ( self ): - cdef: - list chrs - bytes chrom + def sort(self): + chrs: list + chrom: bytes + # sort by position if self.CO_sorted: # if already sorted, quit return chrs = sorted(list(self.peaks.keys())) for chrom in sorted(chrs): - self.peaks[chrom].sort(key=lambda x:x['start']) + self.peaks[chrom].sort(key=lambda x: x['start']) self.CO_sorted = True return - cpdef object randomly_pick ( self, int n, int seed = 12345 ): + @cython.ccall + def randomly_pick(self, n: cython.int, seed: cython.int = 12345): """Shuffle the peaks and get n peaks out of it. Return a new PeakIO object. """ - cdef: - list all_pc - list chrs - bytes chrom - object ret_peakio, p + all_pc: list + chrs: list + chrom: bytes + ret_peakio: PeakIO + p: PeakContent + assert n > 0 chrs = sorted(list(self.peaks.keys())) all_pc = [] for chrom in sorted(chrs): all_pc.extend(self.peaks[chrom]) - random.seed( seed ) - random.shuffle( all_pc ) + random.seed(seed) + random.shuffle(all_pc) all_pc = all_pc[:n] ret_peakio = PeakIO() for p in all_pc: - ret_peakio.add_PeakContent ( p["chrom"], p ) + ret_peakio.add_PeakContent(p["chrom"], p) return ret_peakio - - cpdef void filter_pscore (self, double pscore_cut ): - cdef: - bytes chrom - dict new_peaks - list chrs - object p + + @cython.ccall + def filter_pscore(self, pscore_cut: cython.double): + chrom: bytes + new_peaks: dict + chrs: list + new_peaks = {} chrs = sorted(list(self.peaks.keys())) self.total = 0 for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in self.peaks[chrom] if p['pscore'] >= pscore_cut] - self.total += len( new_peaks[chrom] ) + new_peaks[chrom] = [p for p in self.peaks[chrom] if p['pscore'] >= pscore_cut] + self.total += len(new_peaks[chrom]) self.peaks = new_peaks self.CO_sorted = True self.sort() - cpdef void filter_qscore (self, double qscore_cut ): - cdef: - bytes chrom - dict new_peaks - list chrs - object p + @cython.ccall + def filter_qscore(self, qscore_cut: cython.double): + chrom: bytes + new_peaks: dict + chrs: list new_peaks = {} chrs = sorted(list(self.peaks.keys())) self.total = 0 for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in self.peaks[chrom] if p['qscore'] >= qscore_cut] - self.total += len( new_peaks[chrom] ) + new_peaks[chrom] = [p for p in self.peaks[chrom] if p['qscore'] >= qscore_cut] + self.total += len(new_peaks[chrom]) self.peaks = new_peaks self.CO_sorted = True self.sort() - cpdef void filter_fc (self, float fc_low, float fc_up = 0 ): + @cython.ccall + def filter_fc(self, fc_low: cython.float, fc_up: cython.float = 0): """Filter peaks in a given fc range. If fc_low and fc_up is assigned, the peaks with fc in [fc_low,fc_up) """ - cdef: - bytes chrom - dict new_peaks - list chrs - object p + chrom: bytes + new_peaks: dict + chrs: list new_peaks = {} chrs = list(self.peaks.keys()) self.total = 0 if fc_up > 0 and fc_up > fc_low: for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in self.peaks[chrom] if p['fc'] >= fc_low and p['fc']= fc_low and p['fc'] < fc_up] + self.total += len(new_peaks[chrom]) else: for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in self.peaks[chrom] if p['fc'] >= fc_low] - self.total += len( new_peaks[chrom] ) + new_peaks[chrom] = [p for p in self.peaks[chrom] if p['fc'] >= fc_low] + self.total += len(new_peaks[chrom]) self.peaks = new_peaks self.CO_sorted = True self.sort() - cpdef void filter_score (self, float lower_score, float upper_score = 0 ): + def filter_score(self, lower_score: cython.float, upper_score: cython.float = 0): """Filter peaks in a given score range. """ - cdef: - bytes chrom - dict new_peaks - list chrs - object p + chrom: bytes + new_peaks: dict + chrs: list new_peaks = {} chrs = list(self.peaks.keys()) self.total = 0 if upper_score > 0 and upper_score > lower_score: for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in self.peaks[chrom] if p['score'] >= lower_score and p['score']= lower_score and p['score'] < upper_score] + self.total += len(new_peaks[chrom]) else: for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in self.peaks[chrom] if p['score'] >= lower_score] - self.total += len( new_peaks[chrom] ) + new_peaks[chrom] = [p for p in self.peaks[chrom] if p['score'] >= lower_score] + self.total += len(new_peaks[chrom]) self.peaks = new_peaks self.CO_sorted = True self.sort() - def __str__ (self): + def __str__(self): """convert to text -- for debug """ - cdef: - list chrs - int n_peak - str ret + chrs: list + n_peak: cython.int + ret: str + ret = "" chrs = list(self.peaks.keys()) n_peak = 0 @@ -318,38 +352,44 @@ def __str__ (self): peaks = list(group) if len(peaks) > 1: for i, peak in enumerate(peaks): - ret += "chrom:%s\tstart:%d\tend:%d\tname:peak_%d%s\tscore:%.6g\tsummit:%d\n" % (chrom.decode(),peak['start'],peak['end'],n_peak,subpeak_letters(i),peak["score"],peak["summit"]) + ret += "chrom:%s\tstart:%d\tend:%d\tname:peak_%d%s\tscore:%.6g\tsummit:%d\n" % (chrom.decode(), peak['start'], peak['end'], n_peak, subpeak_letters(i), peak["score"], peak["summit"]) else: peak = peaks[0] - ret += "chrom:%s\tstart:%d\tend:%d\tname:peak_%d\tscore:%.6g\tsummit:%d\n" % (chrom.decode(),peak['start'],peak['end'],n_peak,peak["score"],peak["summit"]) - + ret += "chrom:%s\tstart:%d\tend:%d\tname:peak_%d\tscore:%.6g\tsummit:%d\n" % (chrom.decode(), peak['start'], peak['end'], n_peak, peak["score"], peak["summit"]) return ret - cdef void _to_bed(self, bytes name_prefix=b"%s_peak_", bytes name=b"MACS", - bytes description=b"%s", str score_column="score", - bool trackline=False, print_func=sys.stdout.write): + @cython.cfunc + def _to_bed(self, + name_prefix: bytes = b"%s_peak_", + name: bytes = b"MACS", + description: bytes = b"%s", + score_column: str = "score", + trackline: bool = False, + print_func=sys.stdout.write): """ generalization of tobed and write_to_bed """ - cdef: - list chrs - int n_peak - bytes peakprefix, desc + chrs: list + n_peak: cython.int + peakprefix: bytes + desc: bytes + chrs = list(self.peaks.keys()) n_peak = 0 try: peakprefix = name_prefix % name - except: + except Exception: peakprefix = name_prefix try: desc = description % name - except: + except Exception: desc = description + if trackline: try: - print_func('track name="%s (peaks)" description="%s" visibility=1\n' % ( name.replace(b"\"", b"\\\"").decode(), - desc.replace(b"\"", b"\\\"").decode() ) ) - except: + print_func('track name="%s (peaks)" description="%s" visibility=1\n' % (name.replace(b"\"", b"\\\"").decode(), + desc.replace(b"\"", b"\\\"").decode())) + except Exception: print_func('track name=MACS description=Unknown\n') for chrom in sorted(chrs): for end, group in groupby(self.peaks[chrom], key=itemgetter("end")): @@ -357,27 +397,43 @@ def __str__ (self): peaks = list(group) if len(peaks) > 1: for i, peak in enumerate(peaks): - print_func("%s\t%d\t%d\t%s%d%s\t%.6g\n" % (chrom.decode(),peak['start'],peak['end'],peakprefix.decode(),n_peak,subpeak_letters(i),peak[score_column])) + print_func("%s\t%d\t%d\t%s%d%s\t%.6g\n" % (chrom.decode(), peak['start'], peak['end'], peakprefix.decode(), n_peak, subpeak_letters(i), peak[score_column])) else: peak = peaks[0] - print_func("%s\t%d\t%d\t%s%d\t%.6g\n" % (chrom.decode(),peak['start'],peak['end'],peakprefix.decode(),n_peak,peak[score_column])) - - cdef _to_summits_bed(self, bytes name_prefix=b"%s_peak_", bytes name=b"MACS", - bytes description = b"%s", str score_column="score", - bool trackline=False, print_func=sys.stdout.write): + print_func("%s\t%d\t%d\t%s%d\t%.6g\n" % (chrom.decode(), peak['start'], peak['end'], peakprefix.decode(), n_peak, peak[score_column])) + + @cython.cfunc + def _to_summits_bed(self, + name_prefix: bytes = b"%s_peak_", + name: bytes = b"MACS", + description: bytes = b"%s", + score_column: str = "score", + trackline: bool = False, + print_func=sys.stdout.write): """ generalization of to_summits_bed and write_to_summit_bed """ + chrs: list + n_peak: cython.int + peakprefix: bytes + desc: bytes + chrs = list(self.peaks.keys()) n_peak = 0 - try: peakprefix = name_prefix % name - except: peakprefix = name_prefix - try: desc = description % name - except: desc = description + try: + peakprefix = name_prefix % name + except Exception: + peakprefix = name_prefix + try: + desc = description % name + except Exception: + desc = description if trackline: - try: print_func('track name="%s (summits)" description="%s" visibility=1\n' % ( name.replace(b"\"", b"\\\"").decode(),\ - desc.replace(b"\"", b"\\\"").decode() ) ) - except: print_func('track name=MACS description=Unknown') + try: + print_func('track name="%s (summits)" description="%s" visibility=1\n' % (name.replace(b"\"", b"\\\"").decode(), + desc.replace(b"\"", b"\\\"").decode())) + except Exception: + print_func('track name=MACS description=Unknown') for chrom in sorted(chrs): for end, group in groupby(self.peaks[chrom], key=itemgetter("end")): n_peak += 1 @@ -385,13 +441,13 @@ def __str__ (self): if len(peaks) > 1: for i, peak in enumerate(peaks): summit_p = peak['summit'] - print_func("%s\t%d\t%d\t%s%d%s\t%.6g\n" % (chrom.decode(),summit_p,summit_p+1,peakprefix.decode(),n_peak,subpeak_letters(i),peak[score_column])) + print_func("%s\t%d\t%d\t%s%d%s\t%.6g\n" % (chrom.decode(), summit_p, summit_p+1, peakprefix.decode(), n_peak, subpeak_letters(i), peak[score_column])) else: peak = peaks[0] summit_p = peak['summit'] - print_func("%s\t%d\t%d\t%s%d\t%.6g\n" % (chrom.decode(),summit_p,summit_p+1,peakprefix.decode(),n_peak,peak[score_column])) + print_func("%s\t%d\t%d\t%s%d\t%.6g\n" % (chrom.decode(), summit_p, summit_p+1, peakprefix.decode(), n_peak, peak[score_column])) - def tobed (self): + def tobed(self): """Print out peaks in BED5 format. Five columns are chromosome, peak start, peak end, peak name, and peak height. @@ -408,7 +464,7 @@ def tobed (self): """ return self._to_bed(name_prefix=b"peak_", score_column="score", name=b"", description=b"") - def to_summits_bed (self): + def to_summits_bed(self): """Print out peak summits in BED5 format. Five columns are chromosome, summit start, summit end, peak name, and peak height. @@ -417,8 +473,12 @@ def to_summits_bed (self): return self._to_summits_bed(name_prefix=b"peak_", score_column="score", name=b"", description=b"") # these methods are very fast, specifying types is unnecessary - def write_to_bed (self, fhd, bytes name_prefix=b"peak_", bytes name=b"MACS", - bytes description = b"%s", str score_column="score", trackline=True): + def write_to_bed(self, fhd, + name_prefix: bytes = b"peak_", + name: bytes = b"MACS", + description: bytes = b"%s", + score_column: str = "score", + trackline: bool = True): """Write peaks in BED5 format in a file handler. Score (5th column) is decided by score_column setting. Check the following list. Name column ( 4th column) is made by putting @@ -439,13 +499,20 @@ def write_to_bed (self, fhd, bytes name_prefix=b"peak_", bytes name=b"MACS", fc:fold_change, qscore:qvalue """ - #print(description) - return self._to_bed(name_prefix=name_prefix, name=name, - description=description, score_column=score_column, - print_func=fhd.write, trackline=trackline) - - def write_to_summit_bed (self, fhd, bytes name_prefix = b"peak_", bytes name = b"MACS", - bytes description = b"%s", str score_column ="score", trackline=True): + # print(description) + return self._to_bed(name_prefix=name_prefix, + name=name, + description=description, + score_column=score_column, + print_func=fhd.write, + trackline=trackline) + + def write_to_summit_bed(self, fhd, + name_prefix: bytes = b"%s_peak_", + name: bytes = b"MACS", + description: bytes = b"%s", + score_column: str = "score", + trackline: bool = False): """Write peak summits in BED5 format in a file handler. Score (5th column) is decided by score_column setting. Check the following list. Name column ( 4th column) is made by putting @@ -469,7 +536,11 @@ def write_to_summit_bed (self, fhd, bytes name_prefix = b"peak_", bytes name = b description=description, score_column=score_column, print_func=fhd.write, trackline=trackline) - def write_to_narrowPeak (self, fhd, bytes name_prefix = b"peak_", bytes name = b"peak", str score_column="score", trackline=True): + def write_to_narrowPeak(self, fhd, + name_prefix: bytes = b"%s_peak_", + name: bytes = b"peak", + score_column: str = "score", + trackline: bool = False): """Print out peaks in narrowPeak format. This format is designed for ENCODE project, and basically a @@ -523,33 +594,41 @@ def write_to_narrowPeak (self, fhd, bytes name_prefix = b"peak_", bytes name = b +-----------+------+----------------------------------------+ """ - cdef int n_peak - cdef bytes chrom - cdef long s - cdef str peakname + n_peak: cython.int + chrom: bytes + s: cython.long + peakname: str chrs = list(self.peaks.keys()) n_peak = 0 write = fhd.write - try: peakprefix = name_prefix % name - except: peakprefix = name_prefix + try: + peakprefix = name_prefix % name + except Exception: + peakprefix = name_prefix if trackline: write("track type=narrowPeak name=\"%s\" description=\"%s\" nextItemButton=on\n" % (name.decode(), name.decode())) for chrom in sorted(chrs): for end, group in groupby(self.peaks[chrom], key=itemgetter("end")): n_peak += 1 these_peaks = list(group) - if len(these_peaks) > 1: # from call-summits + if len(these_peaks) > 1: # from call-summits for i, peak in enumerate(these_peaks): peakname = "%s%d%s" % (peakprefix.decode(), n_peak, subpeak_letters(i)) if peak['summit'] == -1: s = -1 else: s = peak['summit'] - peak['start'] - fhd.write( "%s\t%d\t%d\t%s\t%d\t.\t%.6g\t%.6g\t%.6g\t%d\n" - % - (chrom.decode(),peak['start'],peak['end'],peakname,int(10*peak[score_column]), - peak['fc'],peak['pscore'],peak['qscore'],s) ) + fhd.write("%s\t%d\t%d\t%s\t%d\t.\t%.6g\t%.6g\t%.6g\t%d\n" % + (chrom.decode(), + peak['start'], + peak['end'], + peakname, + int(10*peak[score_column]), + peak['fc'], + peak['pscore'], + peak['qscore'], + s)) else: peak = these_peaks[0] peakname = "%s%d" % (peakprefix.decode(), n_peak) @@ -557,13 +636,22 @@ def write_to_narrowPeak (self, fhd, bytes name_prefix = b"peak_", bytes name = b s = -1 else: s = peak['summit'] - peak['start'] - fhd.write( "%s\t%d\t%d\t%s\t%d\t.\t%.6g\t%.6g\t%.6g\t%d\n" - % - (chrom.decode(),peak['start'],peak['end'],peakname,int(10*peak[score_column]), - peak['fc'],peak['pscore'],peak['qscore'],s) ) + fhd.write("%s\t%d\t%d\t%s\t%d\t.\t%.6g\t%.6g\t%.6g\t%d\n" % + (chrom.decode(), + peak['start'], + peak['end'], + peakname, + int(10*peak[score_column]), + peak['fc'], + peak['pscore'], + peak['qscore'], + s)) return - def write_to_xls (self, ofhd, bytes name_prefix = b"%s_peak_", bytes name = b"MACS"): + @cython.ccall + def write_to_xls(self, ofhd, + name_prefix: bytes = b"%s_peak_", + name: bytes = b"MACS"): """Save the peak results in a tab-delimited plain text file with suffix .xls. @@ -571,11 +659,19 @@ def write_to_xls (self, ofhd, bytes name_prefix = b"%s_peak_", bytes name = b"MA wait... why I have two write_to_xls in this class? """ + peakprefix: bytes + chrs: list + these_peaks: list + n_peak: cython.int + i: cython.int + write = ofhd.write - write("\t".join(("chr","start", "end", "length", "abs_summit", "pileup", "-log10(pvalue)", "fold_enrichment", "-log10(qvalue)", "name"))+"\n") + write("\t".join(("chr", "start", "end", "length", "abs_summit", "pileup", "-log10(pvalue)", "fold_enrichment", "-log10(qvalue)", "name"))+"\n") - try: peakprefix = name_prefix % name - except: peakprefix = name_prefix + try: + peakprefix = name_prefix % name + except Exception: + peakprefix = name_prefix peaks = self.peaks chrs = list(peaks.keys()) @@ -587,47 +683,56 @@ def write_to_xls (self, ofhd, bytes name_prefix = b"%s_peak_", bytes name = b"MA if len(these_peaks) > 1: for i, peak in enumerate(these_peaks): peakname = "%s%d%s" % (peakprefix.decode(), n_peak, subpeak_letters(i)) - #[start,end,end-start,summit,peak_height,number_tags,pvalue,fold_change,qvalue] - write("%s\t%d\t%d\t%d" % (chrom.decode(),peak['start']+1,peak['end'],peak['length'])) - write("\t%d" % (peak['summit']+1)) # summit position - write("\t%.6g" % (round(peak['pileup'],2))) # pileup height at summit - write("\t%.6g" % (peak['pscore'])) # -log10pvalue at summit - write("\t%.6g" % (peak['fc'])) # fold change at summit - write("\t%.6g" % (peak['qscore'])) # -log10qvalue at summit + # [start,end,end-start,summit,peak_height,number_tags,pvalue,fold_change,qvalue] + write("%s\t%d\t%d\t%d" % (chrom.decode(), + peak['start']+1, + peak['end'], + peak['length'])) + write("\t%d" % (peak['summit']+1)) # summit position + write("\t%.6g" % (round(peak['pileup'], 2))) # pileup height at summit + write("\t%.6g" % (peak['pscore'])) # -log10pvalue at summit + write("\t%.6g" % (peak['fc'])) # fold change at summit + write("\t%.6g" % (peak['qscore'])) # -log10qvalue at summit write("\t%s" % peakname) write("\n") else: peak = these_peaks[0] peakname = "%s%d" % (peakprefix.decode(), n_peak) - #[start,end,end-start,summit,peak_height,number_tags,pvalue,fold_change,qvalue] - write("%s\t%d\t%d\t%d" % (chrom.decode(),peak['start']+1,peak['end'],peak['length'])) - write("\t%d" % (peak['summit']+1)) # summit position - write("\t%.6g" % (round(peak['pileup'],2))) # pileup height at summit - write("\t%.6g" % (peak['pscore'])) # -log10pvalue at summit - write("\t%.6g" % (peak['fc'])) # fold change at summit - write("\t%.6g" % (peak['qscore'])) # -log10qvalue at summit + # [start,end,end-start,summit,peak_height,number_tags,pvalue,fold_change,qvalue] + write("%s\t%d\t%d\t%d" % (chrom.decode(), + peak['start']+1, + peak['end'], + peak['length'])) + write("\t%d" % (peak['summit']+1)) # summit position + write("\t%.6g" % (round(peak['pileup'], 2))) # pileup height at summit + write("\t%.6g" % (peak['pscore'])) # -log10pvalue at summit + write("\t%.6g" % (peak['fc'])) # fold change at summit + write("\t%.6g" % (peak['qscore'])) # -log10qvalue at summit write("\t%s" % peakname) write("\n") return - - cpdef void exclude (self, object peaksio2): + @cython.ccall + def exclude(self, peaksio2: object): """ Remove overlapping peaks in peaksio2, another PeakIO object. """ - cdef: - dict peaks1, peaks2 - list chrs1, chrs2 - bytes k - dict ret_peaks - bool overlap_found - object r1, r2 # PeakContent objects - long n_rl1, n_rl2 + peaks1: dict + peaks2: dict + chrs1: list + chrs2: list + k: bytes + ret_peaks: dict + overlap_found: bool + r1: PeakContent + r2: PeakContent + n_rl1: cython.long + n_rl2: cython.long self.sort() peaks1 = self.peaks self.total = 0 - assert isinstance(peaksio2,PeakIO) + assert isinstance(peaksio2, PeakIO) peaksio2.sort() peaks2 = peaksio2.peaks @@ -638,44 +743,44 @@ def write_to_xls (self, ofhd, bytes name_prefix = b"%s_peak_", bytes name = b"MA #print(f"chromosome {k}") if not chrs2.count(k): # no such chromosome in peaks1, then don't touch the peaks in this chromosome - ret_peaks[ k ] = peaks1[ k ] + ret_peaks[k] = peaks1[k] continue - ret_peaks[ k ] = [] - n_rl1 = len( peaks1[k] ) - n_rl2 = len( peaks2[k] ) - rl1_k = iter( peaks1[k] ).__next__ - rl2_k = iter( peaks2[k] ).__next__ + ret_peaks[k] = [] + n_rl1 = len(peaks1[k]) + n_rl2 = len(peaks2[k]) + rl1_k = iter(peaks1[k]).__next__ + rl2_k = iter(peaks2[k]).__next__ overlap_found = False r1 = rl1_k() n_rl1 -= 1 r2 = rl2_k() n_rl2 -= 1 - while ( True ): + while (True): # we do this until there is no r1 or r2 left. if r2["start"] < r1["end"] and r1["start"] < r2["end"]: # since we found an overlap, r1 will be skipped/excluded # and move to the next r1 overlap_found = True - #print(f"found overlap of {r1['start']} {r1['end']} and {r2['start']} {r2['end']}, move to the next r1") + # print(f"found overlap of {r1['start']} {r1['end']} and {r2['start']} {r2['end']}, move to the next r1") n_rl1 -= 1 if n_rl1 >= 0: r1 = rl1_k() - #print(f"move to next r1 {r1['start']} {r1['end']}") + # print(f"move to next r1 {r1['start']} {r1['end']}") overlap_found = False continue else: break if r1["end"] < r2["end"]: - #print(f"now we need to move r1 {r1['start']} {r1['end']}") + # print(f"now we need to move r1 {r1['start']} {r1['end']}") # in this case, we need to move to the next r1, # we will check if overlap_found is true, if not, we put r1 in a new dict if not overlap_found: - #print(f"we add this r1 {r1['start']} {r1['end']} to list") - ret_peaks[ k ].append( r1 ) + # print(f"we add this r1 {r1['start']} {r1['end']} to list") + ret_peaks[k].append(r1) n_rl1 -= 1 if n_rl1 >= 0: r1 = rl1_k() - #print(f"move to next r1 {r1['start']} {r1['end']}") + # print(f"move to next r1 {r1['start']} {r1['end']}") overlap_found = False else: # no more r1 left @@ -685,54 +790,61 @@ def write_to_xls (self, ofhd, bytes name_prefix = b"%s_peak_", bytes name = b"MA if n_rl2: r2 = rl2_k() n_rl2 -= 1 - #print(f"move to next r2 {r2['start']} {r2['end']}") + # print(f"move to next r2 {r2['start']} {r2['end']}") else: # no more r2 left break # add the rest of r1 - #print( f"n_rl1: {n_rl1} n_rl2:{n_rl2} last overlap_found is {overlap_found}" ) - #if overlap_found: + # print( f"n_rl1: {n_rl1} n_rl2:{n_rl2} last overlap_found is {overlap_found}" ) + # if overlap_found: # n_rl1 -= 1 if n_rl1 >= 0: - ret_peaks[ k ].extend( peaks1[ k ][-n_rl1-1:] ) + ret_peaks[k].extend(peaks1[k][-n_rl1-1:]) for k in ret_peaks.keys(): - self.total += len( ret_peaks[ k ] ) + self.total += len(ret_peaks[k]) self.peaks = ret_peaks self.CO_sorted = True - self.sort() + self.sort() return - def read_from_xls (self, ofhd): + @cython.ccall + def read_from_xls(self, ofhd): """Save the peak results in a tab-delimited plain text file with suffix .xls. """ - cdef: - bytes line = b'' - bytes chrom = b'' - int n_peak = 0 - int start, end, length, summit - float pileup, pscore, fc, qscore - list fields + line: bytes = b'' + chrom: bytes = b'' + start: cython.int + end: cython.int + length: cython.int + summit: cython.int + pileup: cython.float + pscore: cython.float + fc: cython.float + qscore: cython.float + fields: list + while True: - if not (line.startswith('#') or line.strip() == ''): break + if not (line.startswith('#') or line.strip() == ''): + break line = ofhd.readline() # sanity check columns = line.rstrip().split('\t') - for a,b in zip(columns, ("chr","start", "end", "length", "abs_summit", - "pileup", "-log10(pvalue)", "fold_enrichment", - "-log10(qvalue)", "name")): - if not a==b: raise NotImplementedError('column %s not recognized', a) + for a, b in zip(columns, ("chr", "start", "end", "length", "abs_summit", + "pileup", "-log10(pvalue)", "fold_enrichment", + "-log10(qvalue)", "name")): + if not a == b: + raise NotImplementedError('column %s not recognized', a) add = self.add split = str.split rstrip = str.rstrip for i, line in enumerate(ofhd.readlines()): fields = split(line, '\t') - peak = {} chrom = fields[0].encode() start = int(fields[1]) - 1 end = int(fields[2]) @@ -748,68 +860,62 @@ def read_from_xls (self, ofhd): add(chrom, start, end, summit, qscore, pileup, pscore, fc, qscore, peakname) -cpdef parse_peakname(peakname): - """returns peaknumber, subpeak - """ - cdef: - bytes peak_id, peaknumber, subpeak - peak_id = peakname.split(b'_')[-1] - x = re.split('(\D.*)', peak_id) - peaknumber = int(x[0]) - try: - subpeak = x[1] - except IndexError: - subpeak = b'' - return (peaknumber, subpeak) - -cdef class RegionIO: + +@cython.cclass +class RegionIO: """For plain region of chrom, start and end """ - cdef: - dict regions - bool __flag_sorted + regions: dict + __flag_sorted: bool - def __init__ (self): - self.regions= {} + def __init__(self): + self.regions = {} self.__flag_sorted = False - cpdef void add_loc ( self, bytes chrom, int start, int end ): + @cython.ccall + def add_loc(self, chrom: bytes, start: cython.int, end: cython.int): if self.regions.has_key(chrom): - self.regions[chrom].append( (start,end) ) + self.regions[chrom].append((start, end)) else: - self.regions[chrom] = [(start,end), ] + self.regions[chrom] = [(start, end), ] self.__flag_sorted = False return - cpdef void sort (self): - cdef bytes chrom + @cython.ccall + def sort(self): + chrom: bytes for chrom in sorted(list(self.regions.keys())): self.regions[chrom].sort() self.__flag_sorted = True - cpdef set get_chr_names (self): + @cython.ccall + def get_chr_names(self) -> set: return set(sorted(self.regions.keys())) - cpdef void merge_overlap ( self ): + @cython.ccall + def merge_overlap(self): """ merge overlapping regions """ - cdef: - bytes chrom - int s_new_region, e_new_region, i, j - dict regions, new_regions - list chrs, regions_chr - tuple prev_region + chrom: bytes + s_new_region: cython.int + e_new_region: cython.int + i: cython.int + regions: dict + new_regions: dict + chrs: list + regions_chr: list + prev_region: tuple if not self.__flag_sorted: self.sort() regions = self.regions new_regions = {} - chrs = sorted( list( regions.keys() ) ) - for i in range( len( chrs ) ): + chrs = sorted(list(regions.keys())) + for i in range(len(chrs)): chrom = chrs[i] - new_regions[chrom]=[] + new_regions[chrom] = [] n_append = new_regions[chrom].append prev_region = None regions_chr = regions[chrom] @@ -821,7 +927,7 @@ def __init__ (self): if regions_chr[i][0] <= prev_region[1]: s_new_region = prev_region[0] e_new_region = regions_chr[i][1] - prev_region = (s_new_region,e_new_region) + prev_region = (s_new_region, e_new_region) else: n_append(prev_region) prev_region = regions_chr[i] @@ -831,43 +937,53 @@ def __init__ (self): self.sort() return - cpdef write_to_bed (self, fhd ): - cdef: - int i - bytes chrom - list chrs - tuple region + @cython.ccall + def write_to_bed(self, fhd): + i: cython.int + chrom: bytes + chrs: list + region: tuple chrs = sorted(list(self.regions.keys())) - for i in range( len(chrs) ): + for i in range(len(chrs)): chrom = chrs[i] for region in self.regions[chrom]: - fhd.write( "%s\t%d\t%d\n" % (chrom.decode(),region[0],region[1] ) ) - - -cdef class BroadPeakContent: - cdef: - long start - long end - long length - float score - bytes thickStart - bytes thickEnd - long blockNum - bytes blockSizes - bytes blockStarts - float pileup - float pscore - float fc - float qscore - bytes name - - def __init__ ( self, long start, long end, float score, - bytes thickStart, bytes thickEnd, - long blockNum, bytes blockSizes, - bytes blockStarts, float pileup, - float pscore, float fold_change, - float qscore, bytes name = b"NA" ): + fhd.write("%s\t%d\t%d\n" % (chrom.decode(), + region[0], + region[1])) + + +@cython.cclass +class BroadPeakContent: + start: cython.int + end: cython.int + length: cython.int + score: cython.float + thickStart: bytes + thickEnd: bytes + blockNum: cython.int + blockSizes: bytes + blockStarts: bytes + pileup: cython.float + pscore: cython.float + fc: cython.float + qscore: cython.float + name: bytes + + def __init__(self, + start: cython.int, + end: cython.int, + score: cython.float, + thickStart: bytes, + thickEnd: bytes, + blockNum: cython.int, + blockSizes: bytes, + blockStarts: bytes, + pileup: cython.float, + pscore: cython.float, + fold_change: cython.float, + qscore: cython.float, + name: bytes = b"NA"): self.start = start self.end = end self.score = score @@ -876,7 +992,6 @@ def __init__ ( self, long start, long end, float score, self.blockNum = blockNum self.blockSizes = blockSizes self.blockStarts = blockStarts - self.length = end - start self.pileup = pileup self.pscore = pscore @@ -884,7 +999,7 @@ def __init__ ( self, long start, long end, float score, self.qscore = qscore self.name = name - def __getitem__ ( self, a ): + def __getitem__(self, a): if a == "start": return self.start elif a == "end": @@ -914,26 +1029,36 @@ def __getitem__ ( self, a ): elif a == "name": return self.name - def __str__ (self): - return "start:%d;end:%d;score:%f" % ( self.start, self.end, self.score ) + def __str__(self): + return "start:%d;end:%d;score:%f" % (self.start, self.end, self.score) -cdef class BroadPeakIO: +@cython.cclass +class BroadPeakIO: """IO for broad peak information. """ - cdef: - dict peaks + peaks: dict - def __init__ (self): + def __init__(self): self.peaks = {} - def add (self, char * chromosome, long start, long end, long score = 0, - bytes thickStart=b".", bytes thickEnd=b".", - long blockNum=0, bytes blockSizes=b".", - bytes blockStarts=b".", float pileup = 0, - float pscore = 0, float fold_change = 0, - float qscore = 0, bytes name = b"NA" ): + @cython.ccall + def add(self, + chromosome: bytes, + start: cython.int, + end: cython.int, + score: cython.float = 0.0, + thickStart: bytes = b".", + thickEnd: bytes = b".", + blockNum: cython.int = 0, + blockSizes: bytes = b".", + blockStarts: bytes = b".", + pileup: cython.float = 0, + pscore: cython.float = 0, + fold_change: cython.float = 0, + qscore: cython.float = 0, + name: bytes = b"NA"): """items chromosome : chromosome name, start : broad region start, @@ -952,81 +1077,97 @@ def add (self, char * chromosome, long start, long end, long score = 0, """ if not self.peaks.has_key(chromosome): self.peaks[chromosome] = [] - self.peaks[chromosome].append( BroadPeakContent( start, end, score, thickStart, thickEnd, - blockNum, blockSizes, blockStarts, - pileup, pscore, fold_change, qscore, name ) ) - - def filter_pscore (self, double pscore_cut ): - cdef: - bytes chrom - dict peaks - dict new_peaks - list chrs - BroadPeakContent p + self.peaks[chromosome].append(BroadPeakContent(start, + end, + score, + thickStart, + thickEnd, + blockNum, + blockSizes, + blockStarts, + pileup, + pscore, + fold_change, + qscore, + name)) + + @cython.ccall + def filter_pscore(self, pscore_cut: cython.float): + chrom: bytes + peaks: dict + new_peaks: dict + chrs: list peaks = self.peaks new_peaks = {} chrs = list(peaks.keys()) for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in peaks[chrom] if p['pscore'] >= pscore_cut] + new_peaks[chrom] = [p for p in peaks[chrom] if p['pscore'] >= pscore_cut] self.peaks = new_peaks - def filter_qscore (self, double qscore_cut ): - cdef: - bytes chrom - dict peaks - dict new_peaks - list chrs - BroadPeakContent p + @cython.ccall + def filter_qscore(self, qscore_cut: cython.float): + chrom: bytes + peaks: dict + new_peaks: dict + chrs: list peaks = self.peaks new_peaks = {} chrs = list(peaks.keys()) for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in peaks[chrom] if p['qscore'] >= qscore_cut] + new_peaks[chrom] = [p for p in peaks[chrom] if p['qscore'] >= qscore_cut] self.peaks = new_peaks - def filter_fc (self, fc_low, fc_up=None ): + @cython.ccall + def filter_fc(self, fc_low: float, fc_up: float = -1): """Filter peaks in a given fc range. - If fc_low and fc_up is assigned, the peaks with fc in [fc_low,fc_up) + If fc_low and fc_up is assigned, the peaks with fc in + [fc_low,fc_up) + + fc_up has to be a positive number, otherwise it won't be + applied. """ - cdef: - bytes chrom - dict peaks - dict new_peaks - list chrs - BroadPeakContent p + chrom: bytes + peaks: dict + new_peaks: dict + chrs: list peaks = self.peaks new_peaks = {} chrs = list(peaks.keys()) - if fc_up: + if fc_up >= 0: for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in peaks[chrom] if p['fc'] >= fc_low and p['fc']= fc_low and p['fc'] < fc_up] else: for chrom in sorted(chrs): - new_peaks[chrom]=[p for p in peaks[chrom] if p['fc'] >= fc_low] + new_peaks[chrom] = [p for p in peaks[chrom] if p['fc'] >= fc_low] self.peaks = new_peaks - def total (self): - cdef: - bytes chrom - dict peaks - list chrs - long x + @cython.ccall + def total(self): + chrom: bytes + peaks: dict + chrs: list + x: cython.long = 0 peaks = self.peaks chrs = list(peaks.keys()) - x = 0 for chrom in sorted(chrs): x += len(peaks[chrom]) return x - def write_to_gappedPeak (self, fhd, bytes name_prefix=b"peak_", bytes name=b'peak', bytes description=b"%s", str score_column="score", trackline=True): + @cython.ccall + def write_to_gappedPeak(self, fhd, + name_prefix: bytes = b"peak_", + name: bytes = b'peak', + description: bytes = b"%s", + score_column: str = "score", + trackline: bool = True): """Print out peaks in gappedBed format. Only those with stronger enrichment regions are saved. This format is basically a BED12+3 format. @@ -1095,24 +1236,49 @@ def write_to_gappedPeak (self, fhd, bytes name_prefix=b"peak_", bytes name=b'pea +--------------+------+----------------------------------------+ """ + chrs: list + n_peak: cython.int = 0 + peak: BroadPeakContent + desc: bytes + peakprefix: bytes + chrom: bytes + chrs = list(self.peaks.keys()) - n_peak = 0 - try: peakprefix = name_prefix % name - except: peakprefix = name_prefix - try: desc = description % name - except: desc = description + try: + peakprefix = name_prefix % name + except Exception: + peakprefix = name_prefix + try: + desc = description % name + except Exception: + desc = description if trackline: - fhd.write("track name=\"%s\" description=\"%s\" type=gappedPeak nextItemButton=on\n" % (name.decode(), desc.decode()) ) + fhd.write("track name=\"%s\" description=\"%s\" type=gappedPeak nextItemButton=on\n" % (name.decode(), desc.decode())) for chrom in sorted(chrs): for peak in self.peaks[chrom]: n_peak += 1 if peak["thickStart"] != b".": - fhd.write( "%s\t%d\t%d\t%s%d\t%d\t.\t0\t0\t0\t%d\t%s\t%s\t%.6g\t%.6g\t%.6g\n" - % - (chrom.decode(),peak["start"],peak["end"],peakprefix.decode(),n_peak,int(10*peak[score_column]), - peak["blockNum"],peak["blockSizes"].decode(),peak["blockStarts"].decode(), peak['fc'], peak['pscore'], peak['qscore'] ) ) - - def write_to_Bed12 (self, fhd, bytes name_prefix=b"peak_", bytes name=b'peak', bytes description=b"%s", str score_column="score", trackline=True): + fhd.write("%s\t%d\t%d\t%s%d\t%d\t.\t0\t0\t0\t%d\t%s\t%s\t%.6g\t%.6g\t%.6g\n" % + (chrom.decode(), + peak["start"], + peak["end"], + peakprefix.decode(), + n_peak, + int(10*peak[score_column]), + peak["blockNum"], + peak["blockSizes"].decode(), + peak["blockStarts"].decode(), + peak['fc'], + peak['pscore'], + peak['qscore'])) + + @cython.ccall + def write_to_Bed12(self, fhd, + name_prefix: bytes = b"peak_", + name: bytes = b'peak', + description: bytes = b"%s", + score_column: str = "score", + trackline: bool = True): """Print out peaks in Bed12 format. +--------------+------+----------------------------------------+ @@ -1167,31 +1333,58 @@ def write_to_Bed12 (self, fhd, bytes name_prefix=b"peak_", bytes name=b'peak', b +--------------+------+----------------------------------------+ """ + chrs: list + n_peak: cython.int = 0 + peakprefix: bytes + peak: BroadPeakContent + desc: bytes + peakprefix: bytes + chrom: bytes + chrs = list(self.peaks.keys()) - n_peak = 0 - try: peakprefix = name_prefix % name - except: peakprefix = name_prefix - try: desc = description % name - except: desc = description + try: + peakprefix = name_prefix % name + except Exception: + peakprefix = name_prefix + try: + desc = description % name + except Exception: + desc = description if trackline: - fhd.write("track name=\"%s\" description=\"%s\" type=bed nextItemButton=on\n" % (name.decode(), desc.decode()) ) + fhd.write("track name=\"%s\" description=\"%s\" type=bed nextItemButton=on\n" % (name.decode(), desc.decode())) for chrom in sorted(chrs): for peak in self.peaks[chrom]: n_peak += 1 if peak["thickStart"] == b".": # this will violate gappedPeak format, since it's a complement like broadPeak line. - fhd.write( "%s\t%d\t%d\t%s%d\t%d\t.\n" - % - (chrom.decode(),peak["start"],peak["end"],peakprefix.decode(),n_peak,int(10*peak[score_column]) ) ) + fhd.write("%s\t%d\t%d\t%s%d\t%d\t.\n" % + (chrom.decode(), + peak["start"], + peak["end"], + peakprefix.decode(), + n_peak, + int(10*peak[score_column]))) else: - fhd.write( "%s\t%d\t%d\t%s%d\t%d\t.\t%s\t%s\t0\t%d\t%s\t%s\n" - % - (chrom.decode(), peak["start"], peak["end"], peakprefix.decode(), n_peak, int(10*peak[score_column]), - peak["thickStart"].decode(), peak["thickEnd"].decode(), - peak["blockNum"], peak["blockSizes"].decode(), peak["blockStarts"].decode() )) - - - def write_to_broadPeak (self, fhd, bytes name_prefix=b"peak_", bytes name=b'peak', bytes description=b"%s", str score_column="score", trackline=True): + fhd.write("%s\t%d\t%d\t%s%d\t%d\t.\t%s\t%s\t0\t%d\t%s\t%s\n" % + (chrom.decode(), + peak["start"], + peak["end"], + peakprefix.decode(), + n_peak, + int(10*peak[score_column]), + peak["thickStart"].decode(), + peak["thickEnd"].decode(), + peak["blockNum"], + peak["blockSizes"].decode(), + peak["blockStarts"].decode())) + + @cython.ccall + def write_to_broadPeak(self, fhd, + name_prefix: bytes = b"peak_", + name: bytes = b'peak', + description: bytes = b"%s", + score_column: str = "score", + trackline: bool = True): """Print out peaks in broadPeak format. This format is designed for ENCODE project, and basically a @@ -1241,16 +1434,20 @@ def write_to_broadPeak (self, fhd, bytes name_prefix=b"peak_", bytes name=b'peak +-----------+------+----------------------------------------+ """ - cdef int n_peak - cdef bytes chrom - cdef long s - cdef str peakname + chrs: list + n_peak: cython.int = 0 + peakprefix: bytes + peak: BroadPeakContent + peakprefix: bytes + chrom: bytes + peakname: str chrs = list(self.peaks.keys()) - n_peak = 0 write = fhd.write - try: peakprefix = name_prefix % name - except: peakprefix = name_prefix + try: + peakprefix = name_prefix % name + except Exception: + peakprefix = name_prefix if trackline: write("track type=broadPeak name=\"%s\" description=\"%s\" nextItemButton=on\n" % (name.decode(), name.decode())) for chrom in sorted(chrs): @@ -1259,13 +1456,21 @@ def write_to_broadPeak (self, fhd, bytes name_prefix=b"peak_", bytes name=b'peak these_peaks = list(group) peak = these_peaks[0] peakname = "%s%d" % (peakprefix.decode(), n_peak) - fhd.write( "%s\t%d\t%d\t%s\t%d\t.\t%.6g\t%.6g\t%.6g\n" % - (chrom.decode(),peak['start'],peak['end'],peakname,int(10*peak[score_column]), - peak['fc'],peak['pscore'],peak['qscore'] ) ) + fhd.write("%s\t%d\t%d\t%s\t%d\t.\t%.6g\t%.6g\t%.6g\n" % + (chrom.decode(), + peak['start'], + peak['end'], + peakname, + int(10*peak[score_column]), + peak['fc'], + peak['pscore'], + peak['qscore'])) return - - def write_to_xls (self, ofhd, bytes name_prefix=b"%s_peak_", bytes name=b"MACS"): + @cython.ccall + def write_to_xls(self, ofhd, + name_prefix: bytes = b"%s_peak_", + name: bytes = b"MACS"): """Save the peak results in a tab-delimited plain text file with suffix .xls. @@ -1273,11 +1478,21 @@ def write_to_xls (self, ofhd, bytes name_prefix=b"%s_peak_", bytes name=b"MACS") wait... why I have two write_to_xls in this class? """ + chrom: bytes + chrs: list + peakprefix: bytes + peaks: dict + these_peaks: list + peak: BroadPeakContent + peakname: str + write = ofhd.write - write("\t".join(("chr","start", "end", "length", "pileup", "-log10(pvalue)", "fold_enrichment", "-log10(qvalue)", "name"))+"\n") + write("\t".join(("chr", "start", "end", "length", "pileup", "-log10(pvalue)", "fold_enrichment", "-log10(qvalue)", "name"))+"\n") - try: peakprefix = name_prefix % name - except: peakprefix = name_prefix + try: + peakprefix = name_prefix % name + except Exception: + peakprefix = name_prefix peaks = self.peaks chrs = list(peaks.keys()) @@ -1288,11 +1503,14 @@ def write_to_xls (self, ofhd, bytes name_prefix=b"%s_peak_", bytes name=b"MACS") these_peaks = list(group) peak = these_peaks[0] peakname = "%s%d" % (peakprefix.decode(), n_peak) - write("%s\t%d\t%d\t%d" % (chrom.decode(),peak['start']+1,peak['end'],peak['length'])) - write("\t%.6g" % (round(peak['pileup'],2))) # pileup height at summit - write("\t%.6g" % (peak['pscore'])) # -log10pvalue at summit - write("\t%.6g" % (peak['fc'])) # fold change at summit - write("\t%.6g" % (peak['qscore'])) # -log10qvalue at summit + write("%s\t%d\t%d\t%d" % (chrom.decode(), + peak['start']+1, + peak['end'], + peak['length'])) + write("\t%.6g" % (round(peak['pileup'], 2))) # pileup height at summit + write("\t%.6g" % (peak['pscore'])) # -log10pvalue at summit + write("\t%.6g" % (peak['fc'])) # fold change at summit + write("\t%.6g" % (peak['qscore'])) # -log10qvalue at summit write("\t%s" % peakname) write("\n") return diff --git a/MACS3/Signal/CallPeakUnit.pyx b/MACS3/Signal/CallPeakUnit.pyx index c6ffb7b8..c83aba7e 100644 --- a/MACS3/Signal/CallPeakUnit.pyx +++ b/MACS3/Signal/CallPeakUnit.pyx @@ -1,7 +1,7 @@ # cython: language_level=3 # cython: profile=True # cython: linetrace=True -# Time-stamp: <2022-09-15 17:06:17 Tao Liu> +# Time-stamp: <2024-10-10 16:45:01 Tao Liu> """Module for Calculate Scores. @@ -46,7 +46,7 @@ from libc.math cimport exp,log,log10, M_LN10, log1p, erf, sqrt, floor, ceil # MACS3 modules # ------------------------------------ from MACS3.Signal.SignalProcessing import maxima, enforce_valleys, enforce_peakyness -from MACS3.IO.PeakIO import PeakIO, BroadPeakIO, parse_peakname +from MACS3.IO.PeakIO import PeakIO, BroadPeakIO from MACS3.Signal.FixWidthTrack import FWTrack from MACS3.Signal.PairedEndTrack import PETrackI from MACS3.Signal.Prob import poisson_cdf diff --git a/MACS3/Signal/PairedEndTrack.py b/MACS3/Signal/PairedEndTrack.py new file mode 100644 index 00000000..8273495a --- /dev/null +++ b/MACS3/Signal/PairedEndTrack.py @@ -0,0 +1,730 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-10 17:03:45 Tao Liu> + +"""Module for filter duplicate tags from paired-end data + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file LICENSE included with +the distribution). +""" + +# ------------------------------------ +# Python modules +# ------------------------------------ +import io +import sys +from array import array as pyarray +from collections import Counter + +# ------------------------------------ +# MACS3 modules +# ------------------------------------ +from MACS3.Signal.Pileup import (quick_pileup, + over_two_pv_array, + se_all_in_one_pileup) +from MACS3.Signal.BedGraph import bedGraphTrackI +from MACS3.Signal.PileupV2 import pileup_from_LR_hmmratac +# ------------------------------------ +# Other modules +# ------------------------------------ +import cython +import numpy as np +import cython.cimports.numpy as cnp +from cython.cimports.cpython import bool +from cython.cimports.libc.stdint import INT32_MAX as INT_MAX + +from MACS3.Utilities.Logger import logging + +logger = logging.getLogger(__name__) +debug = logger.debug +info = logger.info + +# Let numpy enforce PE-ness using ndarray, gives bonus speedup when sorting +# PE data doesn't have strandedness + + +@cython.cclass +class PETrackI: + """Paired End Locations Track class I along the whole genome + (commonly with the same annotation type), which are stored in a + dict. + + Locations are stored and organized by sequence names (chr names) in a + dict. They can be sorted by calling self.sort() function. + """ + __locations = cython.declare(dict, visibility="public") + __size = cython.declare(dict, visibility="public") + __buf_size = cython.declare(dict, visibility="public") + __sorted = cython.declare(bool, visibility="public") + total = cython.declare(cython.ulong, visibility="public") + annotation = cython.declare(str, visibility="public") + rlengths = cython.declare(dict, visibility="public") + buffer_size = cython.declare(cython.long, visibility="public") + length = cython.declare(cython.long, visibility="public") + average_template_length = cython.declare(cython.float, visibility="public") + __destroyed: bool + + def __init__(self, anno: str = "", buffer_size: cython.long = 100000): + """fw is the fixed-width for all locations. + + """ + # dictionary with chrname as key, nparray with + # [('l','int32'),('r','int32')] as value + self.__locations = {} + # dictionary with chrname as key, size of the above nparray as value + self.__size = {} + # dictionary with chrname as key, size of the above nparray as value + self.__buf_size = {} + self.__sorted = False + self.total = 0 # total fragments + self.annotation = anno # need to be figured out + self.rlengths = {} + self.buffer_size = buffer_size + self.length = 0 + self.average_template_length = 0.0 + + @cython.ccall + def add_loc(self, chromosome: bytes, + start: cython.int, end: cython.int): + """Add a location to the list according to the sequence name. + + chromosome -- mostly the chromosome name + fiveendpos -- 5' end pos, left for plus strand, right for neg strand + """ + i: cython.int + + if chromosome not in self.__locations: + self.__buf_size[chromosome] = self.buffer_size + # note: ['l'] is the leftmost end, ['r'] is the rightmost end of fragment. + self.__locations[chromosome] = np.zeros(shape=self.buffer_size, + dtype=[('l', 'i4'), ('r', 'i4')]) + self.__locations[chromosome][0] = (start, end) + self.__size[chromosome] = 1 + else: + i = self.__size[chromosome] + if self.__buf_size[chromosome] == i: + self.__buf_size[chromosome] += self.buffer_size + self.__locations[chromosome].resize((self.__buf_size[chromosome]), + refcheck=False) + self.__locations[chromosome][i] = (start, end) + self.__size[chromosome] = i + 1 + self.length += end - start + return + + @cython.ccall + def destroy(self): + """Destroy this object and release mem. + """ + chrs: set + chromosome: bytes + + chrs = self.get_chr_names() + for chromosome in sorted(chrs): + if chromosome in self.__locations: + self.__locations[chromosome].resize(self.buffer_size, + refcheck=False) + self.__locations[chromosome].resize(0, + refcheck=False) + self.__locations[chromosome] = None + self.__locations.pop(chromosome) + self.__destroyed = True + return + + @cython.ccall + def set_rlengths(self, rlengths: dict) -> bool: + """Set reference chromosome lengths dictionary. + + Only the chromosome existing in this petrack object will be updated. + + If a chromosome in this petrack is not covered by given + rlengths, and it has no associated length, it will be set as + maximum integer. + """ + valid_chroms: set + missed_chroms: set + chrom: bytes + + valid_chroms = set(self.__locations.keys()).intersection(rlengths.keys()) + for chrom in sorted(valid_chroms): + self.rlengths[chrom] = rlengths[chrom] + missed_chroms = set(self.__locations.keys()).difference(rlengths.keys()) + for chrom in sorted(missed_chroms): + self.rlengths[chrom] = INT_MAX + return True + + @cython.ccall + def get_rlengths(self) -> dict: + """Get reference chromosome lengths dictionary. + + If self.rlengths is empty, create a new dict where the length of + chromosome will be set as the maximum integer. + """ + if not self.rlengths: + self.rlengths = dict([(k, INT_MAX) for k in self.__locations.keys()]) + return self.rlengths + + @cython.ccall + def finalize(self): + """ Resize np arrays for 5' positions and sort them in place + + Note: If this function is called, it's impossible to append more files to this FWTrack object. So remember to call it after all the files are read! + """ + c: bytes + chrnames: set + + self.total = 0 + + chrnames = self.get_chr_names() + + for c in chrnames: + self.__locations[c].resize((self.__size[c]), refcheck=False) + self.__locations[c].sort(order=['l', 'r']) + self.total += self.__size[c] + + self.__sorted = True + self.average_template_length = cython.cast(cython.float, self.length) / self.total + return + + @cython.ccall + def get_locations_by_chr(self, chromosome: bytes): + """Return a tuple of two lists of locations for certain chromosome. + + """ + if chromosome in self.__locations: + return self.__locations[chromosome] + else: + raise Exception("No such chromosome name (%s) in TrackI object!\n" % (chromosome)) + + @cython.ccall + def get_chr_names(self) -> set: + """Return all the chromosome names in this track object as a python set. + """ + return set(self.__locations.keys()) + + @cython.ccall + def sort(self): + """Naive sorting for locations. + + """ + c: bytes + chrnames: set + + chrnames = self.get_chr_names() + + for c in chrnames: + self.__locations[c].sort(order=['l', 'r']) # sort by the leftmost location + self.__sorted = True + return + + @cython.ccall + def count_fraglengths(self) -> dict: + """Return a dictionary of the counts for sizes/fragment + lengths of each pair. + + This function is for HMMRATAC. + + """ + sizes: cnp.ndarray(cnp.int32_t, ndim=1) + s: cython.int + locs: cnp.ndarray + chrnames: list + i: cython.int + + counter = Counter() + chrnames = list(self.get_chr_names()) + for i in range(len(chrnames)): + locs = self.__locations[chrnames[i]] + sizes = locs['r'] - locs['l'] + for s in sizes: + counter[s] += 1 + return dict(counter) + + @cython.ccall + def fraglengths(self) -> cnp.ndarray: + """Return the sizes/fragment lengths of each pair. + + This function is for HMMRATAC EM training. + """ + sizes: cnp.ndarray(np.int32_t, ndim=1) + locs: cnp.ndarray + chrnames: list + i: cython.int + + chrnames = list(self.get_chr_names()) + locs = self.__locations[chrnames[0]] + sizes = locs['r'] - locs['l'] + for i in range(1, len(chrnames)): + locs = self.__locations[chrnames[i]] + sizes = np.concatenate((sizes, locs['r'] - locs['l'])) + return sizes + + @cython.boundscheck(False) # do not check that np indices are valid + @cython.ccall + def filter_dup(self, maxnum: cython.int = -1): + """Filter the duplicated reads. + + Run it right after you add all data into this object. + """ + n: cython.int + loc_start: cython.int + loc_end: cython.int + current_loc_start: cython.int + current_loc_end: cython.int + i: cython.ulong + locs_size: cython.ulong + k: bytes + locs: cnp.ndarray + chrnames: set + selected_idx: cnp.ndarray + + if maxnum < 0: + return # condition to return if not filtering + + if not self.__sorted: + self.sort() + + self.total = 0 + # self.length = 0 + self.average_template_length = 0.0 + + chrnames = self.get_chr_names() + + for k in chrnames: # for each chromosome + locs = self.__locations[k] + locs_size = locs.shape[0] + if locs_size == 1: + # do nothing and continue + continue + # discard duplicate reads and make a new __locations[k] + # initialize boolean array as all TRUE, or all being kept + selected_idx = np.ones(locs_size, dtype=bool) + # get the first loc + (current_loc_start, current_loc_end) = locs[0] + i = 1 # index of new_locs + n = 1 # the number of tags in the current genomic location + for i in range(1, locs_size): + (loc_start, loc_end) = locs[i] + if loc_start != current_loc_start or loc_end != current_loc_end: + # not the same, update currnet_loc_start/end/l, reset n + current_loc_start = loc_start + current_loc_end = loc_end + n = 1 + continue + else: + # both ends are the same, add 1 to duplicate number n + n += 1 + if n > maxnum: + # change the flag to False + selected_idx[i] = False + # subtract current_loc_l from self.length + self.length -= current_loc_end - current_loc_start + self.__locations[k] = locs[selected_idx] + self.__size[k] = self.__locations[k].shape[0] + self.total += self.__size[k] + # free memory? + # I know I should shrink it to 0 size directly, + # however, on Mac OSX, it seems directly assigning 0 + # doesn't do a thing. + selected_idx.resize(self.buffer_size, refcheck=False) + selected_idx.resize(0, refcheck=False) + self.average_template_length = self.length / self.total + return + + @cython.ccall + def sample_percent(self, percent: cython.float, seed: cython.int = -1): + """Sample the tags for a given percentage. + + Warning: the current object is changed! If a new PETrackI is + wanted, use sample_percent_copy instead. + + """ + # num: number of reads allowed on a certain chromosome + num: cython.uint + k: bytes + chrnames: set + + self.total = 0 + self.length = 0 + self.average_template_length = 0.0 + + chrnames = self.get_chr_names() + + if seed >= 0: + info(f"# A random seed {seed} has been used") + rs = np.random.RandomState(np.random.MT19937(np.random.SeedSequence(seed))) + rs_shuffle = rs.shuffle + else: + rs_shuffle = np.random.shuffle + + for k in sorted(chrnames): + # for each chromosome. + # This loop body is too big, I may need to split code later... + + num = cython.cast(cython.uint, + round(self.__locations[k].shape[0] * percent, 5)) + rs_shuffle(self.__locations[k]) + self.__locations[k].resize(num, refcheck=False) + self.__locations[k].sort(order=['l', 'r']) # sort by leftmost positions + self.__size[k] = self.__locations[k].shape[0] + self.length += (self.__locations[k]['r'] - self.__locations[k]['l']).sum() + self.total += self.__size[k] + self.average_template_length = cython.cast(cython.float, self.length)/self.total + return + + @cython.ccall + def sample_percent_copy(self, percent: cython.float, seed: cython.int = -1): + """Sample the tags for a given percentage. Return a new PETrackI object + + """ + # num: number of reads allowed on a certain chromosome + num: cython.uint + k: bytes + chrnames: set + ret_petrackI: PETrackI + loc: cnp.ndarray + + ret_petrackI = PETrackI(anno=self.annotation, buffer_size=self.buffer_size) + chrnames = self.get_chr_names() + + if seed >= 0: + info(f"# A random seed {seed} has been used in the sampling function") + rs = np.random.default_rng(seed) + else: + rs = np.random.default_rng() + + rs_shuffle = rs.shuffle + + # chrnames need to be sorted otherwise we can't assure reproducibility + for k in sorted(chrnames): + # for each chromosome. + # This loop body is too big, I may need to split code later... + loc = np.copy(self.__locations[k]) + num = cython.cast(cython.uint, round(loc.shape[0] * percent, 5)) + rs_shuffle(loc) + loc.resize(num, refcheck=False) + loc.sort(order=['l', 'r']) # sort by leftmost positions + ret_petrackI.__locations[k] = loc + ret_petrackI.__size[k] = loc.shape[0] + ret_petrackI.length += (loc['r'] - loc['l']).sum() + ret_petrackI.total += ret_petrackI.__size[k] + ret_petrackI.average_template_length = cython.cast(cython.float, ret_petrackI.length)/ret_petrackI.total + ret_petrackI.set_rlengths(self.get_rlengths()) + return ret_petrackI + + @cython.ccall + def sample_num(self, samplesize: cython.ulong, seed: cython.int = -1): + """Sample the tags for a given number. + + Warning: the current object is changed! + """ + percent: cython.float + + percent = cython.cast(cython.float, samplesize)/self.total + self.sample_percent(percent, seed) + return + + @cython.ccall + def sample_num_copy(self, samplesize: cython.ulong, seed: cython.int = -1): + """Sample the tags for a given number. + + Warning: the current object is changed! + """ + percent: cython.float + + percent = cython.cast(cython.float, samplesize)/self.total + return self.sample_percent_copy(percent, seed) + + @cython.ccall + def print_to_bed(self, fhd=None): + """Output to BEDPE format files. If fhd is given, write to a + file, otherwise, output to standard output. + + """ + i: cython.int + s: cython.int + e: cython.int + k: bytes + chrnames: set + + if not fhd: + fhd = sys.stdout + assert isinstance(fhd, io.IOBase) + + chrnames = self.get_chr_names() + + for k in chrnames: + # for each chromosome. + # This loop body is too big, I may need to split code later... + + locs = self.__locations[k] + + for i in range(locs.shape[0]): + s, e = locs[i] + fhd.write("%s\t%d\t%d\n" % (k.decode(), s, e)) + return + + @cython.ccall + def pileup_a_chromosome(self, + chrom: bytes, + scale_factor_s: list, + baseline_value: cython.float = 0.0) -> list: + """pileup a certain chromosome, return [p,v] (end position and + value) list. + + scale_factor_s : linearly scale the pileup value applied to + each d in ds. The list should have the same + length as ds. + + baseline_value : a value to be filled for missing values, and + will be the minimum pileup. + + """ + tmp_pileup: list + prev_pileup: list + scale_factor: cython.float + + prev_pileup = None + + for i in range(len(scale_factor_s)): + scale_factor = scale_factor_s[i] + + # Can't directly pass partial nparray there since that will mess up with pointer calculation. + tmp_pileup = quick_pileup(np.sort(self.__locations[chrom]['l']), + np.sort(self.__locations[chrom]['r']), + scale_factor, baseline_value) + + if prev_pileup: + prev_pileup = over_two_pv_array(prev_pileup, + tmp_pileup, + func="max") + else: + prev_pileup = tmp_pileup + + return prev_pileup + + @cython.ccall + def pileup_a_chromosome_c(self, + chrom: bytes, + ds: list, + scale_factor_s: list, + baseline_value: cython.float = 0.0) -> list: + """pileup a certain chromosome, return [p,v] (end position and + value) list. + + This function is for control track. Basically, here is a + simplified function from FixWidthTrack. We pretend the PE is + SE data and left read is on plus strand and right read is on + minus strand. + + ds : tag will be extended to this value to 3' direction, + unless directional is False. Can contain multiple + extension values. Final pileup will the maximum. + scale_factor_s : linearly scale the pileup value applied to + each d in ds. The list should have the same + length as ds. + baseline_value : a value to be filled for missing values, and + will be the minimum pileup. + """ + tmp_pileup: list + prev_pileup: list + scale_factor: cython.float + d: cython.long + five_shift: cython.long + three_shift: cython.long + rlength: cython.long = self.get_rlengths()[chrom] + + if not self.__sorted: + self.sort() + + assert len(ds) == len(scale_factor_s), "ds and scale_factor_s must have the same length!" + + prev_pileup = None + + for i in range(len(scale_factor_s)): + d = ds[i] + scale_factor = scale_factor_s[i] + five_shift = d//2 + three_shift = d//2 + + tmp_pileup = se_all_in_one_pileup(self.__locations[chrom]['l'], + self.__locations[chrom]['r'], + five_shift, + three_shift, + rlength, + scale_factor, + baseline_value) + + if prev_pileup: + prev_pileup = over_two_pv_array(prev_pileup, + tmp_pileup, + func="max") + else: + prev_pileup = tmp_pileup + + return prev_pileup + + @cython.ccall + def pileup_bdg(self, + scale_factor_s: list, + baseline_value: cython.float = 0.0): + """pileup all chromosomes, and return a bedGraphTrackI object. + + scale_factor_s : linearly scale the pileup value applied to + each d in ds. The list should have the same + length as ds. + + baseline_value : a value to be filled for missing values, and + will be the minimum pileup. + + """ + tmp_pileup: list + prev_pileup: list + scale_factor: cython.float + chrom: bytes + bdg: bedGraphTrackI + + bdg = bedGraphTrackI(baseline_value=baseline_value) + + for chrom in sorted(self.get_chr_names()): + prev_pileup = None + for i in range(len(scale_factor_s)): + scale_factor = scale_factor_s[i] + + # Can't directly pass partial nparray there since that + # will mess up with pointer calculation. + tmp_pileup = quick_pileup(np.sort(self.__locations[chrom]['l']), + np.sort(self.__locations[chrom]['r']), + scale_factor, + baseline_value) + + if prev_pileup: + prev_pileup = over_two_pv_array(prev_pileup, + tmp_pileup, + func="max") + else: + prev_pileup = tmp_pileup + # save to bedGraph + bdg.add_chrom_data(chrom, + pyarray('i', prev_pileup[0]), + pyarray('f', prev_pileup[1])) + return bdg + + @cython.ccall + def pileup_bdg_hmmr(self, + mapping: list, + baseline_value: cython.float = 0.0) -> list: + """pileup all chromosomes, and return a list of four + bedGraphTrackI objects: short, mono, di, and tri nucleosomal + signals. + + The idea is that for each fragment length, we generate four + bdg using four weights from four distributions. Then we add + all sets of four bdgs together. + + Way to generate 'mapping', based on HMMR EM means and stddevs: + fl_dict = petrack.count_fraglengths() + fl_list = list(fl_dict.keys()) + fl_list.sort() + weight_mapping = generate_weight_mapping(fl_list, em_means, em_stddevs) + + """ + ret_pileup: list + chroms: set + chrom: bytes + i: cython.int + + ret_pileup = [] + for i in range(len(mapping)): + ret_pileup.append({}) + chroms = self.get_chr_names() + for i in range(len(mapping)): + for chrom in sorted(chroms): + ret_pileup[i][chrom] = pileup_from_LR_hmmratac(self.__locations[chrom], mapping[i]) + return ret_pileup + + +@cython.cclass +class PEtrackII(PETrackI): + """Documentation for PEtrac + + """ + # add another dict for storing barcode for each fragment + __barcode = cython.declare(dict, visibility="public") + __barcode_dict = cython.declare(dict, visibility="public") + # add another dict for storing counts for each fragment + __counts = cython.declare(dict, visibility="public") + + def __init__(self, args): + super(PETrackI, self).__init__() + self.__barcodes = {} + self.__barcode_dict = {} + + @cython.ccall + def add_frag(self, + chromosome: bytes, + start: cython.int, + end: cython.int, + barcode: bytes, + count: cython.uchar): + """Add a location to the list according to the sequence name. + + chromosome: mostly the chromosome name + start: left position of the fragment + end: right position of the fragment + barcode: the barcode of the fragment + count: the count of the fragment + """ + i: cython.int + h: cython.long + + h = hash(barcode) + self.__barcode_dict[h] = barcode + + if chromosome not in self.__locations: + self.__buf_size[chromosome] = self.buffer_size + # note: ['l'] is the leftmost end, ['r'] is the rightmost end of fragment. + self.__locations[chromosome] = np.zeros(shape=self.buffer_size, + dtype=[('l', 'i4'), ('r', 'i4'), ('c', 'u1')]) + self.__barcodes[chromosome] = np.zeros(shape=self.buffer_size, + dtype='i4') + self.__locations[chromosome][0] = (start, end, count) + self.__barcodes[chromosome][0] = h + self.__size[chromosome] = 1 + else: + i = self.__size[chromosome] + if self.__buf_size[chromosome] == i: + self.__buf_size[chromosome] += self.buffer_size + self.__locations[chromosome].resize((self.__buf_size[chromosome]), + refcheck=False) + self.__locations[chromosome][i] = (start, end, count) + self.__barcodes[chromosome][i] = h + self.__size[chromosome] = i + 1 + self.length += end - start + return + + @cython.ccall + def destroy(self): + """Destroy this object and release mem. + """ + chrs: set + chromosome: bytes + + chrs = self.get_chr_names() + for chromosome in sorted(chrs): + if chromosome in self.__locations: + self.__locations[chromosome].resize(self.buffer_size, + refcheck=False) + self.__locations[chromosome].resize(0, + refcheck=False) + self.__locations[chromosome] = None + self.__locations.pop(chromosome) + self.__barcodes.resize(self.buffer_size, + refcheck=False) + self.__barcodes.resize(0, + refcheck=False) + self.__barcodes[chromosome] = None + self.__barcodes.pop(chromosome) + self.__barcode_dict = {} + self.__destroyed = True + return diff --git a/MACS3/Signal/PairedEndTrack.pyx b/MACS3/Signal/PairedEndTrack.pyx deleted file mode 100644 index 808f5d1c..00000000 --- a/MACS3/Signal/PairedEndTrack.pyx +++ /dev/null @@ -1,584 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2022-09-15 17:07:26 Tao Liu> - -"""Module for filter duplicate tags from paired-end data - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file LICENSE included with -the distribution). -""" - -# ------------------------------------ -# Python modules -# ------------------------------------ -import io -import sys -from copy import copy -from array import array as pyarray -from collections import Counter - -import logging -import MACS3.Utilities.Logger - -logger = logging.getLogger(__name__) -debug = logger.debug -info = logger.info -# ------------------------------------ -# MACS3 modules -# ------------------------------------ -from MACS3.Utilities.Constants import * -from MACS3.Signal.Pileup import quick_pileup, over_two_pv_array, se_all_in_one_pileup -from MACS3.Signal.BedGraph import bedGraphTrackI -from MACS3.Signal.PileupV2 import pileup_from_LR_hmmratac -# ------------------------------------ -# Other modules -# ------------------------------------ -import numpy as np -cimport numpy as np -from numpy cimport uint8_t, uint16_t, uint32_t, uint64_t, int8_t, int16_t, int32_t, int64_t, float32_t, float64_t -from cpython cimport bool -cimport cython - - -cdef INT_MAX = (((-1))>>1) - -# We don't use the following structs anymore -# cdef packed struct peLoc: -# int32_t l -# int32_t r - -# cdef class PETrackChromosome: -# cdef: -# public np.ndarray locations -# public uint32_t pointer -# public uint32_t buffer_size -# public uint64_t coverage -# public uint64_t chrlen -# uint32_t __buffer_increment -# bool __sorted -# bool __destroyed - -# Let numpy enforce PE-ness using ndarray, gives bonus speedup when sorting -# PE data doesn't have strandedness - -cdef class PETrackI: - """Paired End Locations Track class I along the whole genome - (commonly with the same annotation type), which are stored in a - dict. - - Locations are stored and organized by sequence names (chr names) in a - dict. They can be sorted by calling self.sort() function. - """ - cdef: - public dict __locations - public dict __size - public dict __buf_size - public bool __sorted - public uint64_t total - public object annotation - public dict rlengths - public int64_t buffer_size - public int64_t length - public float32_t average_template_length - bool __destroyed - - def __init__ (self, char * anno="", int64_t buffer_size = 100000 ): - """fw is the fixed-width for all locations. - - """ - self.__locations = {} # dictionary with chrname as key, nparray with [('l','int32'),('r','int32')] as value - self.__size = {} # dictionary with chrname as key, size of the above nparray as value - self.__buf_size = {} # dictionary with chrname as key, size of the above nparray as value - self.__sorted = False - self.total = 0 # total fragments - self.annotation = anno # need to be figured out - self.rlengths = {} - self.buffer_size = buffer_size - self.length = 0 - self.average_template_length = 0.0 - - cpdef void add_loc ( self, bytes chromosome, int32_t start, int32_t end): - """Add a location to the list according to the sequence name. - - chromosome -- mostly the chromosome name - fiveendpos -- 5' end pos, left for plus strand, right for neg strand - """ - cdef: - int32_t i - - if chromosome not in self.__locations: - self.__buf_size[chromosome] = self.buffer_size - self.__locations[chromosome] = np.zeros(shape=self.buffer_size, dtype=[('l','int32'),('r','int32')]) # note: ['l'] is the leftmost end, ['r'] is the rightmost end of fragment. - self.__locations[chromosome][0] = ( start, end ) - self.__size[chromosome] = 1 - else: - i = self.__size[chromosome] - if self.__buf_size[chromosome] == i: - self.__buf_size[chromosome] += self.buffer_size - self.__locations[chromosome].resize((self.__buf_size[chromosome]), refcheck = False ) - self.__locations[chromosome][ i ] = ( start, end ) - self.__size[chromosome] = i + 1 - self.length += end - start - return - - cpdef void destroy ( self ): - """Destroy this object and release mem. - """ - cdef: - set chrs - bytes chromosome - - chrs = self.get_chr_names() - for chromosome in sorted(chrs): - if chromosome in self.__locations: - self.__locations[chromosome].resize( self.buffer_size, refcheck=False ) - self.__locations[chromosome].resize( 0, refcheck=False ) - self.__locations[chromosome] = None - self.__locations.pop(chromosome) - self.__destroyed = True - return - - cpdef bint set_rlengths ( self, dict rlengths ): - """Set reference chromosome lengths dictionary. - - Only the chromosome existing in this petrack object will be updated. - - If a chromosome in this petrack is not covered by given - rlengths, and it has no associated length, it will be set as - maximum integer. - """ - cdef: - set valid_chroms, missed_chroms - bytes chrom - - valid_chroms = set(self.__locations.keys()).intersection(rlengths.keys()) - for chrom in sorted(valid_chroms): - self.rlengths[chrom] = rlengths[chrom] - missed_chroms = set(self.__locations.keys()).difference(rlengths.keys()) - for chrom in sorted(missed_chroms): - self.rlengths[chrom] = INT_MAX - return True - - cpdef dict get_rlengths ( self ): - """Get reference chromosome lengths dictionary. - - If self.rlengths is empty, create a new dict where the length of - chromosome will be set as the maximum integer. - """ - if not self.rlengths: - self.rlengths = dict([(k, INT_MAX) for k in self.__locations.keys()]) - return self.rlengths - - cpdef void finalize ( self ): - """ Resize np arrays for 5' positions and sort them in place - - Note: If this function is called, it's impossible to append more files to this FWTrack object. So remember to call it after all the files are read! - """ - - cdef: - int32_t i - bytes c - set chrnames - - self.total = 0 - - chrnames = self.get_chr_names() - - for c in chrnames: - self.__locations[c].resize((self.__size[c]), refcheck=False) - self.__locations[c].sort( order=['l', 'r'] ) - self.total += self.__size[c] - - self.__sorted = True - self.average_template_length = ( self.length ) / self.total - return - - cpdef get_locations_by_chr ( self, bytes chromosome ): - """Return a tuple of two lists of locations for certain chromosome. - - """ - if chromosome in self.__locations: - return self.__locations[chromosome] - else: - raise Exception("No such chromosome name (%s) in TrackI object!\n" % (chromosome)) - - cpdef set get_chr_names ( self ): - """Return all the chromosome names in this track object as a python set. - """ - return set(self.__locations.keys()) - - - cpdef void sort ( self ): - """Naive sorting for locations. - - """ - cdef: - uint32_t i - bytes c - set chrnames - - chrnames = self.get_chr_names() - - for c in chrnames: - #print "before", self.__locations[c][0:100] - self.__locations[c].sort( order=['l', 'r'] ) # sort by the leftmost location - #print "before", self.__locations[c][0:100] - self.__sorted = True - return - - cpdef dict count_fraglengths ( self ): - """Return a dictionary of the counts for sizes/fragment lengths of each pair. - - This function is for HMMRATAC. - """ - cdef: - np.ndarray[np.int32_t, ndim=1] sizes - np.int32_t s - np.ndarray locs - list chrnames - int i - #dict ret_dict - bytes k - - counter = Counter() - chrnames = list( self.get_chr_names() ) - for i in range( len(chrnames) ): - locs = self.__locations[ chrnames[i] ] - sizes = locs['r'] - locs['l'] - for s in sizes: - counter[ s ] += 1 - return dict(counter) - - cpdef np.ndarray fraglengths ( self ): - """Return the sizes/fragment lengths of each pair. - - This function is for HMMRATAC EM training. - """ - cdef: - np.ndarray[np.int32_t, ndim=1] sizes - np.ndarray locs - list chrnames - int i - - chrnames = list( self.get_chr_names() ) - locs = self.__locations[ chrnames[ 0 ] ] - sizes = locs['r'] - locs['l'] - for i in range( 1, len(chrnames) ): - locs = self.__locations[ chrnames[i] ] - sizes = np.concatenate( ( sizes, locs['r'] - locs['l'] ) ) - return sizes - - @cython.boundscheck(False) # do not check that np indices are valid - cpdef void filter_dup ( self, int32_t maxnum=-1): - """Filter the duplicated reads. - - Run it right after you add all data into this object. - """ - cdef: - int32_t i_chrom, n, start, end - int32_t loc_start, loc_end, current_loc_start, current_loc_end - uint64_t i - bytes k - np.ndarray locs - uint64_t locs_size - set chrnames - np.ndarray selected_idx - - if maxnum < 0: return # condition to return if not filtering - - if not self.__sorted: self.sort() - - self.total = 0 - #self.length = 0 - self.average_template_length = 0.0 - - chrnames = self.get_chr_names() - - for k in chrnames: # for each chromosome - locs = self.__locations[k] - locs_size = locs.shape[0] - if locs_size == 1: - # do nothing and continue - continue - # discard duplicate reads and make a new __locations[k] - # initialize boolean array as all TRUE, or all being kept - selected_idx = np.ones( locs_size, dtype=bool) - # get the first loc - ( current_loc_start, current_loc_end ) = locs[0] - i = 1 # index of new_locs - n = 1 # the number of tags in the current genomic location - for i in range(1, locs_size): - ( loc_start, loc_end ) = locs[i] - if loc_start != current_loc_start or loc_end != current_loc_end: - # not the same, update currnet_loc_start/end/l, reset n - current_loc_start = loc_start - current_loc_end = loc_end - n = 1 - continue - else: - # both ends are the same, add 1 to duplicate number n - n += 1 - if n > maxnum: - # change the flag to False - selected_idx[ i ] = False - # subtract current_loc_l from self.length - self.length -= current_loc_end - current_loc_start - self.__locations[k] = locs[ selected_idx ] - self.__size[k] = self.__locations[k].shape[0] - self.total += self.__size[k] - # free memory? - # I know I should shrink it to 0 size directly, - # however, on Mac OSX, it seems directly assigning 0 - # doesn't do a thing. - selected_idx.resize( self.buffer_size, refcheck=False) - selected_idx.resize( 0, refcheck=False) - self.average_template_length = self.length / self.total - return - - cpdef void sample_percent (self, float32_t percent, int32_t seed = -1): - """Sample the tags for a given percentage. - - Warning: the current object is changed! If a new PETrackI is wanted, use sample_percent_copy instead. - """ - cdef: - uint32_t num, i_chrom # num: number of reads allowed on a certain chromosome - bytes k - set chrnames - object rs, rs_shuffle - - self.total = 0 - self.length = 0 - self.average_template_length = 0.0 - - chrnames = self.get_chr_names() - - if seed >= 0: - info(f"# A random seed {seed} has been used") - rs = np.random.RandomState(np.random.MT19937(np.random.SeedSequence(seed))) - rs_shuffle = rs.shuffle - else: - rs_shuffle = np.random.shuffle - - for k in sorted(chrnames): - # for each chromosome. - # This loop body is too big, I may need to split code later... - - num = round(self.__locations[k].shape[0] * percent, 5 ) - rs_shuffle( self.__locations[k] ) - self.__locations[k].resize( num, refcheck = False ) - self.__locations[k].sort( order = ['l', 'r'] ) # sort by leftmost positions - self.__size[k] = self.__locations[k].shape[0] - self.length += ( self.__locations[k]['r'] - self.__locations[k]['l'] ).sum() - self.total += self.__size[k] - self.average_template_length = ( self.length )/ self.total - return - - cpdef object sample_percent_copy (self, float32_t percent, int32_t seed = -1): - """Sample the tags for a given percentage. Return a new PETrackI object - - """ - cdef: - uint32_t num, i_chrom # num: number of reads allowed on a certain chromosome - bytes k - set chrnames - object ret_petrackI, rs, rs_shuffle - np.ndarray l - - ret_petrackI = PETrackI( anno=self.annotation, buffer_size = self.buffer_size) - chrnames = self.get_chr_names() - - if seed >= 0: - info(f"# A random seed {seed} has been used in the sampling function") - rs = np.random.default_rng(seed) - else: - rs = np.random.default_rng() - - rs_shuffle = rs.shuffle - for k in sorted(chrnames): # chrnames need to be sorted otherwise we can't assure reproducibility - # for each chromosome. - # This loop body is too big, I may need to split code later... - l = np.copy( self.__locations[k] ) - num = round(l.shape[0] * percent, 5 ) - rs_shuffle( l ) - l.resize( num, refcheck = False ) - l.sort( order = ['l', 'r'] ) # sort by leftmost positions - ret_petrackI.__locations[ k ] = l - ret_petrackI.__size[ k ] = l.shape[0] - ret_petrackI.length += ( l['r'] - l['l'] ).sum() - ret_petrackI.total += ret_petrackI.__size[ k ] - ret_petrackI.average_template_length = ( ret_petrackI.length )/ ret_petrackI.total - ret_petrackI.set_rlengths( self.get_rlengths() ) - return ret_petrackI - - cpdef void sample_num (self, uint64_t samplesize, int32_t seed = -1): - """Sample the tags for a given number. - - Warning: the current object is changed! - """ - cdef: - float32_t percent - percent = (samplesize)/self.total - self.sample_percent ( percent, seed ) - return - - cpdef object sample_num_copy (self, uint64_t samplesize, int32_t seed = -1): - """Sample the tags for a given number. - - Warning: the current object is changed! - """ - cdef: - float32_t percent - percent = (samplesize)/self.total - return self.sample_percent_copy ( percent, seed ) - - cpdef void print_to_bed (self, fhd=None): - """Output to BEDPE format files. If fhd is given, write to a - file, otherwise, output to standard output. - - """ - cdef: - int32_t i, i_chrom, s, e - bytes k - set chrnames - - - if not fhd: - fhd = sys.stdout - assert isinstance(fhd, io.IOBase) - - chrnames = self.get_chr_names() - - for k in chrnames: - # for each chromosome. - # This loop body is too big, I may need to split code later... - - locs = self.__locations[k] - - for i in range(locs.shape[0]): - s, e = locs[ i ] - fhd.write("%s\t%d\t%d\n" % (k.decode(), s, e)) - return - - cpdef list pileup_a_chromosome ( self, bytes chrom, list scale_factor_s, float32_t baseline_value = 0.0 ): - """pileup a certain chromosome, return [p,v] (end position and value) list. - - scale_factor_s : linearly scale the pileup value applied to each d in ds. The list should have the same length as ds. - baseline_value : a value to be filled for missing values, and will be the minimum pileup. - """ - cdef: - list tmp_pileup, prev_pileup - float32_t scale_factor - - prev_pileup = None - - for i in range(len(scale_factor_s)): - scale_factor = scale_factor_s[i] - - tmp_pileup = quick_pileup ( np.sort(self.__locations[chrom]['l']), np.sort(self.__locations[chrom]['r']), scale_factor, baseline_value ) # Can't directly pass partial nparray there since that will mess up with pointer calculation. - - if prev_pileup: - prev_pileup = over_two_pv_array ( prev_pileup, tmp_pileup, func="max" ) - else: - prev_pileup = tmp_pileup - - return prev_pileup - - cpdef list pileup_a_chromosome_c ( self, bytes chrom, list ds, list scale_factor_s, float32_t baseline_value = 0.0 ): - """pileup a certain chromosome, return [p,v] (end position and value) list. - - This function is for control track. Basically, here is a - simplified function from FixWidthTrack. We pretend the PE is - SE data and left read is on plus strand and right read is on - minus strand. - - ds : tag will be extended to this value to 3' direction, - unless directional is False. Can contain multiple extension - values. Final pileup will the maximum. - scale_factor_s : linearly scale the pileup value applied to each d in ds. The list should have the same length as ds. - baseline_value : a value to be filled for missing values, and will be the minimum pileup. - """ - cdef: - list tmp_pileup, prev_pileup - float32_t scale_factor - int64_t d, five_shift, three_shift - int64_t rlength = self.get_rlengths()[chrom] - - if not self.__sorted: self.sort() - - assert len(ds) == len(scale_factor_s), "ds and scale_factor_s must have the same length!" - - prev_pileup = None - - for i in range(len(scale_factor_s)): - d = ds[i] - scale_factor = scale_factor_s[i] - five_shift = d//2 - three_shift= d//2 - - tmp_pileup = se_all_in_one_pileup ( self.__locations[chrom]['l'], self.__locations[chrom]['r'], five_shift, three_shift, rlength, scale_factor, baseline_value ) - - if prev_pileup: - prev_pileup = over_two_pv_array ( prev_pileup, tmp_pileup, func="max" ) - else: - prev_pileup = tmp_pileup - - return prev_pileup - - - cpdef object pileup_bdg ( self, list scale_factor_s, float32_t baseline_value = 0.0 ): - """pileup all chromosomes, and return a bedGraphTrackI object. - - scale_factor_s : linearly scale the pileup value applied to each d in ds. The list should have the same length as ds. - baseline_value : a value to be filled for missing values, and will be the minimum pileup. - """ - cdef: - list tmp_pileup, prev_pileup - float32_t scale_factor - bytes chrom - object bdg - int32_t prev_s - - #info(f"start to pileup") - bdg = bedGraphTrackI( baseline_value = baseline_value ) - - for chrom in sorted(self.get_chr_names()): - prev_pileup = None - for i in range(len(scale_factor_s)): - scale_factor = scale_factor_s[i] - - tmp_pileup = quick_pileup ( np.sort(self.__locations[chrom]['l']), np.sort(self.__locations[chrom]['r']), scale_factor, baseline_value ) # Can't directly pass partial nparray there since that will mess up with pointer calculation. - - if prev_pileup: - prev_pileup = over_two_pv_array ( prev_pileup, tmp_pileup, func="max" ) - else: - prev_pileup = tmp_pileup - # save to bedGraph - bdg.add_chrom_data( chrom, pyarray('i', prev_pileup[0]), pyarray('f', prev_pileup[1]) ) - return bdg - - cpdef list pileup_bdg_hmmr ( self, list mapping, float32_t baseline_value = 0.0 ): - """pileup all chromosomes, and return a list of four bedGraphTrackI objects: short, mono, di, and tri nucleosomal signals. - - The idea is that for each fragment length, we generate four bdg using four weights from four distributions. Then we add all sets of four bdgs together. - - Way to generate 'mapping', based on HMMR EM means and stddevs: - fl_dict = petrack.count_fraglengths() - fl_list = list(fl_dict.keys()) - fl_list.sort() - weight_mapping = generate_weight_mapping( fl_list, em_means, em_stddevs ) - """ - cdef: - list ret_pileup - set chroms - bytes chrom - int i - - ret_pileup = [] - for i in range( len(mapping) ): ret_pileup.append( {} ) - chroms = self.get_chr_names() - for i in range( len(mapping) ): - for chrom in sorted(chroms): - ret_pileup[ i ][ chrom ] = pileup_from_LR_hmmratac( self.__locations[ chrom ], mapping[ i ] ) - return ret_pileup - diff --git a/MACS3/Signal/ScoreTrack.pyx b/MACS3/Signal/ScoreTrack.pyx index 0426b18a..1ef3d31b 100644 --- a/MACS3/Signal/ScoreTrack.pyx +++ b/MACS3/Signal/ScoreTrack.pyx @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-05-14 12:06:19 Tao Liu> +# Time-stamp: <2024-10-10 16:45:13 Tao Liu> """Module for Feature IO classes. @@ -20,7 +20,7 @@ from functools import reduce # ------------------------------------ from MACS3.Signal.SignalProcessing import maxima, enforce_valleys, enforce_peakyness from MACS3.Signal.Prob import poisson_cdf -from MACS3.IO.PeakIO import PeakIO, BroadPeakIO, parse_peakname +from MACS3.IO.PeakIO import PeakIO, BroadPeakIO # ------------------------------------ # Other modules diff --git a/setup.py b/setup.py index a36e558b..65d78062 100644 --- a/setup.py +++ b/setup.py @@ -120,7 +120,7 @@ def main(): include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.PairedEndTrack", - ["MACS3/Signal/PairedEndTrack.pyx"], + ["MACS3/Signal/PairedEndTrack.py"], include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.BedGraph", @@ -188,7 +188,7 @@ def main(): ["MACS3/IO/Parser.py"], extra_compile_args=extra_c_args), Extension("MACS3.IO.PeakIO", - ["MACS3/IO/PeakIO.pyx"], + ["MACS3/IO/PeakIO.py"], extra_compile_args=extra_c_args), Extension("MACS3.IO.BedGraphIO", ["MACS3/IO/BedGraphIO.py"], From fba8d36b5313e0713e04e19fe4d73ba45aa64716 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Fri, 11 Oct 2024 11:27:18 -0400 Subject: [PATCH 02/13] update --- MACS3/Commands/callvar_cmd.py | 8 ++--- MACS3/Commands/refinepeak_cmd.py | 2 +- MACS3/IO/PeakIO.py | 50 +++++++++++++------------------- MACS3/Signal/FixWidthTrack.pyx | 2 +- MACS3/Signal/PairedEndTrack.py | 3 +- test/test_ScoreTrack.py | 10 +++---- 6 files changed, 33 insertions(+), 42 deletions(-) diff --git a/MACS3/Commands/callvar_cmd.py b/MACS3/Commands/callvar_cmd.py index 7f1a8097..cbd900fc 100644 --- a/MACS3/Commands/callvar_cmd.py +++ b/MACS3/Commands/callvar_cmd.py @@ -1,4 +1,4 @@ -# Time-stamp: <2024-10-02 16:34:23 Tao Liu> +# Time-stamp: <2024-10-11 10:28:07 Tao Liu> """Description: Call variants directly @@ -137,11 +137,11 @@ def run(args): peakio = open(peakbedfile) peaks = PeakIO() - i = 0 + #i = 0 for t_peak in peakio: fs = t_peak.rstrip().split() - i += 1 - peaks.add(fs[0].encode(), int(fs[1]), int(fs[2]), name=b"%d" % i) + # i += 1 + peaks.add(fs[0].encode(), int(fs[1]), int(fs[2])) # , name=b"%d" % i) peaks.sort() # chrs = peaks.get_chr_names() diff --git a/MACS3/Commands/refinepeak_cmd.py b/MACS3/Commands/refinepeak_cmd.py index 47f7610a..ba9a4939 100644 --- a/MACS3/Commands/refinepeak_cmd.py +++ b/MACS3/Commands/refinepeak_cmd.py @@ -1,4 +1,4 @@ -# Time-stamp: <2024-10-02 17:01:42 Tao Liu> +# Time-stamp: <2024-10-11 11:11:00 Tao Liu> """Description: refine peak summits diff --git a/MACS3/IO/PeakIO.py b/MACS3/IO/PeakIO.py index 433dafbf..9ba1496f 100644 --- a/MACS3/IO/PeakIO.py +++ b/MACS3/IO/PeakIO.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-10 17:00:18 Tao Liu> +# Time-stamp: <2024-10-11 11:13:11 Tao Liu> """Module for PeakIO IO classes. @@ -15,7 +15,6 @@ from itertools import groupby from operator import itemgetter import random -import re import sys # ------------------------------------ @@ -75,7 +74,7 @@ def __init__(self, pscore: cython.float, fold_change: cython.float, qscore: cython.float, - name: bytes = b"NA"): + name: bytes = b""): self.chrom = chrom self.start = start self.end = end @@ -163,26 +162,15 @@ def __init__(self): @cython.ccall def add(self, chromosome: bytes, - start: cython.int, - end: cython.int, - summit: cython.int = 0, - peak_score: cython.float = 0, - pileup: cython.float = 0, - pscore: cython.float = 0, - fold_change: cython.float = 0, - qscore: cython.float = 0, - name: bytes = b"NA"): - """items: - start:start - end:end, - length:end-start, - summit:summit, - score:peak_score, - pileup:pileup, - pscore:pscore, - fc:fold_change, - qscore:qscore - """ + start: cython.int, # leftmost position + end: cython.int, # rightmost position + summit: cython.int = 0, # summit position + peak_score: cython.float = 0, # score + pileup: cython.float = 0, # pileup value + pscore: cython.float = 0, # -log10 pvalue + fold_change: cython.float = 0, # fold change + qscore: cython.float = 0, # -log10 qvalue + name: bytes = b""): # peak name if not self.peaks.has_key(chromosome): self.peaks[chromosome] = [] self.peaks[chromosome].append(PeakContent(chromosome, @@ -215,7 +203,7 @@ def get_data_from_chrom(self, chrom: bytes) -> list: return self.peaks[chrom] def get_chr_names(self) -> set: - return set(sorted(self.peaks.keys())) + return set(self.peaks.keys()) def sort(self): chrs: list @@ -342,6 +330,8 @@ def __str__(self): chrs: list n_peak: cython.int ret: str + chrom: bytes + peaks: list ret = "" chrs = list(self.peaks.keys()) @@ -448,7 +438,7 @@ def _to_summits_bed(self, print_func("%s\t%d\t%d\t%s%d\t%.6g\n" % (chrom.decode(), summit_p, summit_p+1, peakprefix.decode(), n_peak, peak[score_column])) def tobed(self): - """Print out peaks in BED5 format. + """Print out (stdout) peaks in BED5 format. Five columns are chromosome, peak start, peak end, peak name, and peak height. @@ -462,19 +452,19 @@ def tobed(self): fc:fold_change, qscore:qvalue """ - return self._to_bed(name_prefix=b"peak_", score_column="score", name=b"", description=b"") + return self._to_bed(name_prefix=b"%s_peak_", score_column="score", name=self.name, description=b"") def to_summits_bed(self): - """Print out peak summits in BED5 format. + """Print out (stdout) peak summits in BED5 format. Five columns are chromosome, summit start, summit end, peak name, and peak height. """ - return self._to_summits_bed(name_prefix=b"peak_", score_column="score", name=b"", description=b"") + return self._to_summits_bed(name_prefix=b"%s_peak_", score_column="score", name=self.name, description=b"") # these methods are very fast, specifying types is unnecessary def write_to_bed(self, fhd, - name_prefix: bytes = b"peak_", + name_prefix: bytes = b"%s_peak_", name: bytes = b"MACS", description: bytes = b"%s", score_column: str = "score", @@ -538,7 +528,7 @@ def write_to_summit_bed(self, fhd, def write_to_narrowPeak(self, fhd, name_prefix: bytes = b"%s_peak_", - name: bytes = b"peak", + name: bytes = b"MACS", score_column: str = "score", trackline: bool = False): """Print out peaks in narrowPeak format. diff --git a/MACS3/Signal/FixWidthTrack.pyx b/MACS3/Signal/FixWidthTrack.pyx index 077d6324..01e5e0f6 100644 --- a/MACS3/Signal/FixWidthTrack.pyx +++ b/MACS3/Signal/FixWidthTrack.pyx @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2022-09-15 17:17:37 Tao Liu> +# Time-stamp: <2024-10-11 11:11:10 Tao Liu> """Module for FWTrack classes. diff --git a/MACS3/Signal/PairedEndTrack.py b/MACS3/Signal/PairedEndTrack.py index 8273495a..848bc55b 100644 --- a/MACS3/Signal/PairedEndTrack.py +++ b/MACS3/Signal/PairedEndTrack.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-10 17:03:45 Tao Liu> +# Time-stamp: <2024-10-11 11:21:30 Tao Liu> """Module for filter duplicate tags from paired-end data @@ -684,6 +684,7 @@ def add_frag(self, if chromosome not in self.__locations: self.__buf_size[chromosome] = self.buffer_size # note: ['l'] is the leftmost end, ['r'] is the rightmost end of fragment. + # ['c'] is the count number of this fragment self.__locations[chromosome] = np.zeros(shape=self.buffer_size, dtype=[('l', 'i4'), ('r', 'i4'), ('c', 'u1')]) self.__barcodes[chromosome] = np.zeros(shape=self.buffer_size, diff --git a/test/test_ScoreTrack.py b/test/test_ScoreTrack.py index 61eadc4e..41eaeb98 100644 --- a/test/test_ScoreTrack.py +++ b/test/test_ScoreTrack.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Time-stamp: <2020-11-30 14:12:58 Tao Liu> +# Time-stamp: <2024-10-11 10:17:53 Tao Liu> import io import unittest @@ -96,11 +96,11 @@ def setUp(self): chrY 160 210 6.40804 """ # for peak calls - self.peak1 = """chrY 0 60 peak_1 60.4891 -chrY 160 210 peak_2 6.40804 + self.peak1 = """chrY 0 60 MACS_peak_1 60.4891 +chrY 160 210 MACS_peak_2 6.40804 """ - self.summit1 = """chrY 5 6 peak_1 60.4891 -chrY 185 186 peak_2 6.40804 + self.summit1 = """chrY 5 6 MACS_peak_1 60.4891 +chrY 185 186 MACS_peak_2 6.40804 """ self.xls1 ="""chr start end length abs_summit pileup -log10(pvalue) fold_enrichment -log10(qvalue) name chrY 1 60 60 6 100 63.2725 9.18182 -1 MACS_peak_1 From 1b494bff7a25acd419829e8c660659e2443ad0a6 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Mon, 14 Oct 2024 15:00:50 -0400 Subject: [PATCH 03/13] rewrite some pyx files --- MACS3/Signal/FixWidthTrack.py | 699 +++++++++++++++++++++++++++++++++ MACS3/Signal/FixWidthTrack.pyx | 608 ---------------------------- MACS3/Signal/PairedEndTrack.py | 466 ++++++++++++++++------ MACS3/Signal/Pileup.py | 2 +- setup.py | 2 +- test/test_PairedEndTrack.py | 116 ++++-- 6 files changed, 1123 insertions(+), 770 deletions(-) create mode 100644 MACS3/Signal/FixWidthTrack.py delete mode 100644 MACS3/Signal/FixWidthTrack.pyx diff --git a/MACS3/Signal/FixWidthTrack.py b/MACS3/Signal/FixWidthTrack.py new file mode 100644 index 00000000..9774c236 --- /dev/null +++ b/MACS3/Signal/FixWidthTrack.py @@ -0,0 +1,699 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-14 14:53:06 Tao Liu> + +"""Module for FWTrack classes. + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file LICENSE included with +the distribution). +""" + +# ------------------------------------ +# python modules +# ------------------------------------ +import sys +import io + +# ------------------------------------ +# MACS3 modules +# ------------------------------------ + +from MACS3.IO.PeakIO import PeakIO +from MACS3.Signal.Pileup import se_all_in_one_pileup, over_two_pv_array + +# ------------------------------------ +# Other modules +# ------------------------------------ +import cython +import numpy as np +from cython.cimports.cpython import bool +import cython.cimports.numpy as cnp +from cython.cimports.libc.stdint import INT32_MAX as INT_MAX + +# ------------------------------------ +# constants +# ------------------------------------ + +# ------------------------------------ +# Misc functions +# ------------------------------------ + +# ------------------------------------ +# Classes +# ------------------------------------ + + +@cython.cclass +class FWTrack: + """Fixed Width Locations Track class along the whole genome + (commonly with the same annotation type), which are stored in a + dict. + + Locations are stored and organized by sequence names (chr names) in a + dict. They can be sorted by calling self.sort() function. + """ + locations: dict + pointer: dict + buf_size: dict + rlengths: dict + is_sorted: bool + is_destroyed: bool + total = cython.declare(cython.ulong, visibility="public") + annotation = cython.declare(str, visibility="public") + buffer_size = cython.declare(cython.long, visibility="public") + length = cython.declare(cython.long, visibility="public") + fw = cython.declare(cython.int, visibility="public") + + def __init__(self, + fw: cython.int = 0, + anno: str = "", + buffer_size: cython.long = 100000): + """fw is the fixed-width for all locations. + + """ + self.fw = fw + self.locations = {} # location pairs: two strands + self.pointer = {} # location pairs + self.buf_size = {} # location pairs + self.is_sorted = False + self.total = 0 # total tags + self.annotation = anno # need to be figured out + # lengths of reference sequences, e.g. each chromosome in a genome + self.rlengths = {} + self.buffer_size = buffer_size + self.length = 0 + self.is_destroyed = False + + @cython.ccall + def destroy(self): + """Destroy this object and release mem. + """ + chrs: set + chromosome: bytes + + chrs = self.get_chr_names() + for chromosome in sorted(chrs): + if chromosome in self.locations: + self.locations[chromosome][0].resize(self.buffer_size, + refcheck=False) + self.locations[chromosome][0].resize(0, + refcheck=False) + self.locations[chromosome][1].resize(self.buffer_size, + refcheck=False) + self.locations[chromosome][1].resize(0, + refcheck=False) + self.locations[chromosome] = [None, None] + self.locations.pop(chromosome) + self.is_destroyed = True + return + + @cython.ccall + def add_loc(self, + chromosome: bytes, + fiveendpos: cython.int, + strand: cython.int): + """Add a location to the list according to the sequence name. + + chromosome -- mostly the chromosome name + fiveendpos -- 5' end pos, left for plus strand, right for minus strand + strand -- 0: plus, 1: minus + """ + i: cython.int + b: cython.int + arr: cnp.ndarray + + if chromosome not in self.locations: + self.buf_size[chromosome] = [self.buffer_size, self.buffer_size] + self.locations[chromosome] = [np.zeros(self.buffer_size, dtype='i4'), + np.zeros(self.buffer_size, dtype='i4')] + self.pointer[chromosome] = [0, 0] + self.locations[chromosome][strand][0] = fiveendpos + self.pointer[chromosome][strand] = 1 + else: + i = self.pointer[chromosome][strand] + b = self.buf_size[chromosome][strand] + arr = self.locations[chromosome][strand] + if b == i: + b += self.buffer_size + arr.resize(b, refcheck=False) + self.buf_size[chromosome][strand] = b + arr[i] = fiveendpos + self.pointer[chromosome][strand] += 1 + return + + @cython.ccall + def finalize(self): + """ Resize np arrays for 5' positions and sort them in place + + Note: If this function is called, it's impossible to append more files to this FWTrack object. So remember to call it after all the files are read! + """ + c: bytes + chrnames: set + + self.total = 0 + + chrnames = self.get_chr_names() + + for c in chrnames: + self.locations[c][0].resize(self.pointer[c][0], refcheck=False) + self.locations[c][0].sort() + self.locations[c][1].resize(self.pointer[c][1], refcheck=False) + self.locations[c][1].sort() + self.total += self.locations[c][0].size + self.locations[c][1].size + + self.is_sorted = True + self.length = self.fw * self.total + return + + @cython.ccall + def set_rlengths(self, rlengths: dict) -> bool: + """Set reference chromosome lengths dictionary. + + Only the chromosome existing in this fwtrack object will be updated. + + If chromosome in this fwtrack is not covered by given + rlengths, and it has no associated length, it will be set as + maximum integer. + + """ + valid_chroms: set + missed_chroms: set + chrom: bytes + + valid_chroms = set(self.locations.keys()).intersection(rlengths.keys()) + for chrom in sorted(valid_chroms): + self.rlengths[chrom] = rlengths[chrom] + missed_chroms = set(self.locations.keys()).difference(rlengths.keys()) + for chrom in sorted(missed_chroms): + self.rlengths[chrom] = INT_MAX + return True + + @cython.ccall + def get_rlengths(self) -> dict: + """Get reference chromosome lengths dictionary. + + If self.rlength is empty, create a new dict where the length of + chromosome will be set as the maximum integer. + """ + if not self.rlengths: + self.rlengths = dict([(k, INT_MAX) for k in self.locations.keys()]) + return self.rlengths + + @cython.ccall + def get_locations_by_chr(self, chromosome: bytes): + """Return a tuple of two lists of locations for certain chromosome. + + """ + if chromosome in self.locations: + return self.locations[chromosome] + else: + raise Exception("No such chromosome name (%s) in TrackI object!\n" % (chromosome)) + + @cython.ccall + def get_chr_names(self) -> set: + """Return all the chromosome names stored in this track object. + """ + return set(sorted(self.locations.keys())) + + @cython.ccall + def sort(self): + """Naive sorting for locations. + + """ + c: bytes + chrnames: set + + chrnames = self.get_chr_names() + + for c in chrnames: + self.locations[c][0].sort() + self.locations[c][1].sort() + + self.is_sorted = True + return + + @cython.boundscheck(False) # do not check that np indices are valid + @cython.ccall + def filter_dup(self, maxnum: cython.int = -1) -> cython.ulong: + """Filter the duplicated reads. + + Run it right after you add all data into this object. + + Note, this function will *throw out* duplicates + permenantly. If you want to keep them, use separate_dups + instead. + """ + p: cython.int + n: cython.int + current_loc: cython.int + # index for old array, and index for new one + i_old: cython.ulong + i_new: cython.ulong + size: cython.ulong + k: bytes + plus: cnp.ndarray(cython.int, ndim=1) + new_plus: cnp.ndarray(cython.int, ndim=1) + minus: cnp.ndarray(cython.int, ndim=1) + new_minus: cnp.ndarray(cython.int, ndim=1) + chrnames: set + + if maxnum < 0: + return self.total # do nothing + + if not self.is_sorted: + self.sort() + + self.total = 0 + self.length = 0 + + chrnames = self.get_chr_names() + + for k in chrnames: + # for each chromosome. + # This loop body is too big, I may need to split code later... + + # + strand + i_new = 0 + plus = self.locations[k][0] + size = plus.shape[0] + if len(plus) <= 1: + new_plus = plus # do nothing + else: + new_plus = np.zeros(self.pointer[k][0] + 1, dtype='i4') + new_plus[i_new] = plus[i_new] # first item + i_new += 1 + # the number of tags in the current location + n = 1 + current_loc = plus[0] + for i_old in range(1, size): + p = plus[i_old] + if p == current_loc: + n += 1 + else: + current_loc = p + n = 1 + if n <= maxnum: + new_plus[i_new] = p + i_new += 1 + new_plus.resize(i_new, refcheck=False) + self.total += i_new + self.pointer[k][0] = i_new + # free memory? + # I know I should shrink it to 0 size directly, + # however, on Mac OSX, it seems directly assigning 0 + # doesn't do a thing. + plus.resize(self.buffer_size, refcheck=False) + plus.resize(0, refcheck=False) + # hope there would be no mem leak... + + # - strand + i_new = 0 + minus = self.locations[k][1] + size = minus.shape[0] + if len(minus) <= 1: + new_minus = minus # do nothing + else: + new_minus = np.zeros(self.pointer[k][1] + 1, + dtype='i4') + new_minus[i_new] = minus[i_new] # first item + i_new += 1 + # the number of tags in the current location + n = 1 + current_loc = minus[0] + for i_old in range(1, size): + p = minus[i_old] + if p == current_loc: + n += 1 + else: + current_loc = p + n = 1 + if n <= maxnum: + new_minus[i_new] = p + i_new += 1 + new_minus.resize(i_new, refcheck=False) + self.total += i_new + self.pointer[k][1] = i_new + # free memory ? + # I know I should shrink it to 0 size directly, + # however, on Mac OSX, it seems directly assigning 0 + # doesn't do a thing. + minus.resize(self.buffer_size, refcheck=False) + minus.resize(0, refcheck=False) + # hope there would be no mem leak... + + self.locations[k] = [new_plus, new_minus] + + self.length = self.fw * self.total + return self.total + + @cython.ccall + def sample_percent(self, percent: cython.float, seed: cython.int = -1): + """Sample the tags for a given percentage. + + Warning: the current object is changed! + """ + num: cython.int # num: number of reads allowed on a certain chromosome + k: bytes + chrnames: set + + self.total = 0 + self.length = 0 + + chrnames = self.get_chr_names() + + if seed >= 0: + np.random.seed(seed) + + for k in chrnames: + # for each chromosome. + # This loop body is too big, I may need to split code later... + + num = cython.cast(cython.int, + round(self.locations[k][0].shape[0] * percent, 5)) + np.random.shuffle(self.locations[k][0]) + self.locations[k][0].resize(num, refcheck=False) + self.locations[k][0].sort() + self.pointer[k][0] = self.locations[k][0].shape[0] + + num = cython.cast(cython.int, + round(self.locations[k][1].shape[0] * percent, 5)) + np.random.shuffle(self.locations[k][1]) + self.locations[k][1].resize(num, refcheck=False) + self.locations[k][1].sort() + self.pointer[k][1] = self.locations[k][1].shape[0] + + self.total += self.pointer[k][0] + self.pointer[k][1] + + self.length = self.fw * self.total + return + + @cython.ccall + def sample_num(self, samplesize: cython.ulong, seed: cython.int = -1): + """Sample the tags for a given percentage. + + Warning: the current object is changed! + """ + percent: cython.float + + percent = cython.cast(cython.float, samplesize) / self.total + self.sample_percent(percent, seed) + return + + @cython.ccall + def print_to_bed(self, fhd=None): + """Output FWTrack to BED format files. If fhd is given, + write to a file, otherwise, output to standard output. + + """ + i: cython.int + p: cython.int + k: bytes + chrnames: set + + if not fhd: + fhd = sys.stdout + assert isinstance(fhd, io.IOBase) + assert self.fw > 0, "FWTrack object .fw should be set larger than 0!" + + chrnames = self.get_chr_names() + + for k in chrnames: + # for each chromosome. + # This loop body is too big, I may need to split code later... + + plus = self.locations[k][0] + + for i in range(plus.shape[0]): + p = plus[i] + fhd.write("%s\t%d\t%d\t.\t.\t%s\n" % (k.decode(), + p, + p + self.fw, + "+")) + + minus = self.locations[k][1] + + for i in range(minus.shape[0]): + p = minus[i] + fhd.write("%s\t%d\t%d\t.\t.\t%s\n" % (k.decode(), + p-self.fw, + p, + "-")) + return + + @cython.ccall + def extract_region_tags(self, chromosome: bytes, + startpos: cython.int, endpos: cython.int) -> tuple: + i: cython.int + pos: cython.int + rt_plus: np.ndarray(cython.int, ndim=1) + rt_minus: np.ndarray(cython.int, ndim=1) + temp: list + chrnames: set + + if not self.is_sorted: + self.sort() + + chrnames = self.get_chr_names() + assert chromosome in chrnames, "chromosome %s can't be found in the FWTrack object." % chromosome + + (plus, minus) = self.locations[chromosome] + + temp = [] + for i in range(plus.shape[0]): + pos = plus[i] + if pos < startpos: + continue + elif pos > endpos: + break + else: + temp.append(pos) + rt_plus = np.array(temp) + + temp = [] + for i in range(minus.shape[0]): + pos = minus[i] + if pos < startpos: + continue + elif pos > endpos: + break + else: + temp.append(pos) + rt_minus = np.array(temp) + return (rt_plus, rt_minus) + + @cython.ccall + def compute_region_tags_from_peaks(self, peaks: PeakIO, + func, + window_size: cython.int = 100, + cutoff: cython.float = 5.0) -> list: + """Extract tags in peak, then apply func on extracted tags. + + peaks: redefined regions to extract raw tags in PeakIO type: check cPeakIO.pyx. + + func: a function to compute *something* from tags found in a predefined region + + window_size: this will be passed to func. + + cutoff: this will be passed to func. + + func needs the fixed number of parameters, so it's not flexible. Here is an example: + + wtd_find_summit(chrom, plus, minus, peak_start, peak_end, name , window_size, cutoff): + + """ + m: cython.int + i: cython.int + j: cython.int + pos: cython.int + startpos: cython.int + endpos: cython.int + + plus: cnp.ndarray(cython.int, ndim=1) + minus: cnp.ndarray(cython.int, ndim=1) + rt_plus: cnp.ndarray(cython.int, ndim=1) + rt_minus: cnp.ndarray(cython.int, ndim=1) + + chrom: bytes + name: bytes + + temp: list + retval: list + pchrnames: set + chrnames: set + + pchrnames = peaks.get_chr_names() + retval = [] + + # this object should be sorted + if not self.is_sorted: + self.sort() + # PeakIO object should be sorted + peaks.sort() + + chrnames = self.get_chr_names() + + for chrom in sorted(pchrnames): + assert chrom in chrnames, "chromosome %s can't be found in the FWTrack object." % chrom + (plus, minus) = self.locations[chrom] + cpeaks = peaks.get_data_from_chrom(chrom) + prev_i = 0 + prev_j = 0 + for m in range(len(cpeaks)): + startpos = cpeaks[m]["start"] - window_size + endpos = cpeaks[m]["end"] + window_size + name = cpeaks[m]["name"] + + temp = [] + for i in range(prev_i, plus.shape[0]): + pos = plus[i] + if pos < startpos: + continue + elif pos > endpos: + prev_i = i + break + else: + temp.append(pos) + rt_plus = np.array(temp, dtype="i4") + + temp = [] + for j in range(prev_j, minus.shape[0]): + pos = minus[j] + if pos < startpos: + continue + elif pos > endpos: + prev_j = j + break + else: + temp.append(pos) + rt_minus = np.array(temp, dtype="i4") + + retval.append(func(chrom, rt_plus, rt_minus, startpos, endpos, + name=name, + window_size=window_size, + cutoff=cutoff)) + # rewind window_size + for i in range(prev_i, 0, -1): + if plus[prev_i] - plus[i] >= window_size: + break + prev_i = i + + for j in range(prev_j, 0, -1): + if minus[prev_j] - minus[j] >= window_size: + break + prev_j = j + # end of a loop + + return retval + + @cython.ccall + def pileup_a_chromosome(self, chrom: bytes, ds: list, + scale_factor_s: list, + baseline_value: cython.float = 0.0, + directional: bool = True, + end_shift: cython.int = 0) -> list: + """pileup a certain chromosome, return [p,v] (end position and + value) list. + + ds : tag will be extended to this value to 3' direction, + unless directional is False. Can contain multiple + extension values. Final pileup will the maximum. + + scale_factor_s : linearly scale the pileup value applied to + each d in ds. The list should have the same + length as ds. + + baseline_value : a value to be filled for missing values, and + will be the minimum pileup. + + directional : if False, the strand or direction of tag will be + ignored, so that extension will be both sides + with d/2. + + end_shift : move cutting ends towards 5->3 direction if value + is positive, or towards 3->5 direction if + negative. Default is 0 -- no shift at all. + + p and v are numpy.ndarray objects. + """ + d: cython.long + five_shift: cython.long + # adjustment to 5' end and 3' end positions to make a fragment + three_shift: cython.long + rlength: cython.long + chrlengths: dict + five_shift_s: list = [] + three_shift_s: list = [] + tmp_pileup: list + prev_pileup: list + + chrlengths = self.get_rlengths() + rlength = chrlengths[chrom] + assert len(ds) == len(scale_factor_s), "ds and scale_factor_s must have the same length!" + + # adjust extension length according to 'directional' and + # 'halfextension' setting. + for d in ds: + if directional: + # only extend to 3' side + five_shift_s.append(- end_shift) + three_shift_s.append(end_shift + d) + else: + # both sides + five_shift_s.append(d//2 - end_shift) + three_shift_s.append(end_shift + d - d//2) + + prev_pileup = None + + for i in range(len(ds)): + five_shift = five_shift_s[i] + three_shift = three_shift_s[i] + scale_factor = scale_factor_s[i] + tmp_pileup = se_all_in_one_pileup(self.locations[chrom][0], + self.locations[chrom][1], + five_shift, + three_shift, + rlength, + scale_factor, + baseline_value) + + if prev_pileup: + prev_pileup = over_two_pv_array(prev_pileup, + tmp_pileup, + func="max") + else: + prev_pileup = tmp_pileup + + return prev_pileup + + +@cython.inline +@cython.cfunc +def left_sum(data, + pos: cython.int, + width: cython.int) -> cython.int: + return sum([data[x] for x in data if x <= pos and x >= pos - width]) + + +@cython.inline +@cython.cfunc +def right_sum(data, + pos: cython.int, + width: cython.int) -> cython.int: + return sum([data[x] for x in data if x >= pos and x <= pos + width]) + + +@cython.inline +@cython.cfunc +def left_forward(data, + pos: cython.int, + window_size: cython.int) -> cython.int: + return data.get(pos, 0) - data.get(pos-window_size, 0) + + +@cython.inline +@cython.cfunc +def right_forward(data, + pos: cython.int, + window_size: cython.int) -> cython.int: + return data.get(pos + window_size, 0) - data.get(pos, 0) diff --git a/MACS3/Signal/FixWidthTrack.pyx b/MACS3/Signal/FixWidthTrack.pyx deleted file mode 100644 index 01e5e0f6..00000000 --- a/MACS3/Signal/FixWidthTrack.pyx +++ /dev/null @@ -1,608 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2024-10-11 11:11:10 Tao Liu> - -"""Module for FWTrack classes. - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file LICENSE included with -the distribution). -""" - -# ------------------------------------ -# python modules -# ------------------------------------ -import sys -import io -from copy import copy -from collections import Counter - -# ------------------------------------ -# MACS3 modules -# ------------------------------------ - -from MACS3.Utilities.Constants import * -from MACS3.Signal.SignalProcessing import * -from MACS3.IO.PeakIO import PeakIO -from MACS3.Signal.Pileup import se_all_in_one_pileup, over_two_pv_array - -# ------------------------------------ -# Other modules -# ------------------------------------ -from cpython cimport bool -cimport cython -import numpy as np -cimport numpy as np -from numpy cimport uint8_t, uint16_t, uint32_t, uint64_t, int8_t, int16_t, int32_t, int64_t, float32_t, float64_t - -# ------------------------------------ -# constants -# ------------------------------------ -__version__ = "FixWidthTrack $Revision$" -__author__ = "Tao Liu " -__doc__ = "FWTrack class" - -cdef INT_MAX = (((-1))>>1) - -# ------------------------------------ -# Misc functions -# ------------------------------------ - -# ------------------------------------ -# Classes -# ------------------------------------ - -cdef class FWTrack: - """Fixed Width Locations Track class along the whole genome - (commonly with the same annotation type), which are stored in a - dict. - - Locations are stored and organized by sequence names (chr names) in a - dict. They can be sorted by calling self.sort() function. - """ - cdef: - dict __locations - dict __pointer - dict __buf_size - bool __sorted - bool __destroyed - dict rlengths - public int64_t buffer_size - public int64_t total - public object annotation - public object dups - public int32_t fw - public int64_t length - - def __init__ (self, int32_t fw=0, char * anno="", int64_t buffer_size = 100000 ): - """fw is the fixed-width for all locations. - - """ - self.fw = fw - self.__locations = {} # location pairs: two strands - self.__pointer = {} # location pairs - self.__buf_size = {} # location pairs - self.__sorted = False - self.total = 0 # total tags - self.annotation = anno # need to be figured out - self.rlengths = {} # lengths of reference sequences, e.g. each chromosome in a genome - self.buffer_size = buffer_size - self.length = 0 - self.__destroyed = False - - cpdef void destroy ( self ): - """Destroy this object and release mem. - """ - cdef: - set chrs - bytes chromosome - - chrs = self.get_chr_names() - for chromosome in sorted(chrs): - if chromosome in self.__locations: - self.__locations[chromosome][0].resize( self.buffer_size, refcheck=False ) - self.__locations[chromosome][0].resize( 0, refcheck=False ) - self.__locations[chromosome][1].resize( self.buffer_size, refcheck=False ) - self.__locations[chromosome][1].resize( 0, refcheck=False ) - self.__locations[chromosome] = [None, None] - self.__locations.pop(chromosome) - self.__destroyed = True - return - - cpdef void add_loc ( self, bytes chromosome, int32_t fiveendpos, int32_t strand ): - """Add a location to the list according to the sequence name. - - chromosome -- mostly the chromosome name - fiveendpos -- 5' end pos, left for plus strand, right for minus strand - strand -- 0: plus, 1: minus - """ - cdef: - int32_t i - int32_t b - np.ndarray arr - - if chromosome not in self.__locations: - self.__buf_size[chromosome] = [ self.buffer_size, self.buffer_size ] - self.__locations[chromosome] = [ np.zeros(self.buffer_size, dtype='int32'), np.zeros(self.buffer_size, dtype='int32') ] # [plus,minus strand] - self.__pointer[chromosome] = [ 0, 0 ] - self.__locations[chromosome][strand][0] = fiveendpos - self.__pointer[chromosome][strand] = 1 - else: - i = self.__pointer[chromosome][strand] - b = self.__buf_size[chromosome][strand] - arr = self.__locations[chromosome][strand] - if b == i: - b += self.buffer_size - arr.resize( b, refcheck = False ) - self.__buf_size[chromosome][strand] = b - arr[i]= fiveendpos - self.__pointer[chromosome][strand] += 1 - return - - cpdef void finalize ( self ): - """ Resize np arrays for 5' positions and sort them in place - - Note: If this function is called, it's impossible to append more files to this FWTrack object. So remember to call it after all the files are read! - """ - - cdef: - int32_t i - bytes c - set chrnames - - self.total = 0 - - chrnames = self.get_chr_names() - - for c in chrnames: - self.__locations[c][0].resize( self.__pointer[c][0], refcheck=False ) - self.__locations[c][0].sort() - self.__locations[c][1].resize( self.__pointer[c][1], refcheck=False ) - self.__locations[c][1].sort() - self.total += self.__locations[c][0].size + self.__locations[c][1].size - - self.__sorted = True - self.length = self.fw * self.total - return - - cpdef bint set_rlengths ( self, dict rlengths ): - """Set reference chromosome lengths dictionary. - - Only the chromosome existing in this fwtrack object will be updated. - - If chromosome in this fwtrack is not covered by given - rlengths, and it has no associated length, it will be set as - maximum integer. - - """ - cdef: - set valid_chroms, missed_chroms, extra_chroms - bytes chrom - - valid_chroms = set(self.__locations.keys()).intersection(rlengths.keys()) - for chrom in sorted(valid_chroms): - self.rlengths[chrom] = rlengths[chrom] - missed_chroms = set(self.__locations.keys()).difference(rlengths.keys()) - for chrom in sorted(missed_chroms): - self.rlengths[chrom] = INT_MAX - return True - - cpdef dict get_rlengths ( self ): - """Get reference chromosome lengths dictionary. - - If self.rlength is empty, create a new dict where the length of - chromosome will be set as the maximum integer. - """ - if not self.rlengths: - self.rlengths = dict([(k, INT_MAX) for k in self.__locations.keys()]) - return self.rlengths - - cpdef get_locations_by_chr ( self, bytes chromosome ): - """Return a tuple of two lists of locations for certain chromosome. - - """ - if chromosome in self.__locations: - return self.__locations[chromosome] - else: - raise Exception("No such chromosome name (%s) in TrackI object!\n" % (chromosome)) - - cpdef set get_chr_names ( self ): - """Return all the chromosome names stored in this track object. - """ - return set(sorted(self.__locations.keys())) - - cpdef void sort ( self ): - """Naive sorting for locations. - - """ - cdef: - int32_t i - bytes c - set chrnames - - chrnames = self.get_chr_names() - - for c in chrnames: - self.__locations[c][0].sort() - self.__locations[c][1].sort() - - self.__sorted = True - return - - @cython.boundscheck(False) # do not check that np indices are valid - cpdef uint64_t filter_dup ( self, int32_t maxnum = -1): - """Filter the duplicated reads. - - Run it right after you add all data into this object. - - Note, this function will *throw out* duplicates - permenantly. If you want to keep them, use separate_dups - instead. - """ - cdef: - int32_t p, m, n, current_loc - # index for old array, and index for new one - uint64_t i_old, i_new, size, new_size - bytes k - np.ndarray[int32_t, ndim=1] plus, new_plus, minus, new_minus - set chrnames - - if maxnum < 0: return self.total # do nothing - - if not self.__sorted: - self.sort() - - self.total = 0 - self.length = 0 - - chrnames = self.get_chr_names() - - for k in chrnames: - # for each chromosome. - # This loop body is too big, I may need to split code later... - - # + strand - i_new = 0 - plus = self.__locations[k][0] - size = plus.shape[0] - if len(plus) <= 1: - new_plus = plus # do nothing - else: - new_plus = np.zeros( self.__pointer[k][0] + 1,dtype='int32' ) - new_plus[ i_new ] = plus[ i_new ] # first item - i_new += 1 - n = 1 # the number of tags in the current location - current_loc = plus[0] - for i_old in range( 1, size ): - p = plus[ i_old ] - if p == current_loc: - n += 1 - else: - current_loc = p - n = 1 - if n <= maxnum: - new_plus[ i_new ] = p - i_new += 1 - new_plus.resize( i_new, refcheck=False ) - self.total += i_new - self.__pointer[k][0] = i_new - # free memory? - # I know I should shrink it to 0 size directly, - # however, on Mac OSX, it seems directly assigning 0 - # doesn't do a thing. - plus.resize( self.buffer_size, refcheck=False ) - plus.resize( 0, refcheck=False ) - # hope there would be no mem leak... - - # - strand - i_new = 0 - minus = self.__locations[k][1] - size = minus.shape[0] - if len(minus) <= 1: - new_minus = minus # do nothing - else: - new_minus = np.zeros( self.__pointer[k][1] + 1,dtype='int32' ) - new_minus[ i_new ] = minus[ i_new ] # first item - i_new += 1 - n = 1 # the number of tags in the current location - current_loc = minus[0] - for i_old in range( 1, size ): - p = minus[ i_old ] - if p == current_loc: - n += 1 - else: - current_loc = p - n = 1 - if n <= maxnum: - new_minus[ i_new ] = p - i_new += 1 - new_minus.resize( i_new, refcheck=False ) - self.total += i_new - self.__pointer[k][1] = i_new - # free memory ? - # I know I should shrink it to 0 size directly, - # however, on Mac OSX, it seems directly assigning 0 - # doesn't do a thing. - minus.resize( self.buffer_size, refcheck=False ) - minus.resize( 0, refcheck=False ) - # hope there would be no mem leak... - - self.__locations[k]=[new_plus,new_minus] - - self.length = self.fw * self.total - return self.total - - cpdef void sample_percent (self, float32_t percent, int32_t seed = -1 ): - """Sample the tags for a given percentage. - - Warning: the current object is changed! - """ - cdef: - int32_t num, i_chrom # num: number of reads allowed on a certain chromosome - bytes k - set chrnames - - self.total = 0 - self.length = 0 - - chrnames = self.get_chr_names() - - if seed >= 0: - np.random.seed(seed) - - for k in chrnames: - # for each chromosome. - # This loop body is too big, I may need to split code later... - - num = round(self.__locations[k][0].shape[0] * percent, 5 ) - np.random.shuffle( self.__locations[k][0] ) - self.__locations[k][0].resize( num, refcheck=False ) - self.__locations[k][0].sort() - self.__pointer[k][0] = self.__locations[k][0].shape[0] - - num = round(self.__locations[k][1].shape[0] * percent, 5 ) - np.random.shuffle( self.__locations[k][1] ) - self.__locations[k][1].resize( num, refcheck=False ) - self.__locations[k][1].sort() - self.__pointer[k][1] = self.__locations[k][1].shape[0] - - self.total += self.__pointer[k][0] + self.__pointer[k][1] - - self.length = self.fw * self.total - return - - cpdef void sample_num (self, uint64_t samplesize, int32_t seed = -1): - """Sample the tags for a given percentage. - - Warning: the current object is changed! - """ - cdef: - float32_t percent - - percent = (samplesize)/self.total - self.sample_percent ( percent, seed ) - return - - cpdef void print_to_bed (self, fhd=None): - """Output FWTrack to BED format files. If fhd is given, - write to a file, otherwise, output to standard output. - - """ - cdef: - int32_t i, i_chrom, p - bytes k - set chrnames - - if not fhd: - fhd = sys.stdout - assert isinstance(fhd,io.IOBase) - assert self.fw > 0, "FWTrack object .fw should be set larger than 0!" - - chrnames = self.get_chr_names() - - for k in chrnames: - # for each chromosome. - # This loop body is too big, I may need to split code later... - - plus = self.__locations[k][0] - - for i in range(plus.shape[0]): - p = plus[i] - fhd.write("%s\t%d\t%d\t.\t.\t%s\n" % (k.decode(),p,p+self.fw,"+") ) - - minus = self.__locations[k][1] - - for i in range(minus.shape[0]): - p = minus[i] - fhd.write("%s\t%d\t%d\t.\t.\t%s\n" % (k.decode(),p-self.fw,p,"-") ) - return - - cpdef tuple extract_region_tags ( self, bytes chromosome, int32_t startpos, int32_t endpos ): - cdef: - int32_t i, pos - np.ndarray[int32_t, ndim=1] rt_plus, rt_minus - list temp - set chrnames - - if not self.__sorted: self.sort() - - chrnames = self.get_chr_names() - assert chromosome in chrnames, "chromosome %s can't be found in the FWTrack object." % chromosome - - (plus, minus) = self.__locations[chromosome] - - temp = [] - for i in range(plus.shape[0]): - pos = plus[i] - if pos < startpos: - continue - elif pos > endpos: - break - else: - temp.append(pos) - rt_plus = np.array(temp) - - temp = [] - for i in range(minus.shape[0]): - pos = minus[i] - if pos < startpos: - continue - elif pos > endpos: - break - else: - temp.append(pos) - rt_minus = np.array(temp) - return (rt_plus, rt_minus) - - cpdef list compute_region_tags_from_peaks ( self, peaks, func, int32_t window_size = 100, float32_t cutoff = 5 ): - """Extract tags in peak, then apply func on extracted tags. - - peaks: redefined regions to extract raw tags in PeakIO type: check cPeakIO.pyx. - - func: a function to compute *something* from tags found in a predefined region - - window_size: this will be passed to func. - - cutoff: this will be passed to func. - - func needs the fixed number of parameters, so it's not flexible. Here is an example: - - wtd_find_summit(chrom, plus, minus, peak_start, peak_end, name , window_size, cutoff): - - """ - - cdef: - int32_t m, i, j, pre_i, pre_j, pos, startpos, endpos - np.ndarray[int32_t, ndim=1] plus, minus, rt_plus, rt_minus - bytes chrom, name - list temp, retval - set pchrnames, chrnames - - pchrnames = peaks.get_chr_names() - retval = [] - - # this object should be sorted - if not self.__sorted: self.sort() - # PeakIO object should be sorted - peaks.sort() - - chrnames = self.get_chr_names() - - for chrom in sorted(pchrnames): - assert chrom in chrnames, "chromosome %s can't be found in the FWTrack object." % chrom - (plus, minus) = self.__locations[chrom] - cpeaks = peaks.get_data_from_chrom(chrom) - prev_i = 0 - prev_j = 0 - for m in range(len(cpeaks)): - startpos = cpeaks[m]["start"] - window_size - endpos = cpeaks[m]["end"] + window_size - name = cpeaks[m]["name"] - - temp = [] - for i in range(prev_i,plus.shape[0]): - pos = plus[i] - if pos < startpos: - continue - elif pos > endpos: - prev_i = i - break - else: - temp.append(pos) - rt_plus = np.array(temp, dtype="int32") - - temp = [] - for j in range(prev_j,minus.shape[0]): - pos = minus[j] - if pos < startpos: - continue - elif pos > endpos: - prev_j = j - break - else: - temp.append(pos) - rt_minus = np.array(temp, dtype="int32") - - retval.append( func(chrom, rt_plus, rt_minus, startpos, endpos, name = name, window_size = window_size, cutoff = cutoff) ) - # rewind window_size - for i in range(prev_i, 0, -1): - if plus[prev_i] - plus[i] >= window_size: - break - prev_i = i - - for j in range(prev_j, 0, -1): - if minus[prev_j] - minus[j] >= window_size: - break - prev_j = j - # end of a loop - - return retval - - cpdef list pileup_a_chromosome ( self, bytes chrom, list ds, list scale_factor_s, float32_t baseline_value = 0.0, bint directional = True, int32_t end_shift = 0 ): - """pileup a certain chromosome, return [p,v] (end position and value) list. - - ds : tag will be extended to this value to 3' direction, - unless directional is False. Can contain multiple extension - values. Final pileup will the maximum. - scale_factor_s : linearly scale the pileup value applied to each d in ds. The list should have the same length as ds. - baseline_value : a value to be filled for missing values, and will be the minimum pileup. - directional : if False, the strand or direction of tag will be ignored, so that extension will be both sides with d/2. - end_shift : move cutting ends towards 5->3 direction if value is positive, or towards 3->5 direction if negative. Default is 0 -- no shift at all. - - - p and v are numpy.ndarray objects. - """ - cdef: - int64_t d - int64_t five_shift, three_shift # adjustment to 5' end and 3' end positions to make a fragment - dict chrlengths = self.get_rlengths () - int64_t rlength = chrlengths[chrom] - object ends - list five_shift_s = [] - list three_shift_s = [] - list tmp_pileup, prev_pileup - - assert len(ds) == len(scale_factor_s), "ds and scale_factor_s must have the same length!" - - # adjust extension length according to 'directional' and 'halfextension' setting. - for d in ds: - if directional: - # only extend to 3' side - five_shift_s.append( - end_shift ) - three_shift_s.append( end_shift + d) - else: - # both sides - five_shift_s.append( d//2 - end_shift ) - three_shift_s.append( end_shift + d - d//2) - - prev_pileup = None - - for i in range(len(ds)): - five_shift = five_shift_s[i] - three_shift = three_shift_s[i] - scale_factor = scale_factor_s[i] - tmp_pileup = se_all_in_one_pileup ( self.__locations[chrom][0], self.__locations[chrom][1], five_shift, three_shift, rlength, scale_factor, baseline_value ) - - if prev_pileup: - prev_pileup = over_two_pv_array ( prev_pileup, tmp_pileup, func="max" ) - else: - prev_pileup = tmp_pileup - - return prev_pileup - -cdef inline int32_t left_sum ( data, int32_t pos, int32_t width ): - """ - """ - return sum([data[x] for x in data if x <= pos and x >= pos - width]) - -cdef inline int32_t right_sum ( data, int32_t pos, int32_t width ): - """ - """ - return sum([data[x] for x in data if x >= pos and x <= pos + width]) - -cdef inline int32_t left_forward ( data, int32_t pos, int32_t window_size ): - return data.get(pos,0) - data.get(pos-window_size, 0) - -cdef inline int32_t right_forward ( data, int32_t pos, int32_t window_size ): - return data.get(pos + window_size, 0) - data.get(pos, 0) - diff --git a/MACS3/Signal/PairedEndTrack.py b/MACS3/Signal/PairedEndTrack.py index 848bc55b..ed056a9b 100644 --- a/MACS3/Signal/PairedEndTrack.py +++ b/MACS3/Signal/PairedEndTrack.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-11 11:21:30 Tao Liu> +# Time-stamp: <2024-10-14 13:15:32 Tao Liu> """Module for filter duplicate tags from paired-end data @@ -53,17 +53,18 @@ class PETrackI: Locations are stored and organized by sequence names (chr names) in a dict. They can be sorted by calling self.sort() function. """ - __locations = cython.declare(dict, visibility="public") - __size = cython.declare(dict, visibility="public") - __buf_size = cython.declare(dict, visibility="public") - __sorted = cython.declare(bool, visibility="public") + locations = cython.declare(dict, visibility="public") + size = cython.declare(dict, visibility="public") + buf_size = cython.declare(dict, visibility="public") + sorted = cython.declare(bool, visibility="public") total = cython.declare(cython.ulong, visibility="public") annotation = cython.declare(str, visibility="public") + # rlengths: reference chromosome lengths dictionary rlengths = cython.declare(dict, visibility="public") buffer_size = cython.declare(cython.long, visibility="public") length = cython.declare(cython.long, visibility="public") average_template_length = cython.declare(cython.float, visibility="public") - __destroyed: bool + destroyed: bool def __init__(self, anno: str = "", buffer_size: cython.long = 100000): """fw is the fixed-width for all locations. @@ -71,12 +72,13 @@ def __init__(self, anno: str = "", buffer_size: cython.long = 100000): """ # dictionary with chrname as key, nparray with # [('l','int32'),('r','int32')] as value - self.__locations = {} + self.locations = {} # dictionary with chrname as key, size of the above nparray as value - self.__size = {} + # size is to remember the size of the fragments added to this chromosome + self.size = {} # dictionary with chrname as key, size of the above nparray as value - self.__buf_size = {} - self.__sorted = False + self.buf_size = {} + self.sorted = False self.total = 0 # total fragments self.annotation = anno # need to be figured out self.rlengths = {} @@ -94,21 +96,21 @@ def add_loc(self, chromosome: bytes, """ i: cython.int - if chromosome not in self.__locations: - self.__buf_size[chromosome] = self.buffer_size + if chromosome not in self.locations: + self.buf_size[chromosome] = self.buffer_size # note: ['l'] is the leftmost end, ['r'] is the rightmost end of fragment. - self.__locations[chromosome] = np.zeros(shape=self.buffer_size, - dtype=[('l', 'i4'), ('r', 'i4')]) - self.__locations[chromosome][0] = (start, end) - self.__size[chromosome] = 1 + self.locations[chromosome] = np.zeros(shape=self.buffer_size, + dtype=[('l', 'i4'), ('r', 'i4')]) + self.locations[chromosome][0] = (start, end) + self.size[chromosome] = 1 else: - i = self.__size[chromosome] - if self.__buf_size[chromosome] == i: - self.__buf_size[chromosome] += self.buffer_size - self.__locations[chromosome].resize((self.__buf_size[chromosome]), - refcheck=False) - self.__locations[chromosome][i] = (start, end) - self.__size[chromosome] = i + 1 + i = self.size[chromosome] + if self.buf_size[chromosome] == i: + self.buf_size[chromosome] += self.buffer_size + self.locations[chromosome].resize((self.buf_size[chromosome]), + refcheck=False) + self.locations[chromosome][i] = (start, end) + self.size[chromosome] = i + 1 self.length += end - start return @@ -121,14 +123,14 @@ def destroy(self): chrs = self.get_chr_names() for chromosome in sorted(chrs): - if chromosome in self.__locations: - self.__locations[chromosome].resize(self.buffer_size, - refcheck=False) - self.__locations[chromosome].resize(0, - refcheck=False) - self.__locations[chromosome] = None - self.__locations.pop(chromosome) - self.__destroyed = True + if chromosome in self.locations: + self.locations[chromosome].resize(self.buffer_size, + refcheck=False) + self.locations[chromosome].resize(0, + refcheck=False) + self.locations[chromosome] = None + self.locations.pop(chromosome) + self.destroyed = True return @cython.ccall @@ -145,10 +147,10 @@ def set_rlengths(self, rlengths: dict) -> bool: missed_chroms: set chrom: bytes - valid_chroms = set(self.__locations.keys()).intersection(rlengths.keys()) + valid_chroms = set(self.locations.keys()).intersection(rlengths.keys()) for chrom in sorted(valid_chroms): self.rlengths[chrom] = rlengths[chrom] - missed_chroms = set(self.__locations.keys()).difference(rlengths.keys()) + missed_chroms = set(self.locations.keys()).difference(rlengths.keys()) for chrom in sorted(missed_chroms): self.rlengths[chrom] = INT_MAX return True @@ -161,14 +163,17 @@ def get_rlengths(self) -> dict: chromosome will be set as the maximum integer. """ if not self.rlengths: - self.rlengths = dict([(k, INT_MAX) for k in self.__locations.keys()]) + self.rlengths = dict([(k, INT_MAX) for k in self.locations.keys()]) return self.rlengths @cython.ccall def finalize(self): - """ Resize np arrays for 5' positions and sort them in place + """Resize np arrays for 5' positions and sort them in place + + Note: If this function is called, it's impossible to append + more files to this PETrackI object. So remember to call it + after all the files are read! - Note: If this function is called, it's impossible to append more files to this FWTrack object. So remember to call it after all the files are read! """ c: bytes chrnames: set @@ -178,11 +183,11 @@ def finalize(self): chrnames = self.get_chr_names() for c in chrnames: - self.__locations[c].resize((self.__size[c]), refcheck=False) - self.__locations[c].sort(order=['l', 'r']) - self.total += self.__size[c] + self.locations[c].resize((self.size[c]), refcheck=False) + self.locations[c].sort(order=['l', 'r']) + self.total += self.size[c] - self.__sorted = True + self.sorted = True self.average_template_length = cython.cast(cython.float, self.length) / self.total return @@ -191,8 +196,8 @@ def get_locations_by_chr(self, chromosome: bytes): """Return a tuple of two lists of locations for certain chromosome. """ - if chromosome in self.__locations: - return self.__locations[chromosome] + if chromosome in self.locations: + return self.locations[chromosome] else: raise Exception("No such chromosome name (%s) in TrackI object!\n" % (chromosome)) @@ -200,7 +205,7 @@ def get_locations_by_chr(self, chromosome: bytes): def get_chr_names(self) -> set: """Return all the chromosome names in this track object as a python set. """ - return set(self.__locations.keys()) + return set(self.locations.keys()) @cython.ccall def sort(self): @@ -213,8 +218,8 @@ def sort(self): chrnames = self.get_chr_names() for c in chrnames: - self.__locations[c].sort(order=['l', 'r']) # sort by the leftmost location - self.__sorted = True + self.locations[c].sort(order=['l', 'r']) # sort by the leftmost location + self.sorted = True return @cython.ccall @@ -234,7 +239,7 @@ def count_fraglengths(self) -> dict: counter = Counter() chrnames = list(self.get_chr_names()) for i in range(len(chrnames)): - locs = self.__locations[chrnames[i]] + locs = self.locations[chrnames[i]] sizes = locs['r'] - locs['l'] for s in sizes: counter[s] += 1 @@ -252,10 +257,10 @@ def fraglengths(self) -> cnp.ndarray: i: cython.int chrnames = list(self.get_chr_names()) - locs = self.__locations[chrnames[0]] + locs = self.locations[chrnames[0]] sizes = locs['r'] - locs['l'] for i in range(1, len(chrnames)): - locs = self.__locations[chrnames[i]] + locs = self.locations[chrnames[i]] sizes = np.concatenate((sizes, locs['r'] - locs['l'])) return sizes @@ -281,7 +286,7 @@ def filter_dup(self, maxnum: cython.int = -1): if maxnum < 0: return # condition to return if not filtering - if not self.__sorted: + if not self.sorted: self.sort() self.total = 0 @@ -291,12 +296,12 @@ def filter_dup(self, maxnum: cython.int = -1): chrnames = self.get_chr_names() for k in chrnames: # for each chromosome - locs = self.__locations[k] + locs = self.locations[k] locs_size = locs.shape[0] if locs_size == 1: # do nothing and continue continue - # discard duplicate reads and make a new __locations[k] + # discard duplicate reads and make a new locations[k] # initialize boolean array as all TRUE, or all being kept selected_idx = np.ones(locs_size, dtype=bool) # get the first loc @@ -319,9 +324,9 @@ def filter_dup(self, maxnum: cython.int = -1): selected_idx[i] = False # subtract current_loc_l from self.length self.length -= current_loc_end - current_loc_start - self.__locations[k] = locs[selected_idx] - self.__size[k] = self.__locations[k].shape[0] - self.total += self.__size[k] + self.locations[k] = locs[selected_idx] + self.size[k] = self.locations[k].shape[0] + self.total += self.size[k] # free memory? # I know I should shrink it to 0 size directly, # however, on Mac OSX, it seems directly assigning 0 @@ -362,13 +367,13 @@ def sample_percent(self, percent: cython.float, seed: cython.int = -1): # This loop body is too big, I may need to split code later... num = cython.cast(cython.uint, - round(self.__locations[k].shape[0] * percent, 5)) - rs_shuffle(self.__locations[k]) - self.__locations[k].resize(num, refcheck=False) - self.__locations[k].sort(order=['l', 'r']) # sort by leftmost positions - self.__size[k] = self.__locations[k].shape[0] - self.length += (self.__locations[k]['r'] - self.__locations[k]['l']).sum() - self.total += self.__size[k] + round(self.locations[k].shape[0] * percent, 5)) + rs_shuffle(self.locations[k]) + self.locations[k].resize(num, refcheck=False) + self.locations[k].sort(order=['l', 'r']) # sort by leftmost positions + self.size[k] = self.locations[k].shape[0] + self.length += (self.locations[k]['r'] - self.locations[k]['l']).sum() + self.total += self.size[k] self.average_template_length = cython.cast(cython.float, self.length)/self.total return @@ -399,15 +404,15 @@ def sample_percent_copy(self, percent: cython.float, seed: cython.int = -1): for k in sorted(chrnames): # for each chromosome. # This loop body is too big, I may need to split code later... - loc = np.copy(self.__locations[k]) + loc = np.copy(self.locations[k]) num = cython.cast(cython.uint, round(loc.shape[0] * percent, 5)) rs_shuffle(loc) loc.resize(num, refcheck=False) loc.sort(order=['l', 'r']) # sort by leftmost positions - ret_petrackI.__locations[k] = loc - ret_petrackI.__size[k] = loc.shape[0] + ret_petrackI.locations[k] = loc + ret_petrackI.size[k] = loc.shape[0] ret_petrackI.length += (loc['r'] - loc['l']).sum() - ret_petrackI.total += ret_petrackI.__size[k] + ret_petrackI.total += ret_petrackI.size[k] ret_petrackI.average_template_length = cython.cast(cython.float, ret_petrackI.length)/ret_petrackI.total ret_petrackI.set_rlengths(self.get_rlengths()) return ret_petrackI @@ -457,7 +462,7 @@ def print_to_bed(self, fhd=None): # for each chromosome. # This loop body is too big, I may need to split code later... - locs = self.__locations[k] + locs = self.locations[k] for i in range(locs.shape[0]): s, e = locs[i] @@ -490,8 +495,8 @@ def pileup_a_chromosome(self, scale_factor = scale_factor_s[i] # Can't directly pass partial nparray there since that will mess up with pointer calculation. - tmp_pileup = quick_pileup(np.sort(self.__locations[chrom]['l']), - np.sort(self.__locations[chrom]['r']), + tmp_pileup = quick_pileup(np.sort(self.locations[chrom]['l']), + np.sort(self.locations[chrom]['r']), scale_factor, baseline_value) if prev_pileup: @@ -534,7 +539,7 @@ def pileup_a_chromosome_c(self, three_shift: cython.long rlength: cython.long = self.get_rlengths()[chrom] - if not self.__sorted: + if not self.sorted: self.sort() assert len(ds) == len(scale_factor_s), "ds and scale_factor_s must have the same length!" @@ -547,8 +552,8 @@ def pileup_a_chromosome_c(self, five_shift = d//2 three_shift = d//2 - tmp_pileup = se_all_in_one_pileup(self.__locations[chrom]['l'], - self.__locations[chrom]['r'], + tmp_pileup = se_all_in_one_pileup(self.locations[chrom]['l'], + self.locations[chrom]['r'], five_shift, three_shift, rlength, @@ -593,8 +598,8 @@ def pileup_bdg(self, # Can't directly pass partial nparray there since that # will mess up with pointer calculation. - tmp_pileup = quick_pileup(np.sort(self.__locations[chrom]['l']), - np.sort(self.__locations[chrom]['r']), + tmp_pileup = quick_pileup(np.sort(self.locations[chrom]['l']), + np.sort(self.locations[chrom]['r']), scale_factor, baseline_value) @@ -640,25 +645,37 @@ def pileup_bdg_hmmr(self, chroms = self.get_chr_names() for i in range(len(mapping)): for chrom in sorted(chroms): - ret_pileup[i][chrom] = pileup_from_LR_hmmratac(self.__locations[chrom], mapping[i]) + ret_pileup[i][chrom] = pileup_from_LR_hmmratac(self.locations[chrom], mapping[i]) return ret_pileup @cython.cclass -class PEtrackII(PETrackI): +class PETrackII(PETrackI): """Documentation for PEtrac """ - # add another dict for storing barcode for each fragment - __barcode = cython.declare(dict, visibility="public") - __barcode_dict = cython.declare(dict, visibility="public") - # add another dict for storing counts for each fragment - __counts = cython.declare(dict, visibility="public") + # add another dict for storing barcode for each fragment we will + # first convert barcode into integer and remember them in the + # barcode_dict, which will map key:barcode -> value:integer + barcodes = cython.declare(dict, visibility="public") + barcode_dict = cython.declare(dict, visibility="public") + # the last number for barcodes, used to map barcode into integer + barcode_last_n: cython.int + + def __init__(self): + super().__init__() + self.barcodes = {} + self.barcode_dict = {} + self.barcode_last_n = 0 - def __init__(self, args): - super(PETrackI, self).__init__() - self.__barcodes = {} - self.__barcode_dict = {} + @cython.ccall + def add_loc(self, chromosome: bytes, + start: cython.int, end: cython.int): + raise NotImplementedError("This function is disabled PETrackII") + + @cython.ccall + def filter_dup(self, maxnum: cython.int = -1): + raise NotImplementedError("This function is disabled PETrackII") @cython.ccall def add_frag(self, @@ -676,32 +693,35 @@ def add_frag(self, count: the count of the fragment """ i: cython.int - h: cython.long + # bn: the integer in barcode_dict for this barcode + bn: cython.int - h = hash(barcode) - self.__barcode_dict[h] = barcode + if barcode not in self.barcode_dict: + self.barcode_dict[barcode] = self.barcode_last_n + self.barcode_last_n += 1 + bn = self.barcode_dict[barcode] - if chromosome not in self.__locations: - self.__buf_size[chromosome] = self.buffer_size + if chromosome not in self.locations: + self.buf_size[chromosome] = self.buffer_size # note: ['l'] is the leftmost end, ['r'] is the rightmost end of fragment. # ['c'] is the count number of this fragment - self.__locations[chromosome] = np.zeros(shape=self.buffer_size, - dtype=[('l', 'i4'), ('r', 'i4'), ('c', 'u1')]) - self.__barcodes[chromosome] = np.zeros(shape=self.buffer_size, - dtype='i4') - self.__locations[chromosome][0] = (start, end, count) - self.__barcodes[chromosome][0] = h - self.__size[chromosome] = 1 + self.locations[chromosome] = np.zeros(shape=self.buffer_size, + dtype=[('l', 'i4'), ('r', 'i4'), ('c', 'u1')]) + self.barcodes[chromosome] = np.zeros(shape=self.buffer_size, + dtype='i4') + self.locations[chromosome][0] = (start, end, count) + self.barcodes[chromosome][0] = bn + self.size[chromosome] = 1 else: - i = self.__size[chromosome] - if self.__buf_size[chromosome] == i: - self.__buf_size[chromosome] += self.buffer_size - self.__locations[chromosome].resize((self.__buf_size[chromosome]), - refcheck=False) - self.__locations[chromosome][i] = (start, end, count) - self.__barcodes[chromosome][i] = h - self.__size[chromosome] = i + 1 - self.length += end - start + i = self.size[chromosome] + if self.buf_size[chromosome] == i: + self.buf_size[chromosome] += self.buffer_size + self.locations[chromosome].resize((self.buf_size[chromosome]), + refcheck=False) + self.locations[chromosome][i] = (start, end, count) + self.barcodes[chromosome][i] = bn + self.size[chromosome] = i + 1 + self.length += (end - start) * count return @cython.ccall @@ -713,19 +733,225 @@ def destroy(self): chrs = self.get_chr_names() for chromosome in sorted(chrs): - if chromosome in self.__locations: - self.__locations[chromosome].resize(self.buffer_size, - refcheck=False) - self.__locations[chromosome].resize(0, - refcheck=False) - self.__locations[chromosome] = None - self.__locations.pop(chromosome) - self.__barcodes.resize(self.buffer_size, - refcheck=False) - self.__barcodes.resize(0, - refcheck=False) - self.__barcodes[chromosome] = None - self.__barcodes.pop(chromosome) - self.__barcode_dict = {} - self.__destroyed = True + if chromosome in self.locations: + self.locations[chromosome].resize(self.buffer_size, + refcheck=False) + self.locations[chromosome].resize(0, + refcheck=False) + self.locations[chromosome] = None + self.locations.pop(chromosome) + self.barcodes.resize(self.buffer_size, + refcheck=False) + self.barcodes.resize(0, + refcheck=False) + self.barcodes[chromosome] = None + self.barcodes.pop(chromosome) + self.barcode_dict = {} + self.destroyed = True + return + + @cython.ccall + def finalize(self): + """Resize np arrays for 5' positions and sort them in place + + Note: If this function is called, it's impossible to append + more files to this PETrackII object. So remember to call it + after all the files are read! + + """ + c: bytes + chrnames: set + indices: cnp.ndarray + + self.total = 0 + + chrnames = self.get_chr_names() + + for c in chrnames: + self.locations[c].resize((self.size[c]), refcheck=False) + indices = np.argsort(self.locations[c], order=['l', 'r']) + self.locations[c] = self.locations[c][indices] + self.barcodes[c] = self.barcodes[c][indices] + self.total += np.sum(self.locations[c]['c']) # self.size[c] + + self.sorted = True + self.average_template_length = cython.cast(cython.float, + self.length) / self.total + return + + @cython.ccall + def sort(self): + """Naive sorting for locations. + + """ + c: bytes + chrnames: set + indices: cnp.ndarray + + chrnames = self.get_chr_names() + + for c in chrnames: + indices = np.argsort(self.locations[c], order=['l', 'r']) + self.locations[c] = self.locations[c][indices] + self.barcodes[c] = self.barcodes[c][indices] + self.sorted = True return + + @cython.ccall + def count_fraglengths(self) -> dict: + """Return a dictionary of the counts for sizes/fragment + lengths of each pair. + + This function is for HMMRATAC. + + """ + sizes: cnp.ndarray(cnp.int32_t, ndim=1) + s: cython.int + locs: cnp.ndarray + chrnames: list + i: cython.int + + counter = Counter() + chrnames = list(self.get_chr_names()) + for i in range(len(chrnames)): + locs = self.locations[chrnames[i]] + sizes = locs['r'] - locs['l'] + for s in sizes: + counter[s] += locs['c'] + return dict(counter) + + @cython.ccall + def fraglengths(self) -> cnp.ndarray: + """Return the sizes/fragment lengths of each pair. + + This function is for HMMRATAC EM training. + """ + sizes: cnp.ndarray(np.int32_t, ndim=1) + t_sizes: cnp.ndarray(np.int32_t, ndim=1) + locs: cnp.ndarray + chrnames: list + i: cython.int + + chrnames = list(self.get_chr_names()) + locs = self.locations[chrnames[0]] + sizes = locs['r'] - locs['l'] + sizes = [x for x, count in zip(sizes, locs['c']) for _ in range(count)] + + for i in range(1, len(chrnames)): + locs = self.locations[chrnames[i]] + t_sizes = locs['r'] - locs['l'] + t_sizes = [x for x, count in zip(t_sizes, locs['c']) for _ in range(count)] + sizes = np.concatenate((sizes, t_sizes)) + return sizes + + @cython.ccall + def subset(self, selected_barcodes: set): + """Make a subset of PETrackII with only the given barcodes. + + Note: the selected_barcodes is a set of barcodes in python + bytes. For example, b"ATCTGCTAGTCTACAT" + + """ + indices: cnp.ndarray + chrs: set + selected_barcodes_filtered: list + selected_barcodes_n: list + chromosome: bytes + ret: PETrackII + + ret = PETrackII() + chrs = self.get_chr_names() + + # first we need to convert barcodes into integers in our + # barcode_dict + selected_barcodes_filtered = [b + for b in selected_barcodes + if b in self.barcode_dict] + ret.barcode_dict = {b: self.barcode_dict[b] + for b in selected_barcodes_filtered} + selected_barcodes_n = [self.barcode_dict[b] + for b in selected_barcodes_filtered] + ret.barcode_last_n = self.barcode_last_n + + # pass some values from self to ret + ret.annotation = self.annotation + ret.sorted = self.sorted + ret.rlengths = self.rlengths + ret.buffer_size = self.buffer_size + ret.total = 0 + ret.length = 0 + ret.average_template_length = 0 + ret.destroyed = True + + for chromosome in sorted(chrs): + indices = np.where(np.isin(self.barcodes[chromosome], list(selected_barcodes_n)))[0] + print(chrs, indices) + ret.barcodes[chromosome] = self.barcodes[chromosome][indices] + print(ret.barcodes) + ret.locations[chromosome] = self.locations[chromosome][indices] + print(ret.locations) + ret.size[chromosome] = len(ret.locations[chromosome]) + print(ret.size) + ret.buf_size[chromosome] = ret.size[chromosome] + ret.total += np.sum(ret.locations[chromosome]['c']) + ret.length += np.sum((ret.locations[chromosome]['r'] - + ret.locations[chromosome]['l']) * + ret.locations[chromosome]['c']) + ret.average_template_length = ret.length / ret.total + return ret + + @cython.ccall + def sample_percent(self, percent: cython.float, seed: cython.int = -1): + raise NotImplementedError("This function is disabled PETrackII") + + @cython.ccall + def sample_percent_copy(self, percent: cython.float, seed: cython.int = -1): + raise NotImplementedError("This function is disabled PETrackII") + + @cython.ccall + def sample_num(self, samplesize: cython.ulong, seed: cython.int = -1): + raise NotImplementedError("This function is disabled PETrackII") + + @cython.ccall + def sample_num_copy(self, samplesize: cython.ulong, seed: cython.int = -1): + raise NotImplementedError("This function is disabled PETrackII") + + @cython.ccall + def pileup_a_chromosome(self, + chrom: bytes, + scale_factor_s: list, + baseline_value: cython.float = 0.0) -> list: + """pileup a certain chromosome, return [p,v] (end position and + pileup value) list. + + scale_factor_s : linearly scale the pileup value applied to + each d in ds. The list should have the same + length as ds. + + baseline_value : a value to be filled for missing values, and + will be the minimum pileup. + + """ + tmp_pileup: list + prev_pileup: list + scale_factor: cython.float + + prev_pileup = None + + for i in range(len(scale_factor_s)): + scale_factor = scale_factor_s[i] + + # Can't directly pass partial nparray there since that will mess up with pointer calculation. + tmp_pileup = quick_pileup(np.sort(self.locations[chrom]['l']), + np.sort(self.locations[chrom]['r']), + scale_factor, baseline_value) + + if prev_pileup: + prev_pileup = over_two_pv_array(prev_pileup, + tmp_pileup, + func="max") + else: + prev_pileup = tmp_pileup + + return prev_pileup + diff --git a/MACS3/Signal/Pileup.py b/MACS3/Signal/Pileup.py index e5068c65..fcff7ce7 100644 --- a/MACS3/Signal/Pileup.py +++ b/MACS3/Signal/Pileup.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-06 20:51:44 Tao Liu> +# Time-stamp: <2024-10-14 13:42:18 Tao Liu> """Module Description: For pileup functions. diff --git a/setup.py b/setup.py index 65d78062..f0dc85b6 100644 --- a/setup.py +++ b/setup.py @@ -116,7 +116,7 @@ def main(): include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.FixWidthTrack", - ["MACS3/Signal/FixWidthTrack.pyx"], + ["MACS3/Signal/FixWidthTrack.py"], include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.PairedEndTrack", diff --git a/test/test_PairedEndTrack.py b/test/test_PairedEndTrack.py index e86924ec..c120f7c9 100644 --- a/test/test_PairedEndTrack.py +++ b/test/test_PairedEndTrack.py @@ -1,78 +1,114 @@ #!/usr/bin/env python -# Time-stamp: <2020-11-24 17:51:32 Tao Liu> +# Time-stamp: <2024-10-11 16:20:17 Tao Liu> import unittest -from MACS3.Signal.PairedEndTrack import * +from MACS3.Signal.PairedEndTrack import PETrackI, PETrackII -class Test_PETrackI(unittest.TestCase): +class Test_PETrackI(unittest.TestCase): def setUp(self): - self.input_regions = [(b"chrY",0,100 ), - (b"chrY",70,270 ), - (b"chrY",70,100 ), - (b"chrY",80,160 ), - (b"chrY",80,160 ), - (b"chrY",80,180 ), - (b"chrY",80,180 ), - (b"chrY",85,185 ), - (b"chrY",85,285 ), - (b"chrY",85,285 ), - (b"chrY",85,285 ), - (b"chrY",85,385 ), - (b"chrY",90,190 ), - (b"chrY",90,190 ), - (b"chrY",90,191 ), - (b"chrY",150,190 ), - (b"chrY",150,250 ), + self.input_regions = [(b"chrY", 0, 100), + (b"chrY", 70, 270), + (b"chrY", 70, 100), + (b"chrY", 80, 160), + (b"chrY", 80, 160), + (b"chrY", 80, 180), + (b"chrY", 80, 180), + (b"chrY", 85, 185), + (b"chrY", 85, 285), + (b"chrY", 85, 285), + (b"chrY", 85, 285), + (b"chrY", 85, 385), + (b"chrY", 90, 190), + (b"chrY", 90, 190), + (b"chrY", 90, 191), + (b"chrY", 150, 190), + (b"chrY", 150, 250), ] - self.t = sum([ x[2]-x[1] for x in self.input_regions ]) + self.t = sum([x[2]-x[1] for x in self.input_regions]) def test_add_loc(self): pe = PETrackI() - for ( c, l, r ) in self.input_regions: + for (c, l, r) in self.input_regions: pe.add_loc(c, l, r) pe.finalize() # roughly check the numbers... - self.assertEqual( pe.total, 17 ) - self.assertEqual( pe.length, self.t ) + self.assertEqual(pe.total, 17) + self.assertEqual(pe.length, self.t) def test_filter_dup(self): pe = PETrackI() - for ( c, l, r ) in self.input_regions: + for (c, l, r) in self.input_regions: pe.add_loc(c, l, r) pe.finalize() # roughly check the numbers... - self.assertEqual( pe.total, 17 ) - self.assertEqual( pe.length, self.t ) + self.assertEqual(pe.total, 17) + self.assertEqual(pe.length, self.t) # filter out more than 3 tags - pe.filter_dup( 3 ) - self.assertEqual( pe.total, 17 ) + pe.filter_dup(3) + self.assertEqual(pe.total, 17) # filter out more than 2 tags - pe.filter_dup( 2 ) - self.assertEqual( pe.total, 16 ) + pe.filter_dup(2) + self.assertEqual(pe.total, 16) # filter out more than 1 tag - pe.filter_dup( 1 ) - self.assertEqual( pe.total, 12 ) - + pe.filter_dup(1) + self.assertEqual(pe.total, 12) def test_sample_num(self): pe = PETrackI() - for ( c, l, r ) in self.input_regions: + for (c, l, r) in self.input_regions: pe.add_loc(c, l, r) pe.finalize() - pe.sample_num( 10 ) - self.assertEqual( pe.total, 10 ) + pe.sample_num(10) + self.assertEqual(pe.total, 10) def test_sample_percent(self): pe = PETrackI() - for ( c, l, r ) in self.input_regions: + for (c, l, r) in self.input_regions: pe.add_loc(c, l, r) pe.finalize() - pe.sample_percent( 0.5 ) - self.assertEqual( pe.total, 8 ) + pe.sample_percent(0.5) + self.assertEqual(pe.total, 8) + + +class Test_PETrackII(unittest.TestCase): + def setUp(self): + self.input_regions = [(b"chrY", 0, 100, b"0w#AAACGAAAGACTCGGA", 2), + (b"chrY", 70, 170, b"0w#AAACGAAAGACTCGGA", 1), + (b"chrY", 80, 190, b"0w#AAACGAAAGACTCGGA", 1), + (b"chrY", 85, 180, b"0w#AAACGAAAGACTCGGA", 3), + (b"chrY", 100, 190, b"0w#AAACGAAAGACTCGGA", 1), + (b"chrY", 0, 100, b"0w#AAACGAACAAGTAACA", 1), + (b"chrY", 70, 170, b"0w#AAACGAACAAGTAACA", 2), + (b"chrY", 80, 190, b"0w#AAACGAACAAGTAACA", 1), + (b"chrY", 85, 180, b"0w#AAACGAACAAGTAACA", 1), + (b"chrY", 100, 190, b"0w#AAACGAACAAGTAACA", 3), + (b"chrY", 10, 110, b"0w#AAACGAACAAGTAAGA", 1), + (b"chrY", 50, 160, b"0w#AAACGAACAAGTAAGA", 2), + (b"chrY", 100, 170, b"0w#AAACGAACAAGTAAGA", 3) + ] + self.t = sum([(x[2]-x[1]) * x[4] for x in self.input_regions]) + + def test_add_frag(self): + pe = PETrackII() + for (c, l, r, b, C) in self.input_regions: + pe.add_frag(c, l, r, b, C) + pe.finalize() + # roughly check the numbers... + self.assertEqual(pe.total, 22) + self.assertEqual(pe.length, self.t) + def test_subset(self): + pe = PETrackII() + for (c, l, r, b, C) in self.input_regions: + pe.add_frag(c, l, r, b, C) + pe.finalize() + pe_subset = pe.subset(set([b'0w#AAACGAACAAGTAACA', b"0w#AAACGAACAAGTAAGA"])) + # roughly check the numbers... + self.assertEqual(pe_subset.total, 14) + self.assertEqual(pe_subset.length, 1305) From 4267d7428c0fbfc62281243ab4d0cc94c29f9f46 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Mon, 14 Oct 2024 23:33:32 -0400 Subject: [PATCH 04/13] implement PETrackII for fragment files from scATAC experiments --- MACS3/Signal/{BedGraph.pyx => BedGraph.py} | 919 ++++++++++++--------- MACS3/Signal/HMMR_Signal_Processing.py | 4 +- MACS3/Signal/PairedEndTrack.py | 234 +++--- MACS3/Signal/Pileup.py | 85 +- MACS3/Signal/PileupV2.py | 285 ++++--- setup.py | 2 +- test/test_HMMR_poisson.py | 84 +- test/test_PairedEndTrack.py | 35 +- test/test_PeakIO.py | 6 +- test/test_Pileup.py | 8 +- 10 files changed, 976 insertions(+), 686 deletions(-) rename MACS3/Signal/{BedGraph.pyx => BedGraph.py} (62%) diff --git a/MACS3/Signal/BedGraph.pyx b/MACS3/Signal/BedGraph.py similarity index 62% rename from MACS3/Signal/BedGraph.pyx rename to MACS3/Signal/BedGraph.py index df57c4f7..2abc6175 100644 --- a/MACS3/Signal/BedGraph.pyx +++ b/MACS3/Signal/BedGraph.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-05-15 19:27:06 Tao Liu> +# Time-stamp: <2024-10-14 19:32:34 Tao Liu> """Module for BedGraph data class. @@ -12,68 +12,81 @@ # ------------------------------------ # python modules # ------------------------------------ -#from array import array -from cpython cimport array +import cython from array import array as pyarray from math import prod # ------------------------------------ # MACS3 modules # ------------------------------------ from MACS3.Signal.ScoreTrack import ScoreTrackII -from MACS3.IO.PeakIO import PeakIO, BroadPeakIO +from MACS3.IO.PeakIO import PeakIO, BroadPeakIO, PeakContent from MACS3.Signal.Prob import chisq_logp_e # ------------------------------------ # Other modules # ------------------------------------ -from cpython cimport bool +from cython.cimports.cpython import bool import numpy as np -cimport numpy as np -from numpy cimport uint8_t, uint16_t, uint32_t, uint64_t, int8_t, int16_t, int32_t, int64_t, float32_t, float64_t +import cython.cimports.numpy as cnp # ------------------------------------ # C lib # ------------------------------------ -from libc.math cimport sqrt, log, log1p, exp, log10 +from cython.cimports.libc.math import sqrt, log10 # ------------------------------------ # constants # ------------------------------------ -__version__ = "BedGraph $Revision$" -__author__ = "Tao Liu " -__doc__ = "bedGraphTrackI class" +LOG10_E = 0.43429448190325176 # ------------------------------------ # Misc functions # ------------------------------------ -LOG10_E = 0.43429448190325176 -cdef inline mean_func( x ): - return sum( x )/len( x ) -cdef inline fisher_func( x ): +@cython.inline +@cython.cfunc +def mean_func(x): + return sum(x)/len(x) + + +@cython.inline +@cython.cfunc +def fisher_func(x): # combine -log10pvalues - return chisq_logp_e( 2*sum (x )/LOG10_E, 2*len( x ), log10=True ) + return chisq_logp_e(2*sum(x)/LOG10_E, 2*len(x), log10=True) + -cdef inline subtract_func( x ): +@cython.inline +@cython.cfunc +def subtract_func(x): # subtraction of two items list return x[1] - x[0] -cdef inline divide_func( x ): + +@cython.inline +@cython.cfunc +def divide_func(x): # division of two items list return x[1] / x[2] -cdef inline product_func( x ): + +@cython.inline +@cython.cfunc +def product_func(x): # production of a list of values # only python 3.8 or above - return prod( x ) - + return prod(x) + # ------------------------------------ # Classes # ------------------------------------ -cdef class bedGraphTrackI: + + +@cython.cclass +class bedGraphTrackI: """Class for bedGraph type data. In bedGraph, data are represented as continuous non-overlapping @@ -94,13 +107,12 @@ this class is 0-indexed and right-open. """ - cdef: - dict __data - public float32_t maxvalue - public float32_t minvalue - public float32_t baseline_value + __data: dict + maxvalue = cython.declare(cython.float, visibility="public") + minvalue = cython.declare(cython.float, visibility="public") + baseline_value = cython.declare(cython.float, visibility="public") - def __init__ (self, float32_t baseline_value=0 ): + def __init__(self, baseline_value: cython.float = 0): """ baseline_value is the value to fill in the regions not defined in bedGraph. For example, if the bedGraph is like: @@ -112,11 +124,15 @@ def __init__ (self, float32_t baseline_value=0 ): """ self.__data = {} - self.maxvalue = -10000000 # initial maximum value is tiny since I want safe_add_loc to update it + self.maxvalue = -10000000 # initial maximum value is tiny since I want safe_add_loc to update it self.minvalue = 10000000 # initial minimum value is large since I want safe_add_loc to update it self.baseline_value = baseline_value - cpdef add_loc ( self, bytes chromosome, int32_t startpos, int32_t endpos, float32_t value): + @cython.ccall + def add_loc(self, chromosome: bytes, + startpos: cython.int, + endpos: cython.int, + value: cython.float): """Add a chr-start-end-value block into __data dictionary. Note, we don't check if the add_loc is called continuously on @@ -124,7 +140,8 @@ def __init__ (self, float32_t baseline_value=0 ): this function within MACS. """ - cdef float32_t pre_v + pre_v: cython.float + # basic assumption, end pos should > start pos if endpos <= 0: @@ -133,7 +150,8 @@ def __init__ (self, float32_t baseline_value=0 ): startpos = 0 if chromosome not in self.__data: - self.__data[chromosome] = [ pyarray('i',[]), pyarray('f',[]) ] + self.__data[chromosome] = [pyarray('i', []), + pyarray('f', [])] c = self.__data[chromosome] if startpos: # start pos is not 0, then add two blocks, the first @@ -145,7 +163,7 @@ def __init__ (self, float32_t baseline_value=0 ): else: c = self.__data[chromosome] # get the preceding region - pre_v = c[1][-1] + pre_v = c[1][-1] # if this region is next to the previous one. if pre_v == value: @@ -161,7 +179,11 @@ def __init__ (self, float32_t baseline_value=0 ): if value < self.minvalue: self.minvalue = value - cpdef add_loc_wo_merge ( self, bytes chromosome, int32_t startpos, int32_t endpos, float32_t value): + @cython.ccall + def add_loc_wo_merge(self, chromosome: bytes, + startpos: cython.int, + endpos: cython.int, + value: cython.float): """Add a chr-start-end-value block into __data dictionary. Note, we don't check if the add_loc is called continuously on @@ -177,9 +199,10 @@ def __init__ (self, float32_t baseline_value=0 ): if value < self.baseline_value: value = self.baseline_value - + if chromosome not in self.__data: - self.__data[chromosome] = [ pyarray('i',[]), pyarray('f',[]) ] + self.__data[chromosome] = [pyarray('i', []), + pyarray('f', [])] c = self.__data[chromosome] if startpos: # start pos is not 0, then add two blocks, the first @@ -194,7 +217,11 @@ def __init__ (self, float32_t baseline_value=0 ): if value < self.minvalue: self.minvalue = value - cpdef add_chrom_data( self, bytes chromosome, object p, object v ): + @cython.ccall + def add_chrom_data(self, + chromosome: bytes, + p: pyarray, + v: pyarray): """Add a pv data to a chromosome. Replace the previous data. p: a pyarray object 'i' for positions @@ -202,19 +229,22 @@ def __init__ (self, float32_t baseline_value=0 ): Note: no checks for error, use with caution """ - cdef: - float32_t maxv, minv + maxv: cython.float + minv: cython.float - self.__data[ chromosome ] = [ p, v ] - maxv = max( v ) - minv = min( v ) + self.__data[chromosome] = [p, v] + maxv = max(v) + minv = min(v) if maxv > self.maxvalue: self.maxvalue = maxv if minv < self.minvalue: self.minvalue = minv return - cpdef add_chrom_data_hmmr_PV( self, bytes chromosome, object pv ): + @cython.ccall + def add_chrom_data_PV(self, + chromosome: bytes, + pv: cnp.ndarray): """Add a pv data to a chromosome. Replace the previous data. This is a kinda silly function to waste time and convert a PV @@ -223,11 +253,11 @@ def __init__ (self, float32_t baseline_value=0 ): Note: no checks for error, use with caution """ - cdef: - float32_t maxv, minv - int32_t i + maxv: cython.float + minv: cython.float - self.__data[ chromosome ] = [ pyarray('i', pv['p']), pyarray('f',pv['v']) ] + self.__data[chromosome] = [pyarray('i', pv['p']), + pyarray('f', pv['v'])] minv = pv['v'].min() maxv = pv['p'].max() if maxv > self.maxvalue: @@ -235,13 +265,13 @@ def __init__ (self, float32_t baseline_value=0 ): if minv < self.minvalue: self.minvalue = minv return - - cpdef bool destroy ( self ): + + @cython.ccall + def destroy(self) -> bool: """ destroy content, free memory. """ - cdef: - set chrs - bytes chrom + chrs: set + chrom: bytes chrs = self.get_chr_names() for chrom in sorted(chrs): @@ -250,7 +280,8 @@ def __init__ (self, float32_t baseline_value=0 ): self.__data.pop(chrom) return True - cpdef list get_data_by_chr (self, bytes chromosome): + @cython.ccall + def get_data_by_chr(self, chromosome: bytes) -> list: """Return array of counts by chromosome. The return value is a tuple: @@ -261,13 +292,15 @@ def __init__ (self, float32_t baseline_value=0 ): else: return [] - cpdef set get_chr_names (self): + @cython.ccall + def get_chr_names(self) -> set: """Return all the chromosome names stored. """ return set(sorted(self.__data.keys())) - cpdef void reset_baseline (self, float32_t baseline_value): + @cython.ccall + def reset_baseline(self, baseline_value: cython.float): """Reset baseline value to baseline_value. So any region between self.baseline_value and baseline_value @@ -279,33 +312,36 @@ def __init__ (self, float32_t baseline_value=0 ): self.merge_regions() return - cdef merge_regions (self): + @cython.cfunc + def merge_regions(self): """Merge nearby regions with the same value. """ - cdef: - int32_t new_pre_pos, pos, i - float32_t new_pre_value, value - bytes chrom - set chrs + # new_pre_pos: cython.int + pos: cython.int + i: cython.int + new_pre_value: cython.float + value: cython.float + chrom: bytes + chrs: set chrs = self.get_chr_names() for chrom in sorted(chrs): - (p,v) = self.__data[chrom] + (p, v) = self.__data[chrom] pnext = iter(p).__next__ vnext = iter(v).__next__ # new arrays - new_pos = pyarray('L',[pnext(),]) - new_value = pyarray('f',[vnext(),]) + new_pos = pyarray('L', [pnext(),]) + new_value = pyarray('f', [vnext(),]) newpa = new_pos.append newva = new_value.append - new_pre_pos = new_pos[0] + # new_pre_pos = new_pos[0] new_pre_value = new_value[0] - for i in range(1,len(p)): + for i in range(1, len(p)): pos = pnext() value = vnext() if value == new_pre_value: @@ -314,33 +350,36 @@ def __init__ (self, float32_t baseline_value=0 ): # add new region newpa(pos) newva(value) - new_pre_pos = pos + # new_pre_pos = pos new_pre_value = value - self.__data[chrom] = [new_pos,new_value] + self.__data[chrom] = [new_pos, new_value] return True - cpdef bool filter_score (self, float32_t cutoff=0): + @cython.ccall + def filter_score(self, cutoff: cython.float = 0) -> bool: """Filter using a score cutoff. Any region lower than score cutoff will be set to self.baseline_value. Self will be modified. """ - cdef: - int32_t new_pre_pos, pos, i - float32_t new_pre_value, value - bytes chrom - set chrs + # new_pre_pos: cython.int + pos: cython.int + i: cython.int + new_pre_value: cython.float + value: cython.float + chrom: bytes + chrs: set chrs = self.get_chr_names() for chrom in sorted(chrs): - (p,v) = self.__data[chrom] + (p, v) = self.__data[chrom] pnext = iter(p).__next__ vnext = iter(v).__next__ # new arrays - new_pos = pyarray('L',[]) - new_value = pyarray('f',[]) - new_pre_pos = 0 + new_pos = pyarray('L', []) + new_value = pyarray('f', []) + # new_pre_pos = 0 new_pre_value = 0 for i in range(len(p)): @@ -360,54 +399,66 @@ def __init__ (self, float32_t baseline_value=0 ): # put it into new arrays new_pos.append(pos) new_value.append(value) - new_pre_pos = new_pos[-1] + # new_pre_pos = new_pos[-1] new_pre_value = new_value[-1] - self.__data[chrom]=[new_pos,new_value] + self.__data[chrom] = [new_pos, new_value] return True - cpdef tuple summary (self): - """Calculate the sum, total_length, max, min, mean, and std. + @cython.ccall + def summary(self) -> tuple: + """Calculate the sum, total_length, max, min, mean, and std. Return a tuple for (sum, total_length, max, min, mean, std). + """ - cdef: - int64_tn_v - float32_t sum_v, max_v, min_v, mean_v, variance, tmp, std_v - int32_t pre_p, l, i + n_v: cython.long + sum_v: cython.float + max_v: cython.float + min_v: cython.float + mean_v: cython.float + variance: cython.float + tmp: cython.float + std_v: cython.float + pre_p: cython.int + ln: cython.int + i: cython.int pre_p = 0 n_v = 0 sum_v = 0 max_v = -100000 min_v = 100000 - for (p,v) in self.__data.values(): + for (p, v) in self.__data.values(): # for each chromosome pre_p = 0 for i in range(len(p)): # for each region - l = p[i]-pre_p - sum_v += v[i]*l - n_v += l + ln = p[i]-pre_p + sum_v += v[i]*ln + n_v += ln pre_p = p[i] - max_v = max(max(v),max_v) - min_v = min(min(v),min_v) + max_v = max(max(v), max_v) + min_v = min(min(v), min_v) mean_v = sum_v/n_v variance = 0.0 - for (p,v) in self.__data.values(): + for (p, v) in self.__data.values(): for i in range(len(p)): # for each region tmp = v[i]-mean_v - l = p[i]-pre_p - variance += tmp*tmp*l + ln = p[i]-pre_p + variance += tmp*tmp*ln pre_p = p[i] variance /= float(n_v-1) std_v = sqrt(variance) return (sum_v, n_v, max_v, min_v, mean_v, std_v) - cpdef object call_peaks (self, float32_t cutoff=1, - int32_t min_length=200, int32_t max_gap=50, - bool call_summits=False): + @cython.ccall + def call_peaks(self, + cutoff: cython.float = 1, + min_length: cython.int = 200, + max_gap: cython.int = 50, + call_summits: bool = False): """This function try to find regions within which, scores are continuously higher than a given cutoff. @@ -430,19 +481,21 @@ def __init__ (self, float32_t baseline_value=0 ): included as `gap` . """ - cdef: - int32_t peak_length, x, pre_p, p, i, summit, tstart, tend - float32_t v, summit_value, tvalue - bytes chrom - set chrs - object peaks + # peak_length: cython.int + x: cython.int + pre_p: cython.int + p: cython.int + i: cython.int + v: cython.float + chrom: bytes + chrs: set chrs = self.get_chr_names() peaks = PeakIO() # dictionary to save peaks for chrom in sorted(chrs): peak_content = None - peak_length = 0 - (ps,vs) = self.get_data_by_chr(chrom) # arrays for position and values + # peak_length = 0 + (ps, vs) = self.get_data_by_chr(chrom) # arrays for position and values psn = iter(ps).__next__ # assign the next function to a viable to speed up vsn = iter(vs).__next__ x = 0 @@ -452,72 +505,90 @@ def __init__ (self, float32_t baseline_value=0 ): try: # try to read the first data range for this chrom p = psn() v = vsn() - except: + except Exception: break x += 1 # index for the next point if v >= cutoff: - peak_content = [(pre_p,p,v),] + peak_content = [(pre_p, p, v),] pre_p = p break # found the first range above cutoff else: pre_p = p - for i in range(x,len(ps)): + for i in range(x, len(ps)): # continue scan the rest regions p = psn() v = vsn() - if v < cutoff: # not be detected as 'peak' + if v < cutoff: # not be detected as 'peak' pre_p = p continue # for points above cutoff # if the gap is allowed if pre_p - peak_content[-1][1] <= max_gap: - peak_content.append((pre_p,p,v)) + peak_content.append((pre_p, p, v)) else: # when the gap is not allowed, close this peak - self.__close_peak(peak_content, peaks, min_length, chrom) #, smoothlen=max_gap / 2 ) + self.__close_peak(peak_content, + peaks, + min_length, + chrom) # , smoothlen=max_gap / 2) # start a new peak - peak_content = [(pre_p,p,v),] + peak_content = [(pre_p, p, v),] pre_p = p # save the last peak if not peak_content: continue - self.__close_peak(peak_content, peaks, min_length, chrom) #, smoothlen=max_gap / 2 ) + self.__close_peak(peak_content, + peaks, + min_length, + chrom) # , smoothlen=max_gap / 2) return peaks - cdef bool __close_peak( self, list peak_content, object peaks, int32_t min_length, bytes chrom ): - cdef: - list tsummit # list for temporary summits - int32_t peak_length, summit, tstart, tend - float32_t summit_value, tvalue - + @cython.cfunc + def __close_peak(self, + peak_content: list, + peaks, + min_length: cython.int, + chrom: bytes) -> bool: + tsummit: list # list for temporary summits + peak_length: cython.int + summit: cython.int + tstart: cython.int + tend: cython.int + summit_value: cython.float + tvalue: cython.float peak_length = peak_content[-1][1]-peak_content[0][0] - if peak_length >= min_length: # if the peak is too small, reject it + if peak_length >= min_length: # if the peak is too small, reject it tsummit = [] summit = 0 summit_value = 0 - for (tstart,tend,tvalue) in peak_content: + for (tstart, tend, tvalue) in peak_content: if not summit_value or summit_value < tvalue: - tsummit = [((tend+tstart)/2),] + tsummit = [cython.cast(cython.int, (tend+tstart)/2),] summit_value = tvalue elif summit_value == tvalue: - tsummit.append( ((tend+tstart)/2) ) - summit = tsummit[((len(tsummit)+1)/2)-1 ] - peaks.add( chrom, - peak_content[0][0], - peak_content[-1][1], - summit = summit, - peak_score = summit_value, - pileup = 0, - pscore = 0, - fold_change = 0, - qscore = 0 - ) + tsummit.append(cython.cast(cython.int, (tend+tstart)/2)) + summit = tsummit[cython.cast(cython.int, (len(tsummit)+1)/2)-1] + peaks.add(chrom, + peak_content[0][0], + peak_content[-1][1], + summit=summit, + peak_score=summit_value, + pileup=0, + pscore=0, + fold_change=0, + qscore=0 + ) return True - cpdef object call_broadpeaks (self, float32_t lvl1_cutoff=500, float32_t lvl2_cutoff=100, - int32_t min_length=200, int32_t lvl1_max_gap=50, int32_t lvl2_max_gap=400): + @cython.ccall + def call_broadpeaks(self, + lvl1_cutoff: cython.float = 500, + lvl2_cutoff: cython.float = 100, + min_length: cython.int = 200, + lvl1_max_gap: cython.int = 50, + lvl2_max_gap: cython.int = 400): """This function try to find enriched regions within which, scores are continuously higher than a given cutoff for level 1, and link them using the gap above level 2 cutoff with a @@ -533,17 +604,25 @@ def __init__ (self, float32_t baseline_value=0 ): Return both general PeakIO object for highly enriched regions and gapped broad regions in BroadPeakIO. """ - cdef: - bytes chrom - int32_t i, j - set chrs - object lvl1, lvl2 # PeakContent class object - list temppeakset, lvl1peakschrom, lvl2peakschrom - + chrom: bytes + i: cython.int + j: cython.int + chrs: set + lvl1: PeakContent + lvl2: PeakContent # PeakContent class object + lvl1peakschrom: list + lvl2peakschrom: list + assert lvl1_cutoff > lvl2_cutoff, "level 1 cutoff should be larger than level 2." assert lvl1_max_gap < lvl2_max_gap, "level 2 maximum gap should be larger than level 1." - lvl1_peaks = self.call_peaks( cutoff=lvl1_cutoff, min_length=min_length, max_gap=lvl1_max_gap, call_summits=False ) - lvl2_peaks = self.call_peaks( cutoff=lvl2_cutoff, min_length=min_length, max_gap=lvl2_max_gap, call_summits=False ) + lvl1_peaks = self.call_peaks(cutoff=lvl1_cutoff, + min_length=min_length, + max_gap=lvl1_max_gap, + call_summits=False) + lvl2_peaks = self.call_peaks(cutoff=lvl2_cutoff, + min_length=min_length, + max_gap=lvl2_max_gap, + call_summits=False) chrs = lvl1_peaks.get_chr_names() broadpeaks = BroadPeakIO() # use lvl2_peaks as linking regions between lvl1_peaks @@ -555,51 +634,73 @@ def __init__ (self, float32_t baseline_value=0 ): # our assumption is lvl1 regions should be included in lvl2 regions try: lvl1 = lvl1peakschrom_next() - for i in range( len(lvl2peakschrom) ): + for i in range(len(lvl2peakschrom)): # for each lvl2 peak, find all lvl1 peaks inside lvl2 = lvl2peakschrom[i] while True: - if lvl2["start"] <= lvl1["start"] and lvl1["end"] <= lvl2["end"]: + if lvl2["start"] <= lvl1["start"] and lvl1["end"] <= lvl2["end"]: tmppeakset.append(lvl1) lvl1 = lvl1peakschrom_next() else: - self.__add_broadpeak ( broadpeaks, chrom, lvl2, tmppeakset) + self.__add_broadpeak(broadpeaks, + chrom, + lvl2, + tmppeakset) tmppeakset = [] break except StopIteration: - self.__add_broadpeak ( broadpeaks, chrom, lvl2, tmppeakset) + self.__add_broadpeak(broadpeaks, chrom, lvl2, tmppeakset) tmppeakset = [] - for j in range( i+1, len(lvl2peakschrom) ): - self.__add_broadpeak ( broadpeaks, chrom, lvl2peakschrom[j], tmppeakset) + for j in range(i+1, len(lvl2peakschrom)): + self.__add_broadpeak(broadpeaks, + chrom, + lvl2peakschrom[j], + tmppeakset) return broadpeaks - cdef object __add_broadpeak (self, object bpeaks, bytes chrom, object lvl2peak, list lvl1peakset): + @cython.cfunc + def __add_broadpeak(self, + bpeaks, + chrom: bytes, + lvl2peak: PeakContent, + lvl1peakset: list): """Internal function to create broad peak. - """ - cdef: - int32_t start, end, blockNum - bytes blockSizes, blockStarts, thickStart, thickEnd - - start = lvl2peak["start"] - end = lvl2peak["end"] - - # the following code will add those broad/lvl2 peaks with no strong/lvl1 peaks inside + start: cython.int + end: cython.int + blockNum: cython.int + blockSizes: bytes + blockStarts: bytes + thickStart: bytes + thickEnd: bytes + + start = lvl2peak["start"] + end = lvl2peak["end"] + + # the following code will add those broad/lvl2 peaks with no + # strong/lvl1 peaks inside if not lvl1peakset: # try: # will complement by adding 1bps start and end to this region # may change in the future if gappedPeak format was improved. - bpeaks.add(chrom, start, end, score=lvl2peak["score"], thickStart=(b"%d" % start), thickEnd=(b"%d" % end), - blockNum = 2, blockSizes = b"1,1", blockStarts = (b"0,%d" % (end-start-1)), pileup = lvl2peak["pileup"], - pscore = lvl2peak["pscore"], fold_change = lvl2peak["fc"], - qscore = lvl2peak["qscore"] ) + bpeaks.add(chrom, start, end, + score=lvl2peak["score"], + thickStart=(b"%d" % start), + thickEnd=(b"%d" % end), + blockNum=2, + blockSizes=b"1,1", + blockStarts=(b"0,%d" % (end-start-1)), + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) return bpeaks thickStart = b"%d" % lvl1peakset[0]["start"] - thickEnd = b"%d" % lvl1peakset[-1]["end"] - blockNum = len(lvl1peakset) - blockSizes = b",".join( [b"%d" % x["length"] for x in lvl1peakset] ) - blockStarts = b",".join( [b"%d" % (x["start"]-start) for x in lvl1peakset] ) + thickEnd = b"%d" % lvl1peakset[-1]["end"] + blockNum = len(lvl1peakset) + blockSizes = b",".join([b"%d" % x["length"] for x in lvl1peakset]) + blockStarts = b",".join([b"%d" % (x["start"]-start) for x in lvl1peakset]) if int(thickStart) != start: # add 1bp left block @@ -614,62 +715,72 @@ def __init__ (self, float32_t baseline_value=0 ): blockSizes = blockSizes+b",1" blockStarts = blockStarts + b"," + (b"%d" % (end-start-1)) - bpeaks.add(chrom, start, end, score=lvl2peak["score"], thickStart=thickStart, thickEnd=thickEnd, - blockNum = blockNum, blockSizes = blockSizes, blockStarts = blockStarts, pileup = lvl2peak["pileup"], - pscore = lvl2peak["pscore"], fold_change = lvl2peak["fc"], - qscore = lvl2peak["qscore"] ) + bpeaks.add(chrom, start, end, + score=lvl2peak["score"], + thickStart=thickStart, + thickEnd=thickEnd, + blockNum=blockNum, + blockSizes=blockSizes, + blockStarts=blockStarts, + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) return bpeaks - cpdef object refine_peaks (self, object peaks): + @cython.ccall + def refine_peaks(self, peaks): """This function try to based on given peaks, re-evaluate the peak region, call the summit. peaks: PeakIO object - return: a new PeakIO object """ - cdef: - int32_t peak_length, x, pre_p, p, i, peak_s, peak_e - float32_t v - bytes chrom - set chrs - object new_peaks + pre_p: cython.int + p: cython.int + peak_s: cython.int + peak_e: cython.int + v: cython.float + chrom: bytes + chrs: set peaks.sort() new_peaks = PeakIO() chrs = self.get_chr_names() assert isinstance(peaks, PeakIO) chrs = chrs.intersection(set(peaks.get_chr_names())) - + for chrom in sorted(chrs): peaks_chr = peaks.get_data_from_chrom(chrom) peak_content = [] - (ps,vs) = self.get_data_by_chr(chrom) # arrays for position and values - psn = iter(ps).__next__ # assign the next function to a viable to speed up + # arrays for position and values + (ps, vs) = self.get_data_by_chr(chrom) + # assign the next function to a viable to speed up + psn = iter(ps).__next__ vsn = iter(vs).__next__ peakn = iter(peaks_chr).__next__ - pre_p = 0 # remember previous position in bedgraph/self + # remember previous position in bedgraph/self + pre_p = 0 p = psn() v = vsn() peak = peakn() peak_s = peak["start"] peak_e = peak["end"] - while True: # look for overlap if p > peak_s and peak_e > pre_p: # now put four coordinates together and pick the middle two s, e = sorted([p, peak_s, peak_e, pre_p])[1:3] # add this content - peak_content.append( (s, e, v) ) + peak_content.append((s, e, v)) # move self/bedGraph try: pre_p = p p = psn() v = vsn() - except: + except Exception: # no more value chunk in bedGraph break elif pre_p >= peak_e: @@ -681,7 +792,7 @@ def __init__ (self, float32_t baseline_value=0 ): peak = peakn() peak_s = peak["start"] peak_e = peak["end"] - except: + except Exception: # no more peak break elif peak_s >= p: @@ -690,7 +801,7 @@ def __init__ (self, float32_t baseline_value=0 ): pre_p = p p = psn() v = vsn() - except: + except Exception: # no more value chunk in bedGraph break else: @@ -701,39 +812,39 @@ def __init__ (self, float32_t baseline_value=0 ): self.__close_peak(peak_content, new_peaks, 0, chrom) return new_peaks - - cpdef int32_t total (self): + @cython.ccall + def total(self) -> cython.int: """Return the number of regions in this object. """ - cdef: - int32_t t + t: cython.int t = 0 - for ( p, v ) in self.__data.values(): + for (p, v) in self.__data.values(): t += len(p) return t - cpdef object set_single_value (self, float32_t new_value): + @cython.ccall + def set_single_value(self, new_value: cython.float): """Change all the values in bedGraph to the same new_value, return a new bedGraphTrackI. """ - cdef: - bytes chrom - int32_t max_p - object ret + chrom: bytes + max_p: cython.int ret = bedGraphTrackI() chroms = set(self.get_chr_names()) for chrom in sorted(chroms): - (p1,v1) = self.get_data_by_chr(chrom) # arrays for position and values + # arrays for position and values + (p1, v1) = self.get_data_by_chr(chrom) # maximum p max_p = max(p1) # add a region from 0 to max_p - ret.add_loc(chrom,0,max_p,new_value) + ret.add_loc(chrom, 0, max_p, new_value) return ret - cpdef object overlie (self, object bdgTracks, str func="max" ): + @cython.ccall + def overlie(self, bdgTracks, func: str = "max"): """Calculate two or more bedGraphTrackI objects by letting self overlying bdgTrack2, with user-defined functions. @@ -769,10 +880,8 @@ def __init__ (self, float32_t baseline_value=0 ): Option: bdgTracks can be a list of bedGraphTrackI objects """ - cdef: - int32_t pre_p, p1, p2 - float32_t v1, v2 - bytes chrom + pre_p: cython.int + chrom: bytes nr_tracks = len(bdgTracks) + 1 # +1 for self assert nr_tracks >= 2, "Specify at least one more bdg objects." @@ -803,7 +912,6 @@ def __init__ (self, float32_t baseline_value=0 ): raise Exception("Invalid function {func}! Choose from 'sum', 'subtract' (only for two bdg objects), 'product', 'divide' (only for two bdg objects), 'max', 'mean' and 'fisher'. ") ret = bedGraphTrackI() - retadd = ret.add_loc common_chr = set(self.get_chr_names()) for track in bdgTracks: @@ -844,69 +952,79 @@ def __init__ (self, float32_t baseline_value=0 ): pass return ret - cpdef bool apply_func ( self, func ): + @cython.ccall + def apply_func(self, func) -> bool: """Apply function 'func' to every value in this bedGraphTrackI object. *Two adjacent regions with same value after applying func will not be merged. """ - cdef int32_t i + i: cython.int - for (p,s) in self.__data.values(): + for (p, s) in self.__data.values(): for i in range(len(s)): s[i] = func(s[i]) self.maxvalue = func(self.maxvalue) self.minvalue = func(self.minvalue) return True - cpdef p2q ( self ): + @cython.ccall + def p2q(self): """Convert pvalue scores to qvalue scores. *Assume scores in this bedGraph are pvalue scores! Not work for other type of scores. """ - cdef: - bytes chrom - object pos_array, pscore_array - dict pvalue_stat = {} - dict pqtable = {} - int64_t n, pre_p, this_p, length, j, pre_l, l, i - float32_t this_v, pre_v, v, q, pre_q, this_t, this_c - int64_t N, k, this_l - float32_t f - int64_t nhcal = 0 - int64_t npcal = 0 - list unique_values - float32_t t0, t1, t + chrom: bytes + pos_array: pyarray + pscore_array: pyarray + pvalue_stat: dict = {} + pqtable: dict = {} + pre_p: cython.long + this_p: cython.long + # pre_l: cython.long + # l: cython.long + i: cython.long + nhcal: cython.long = 0 + N: cython.long + k: cython.long + this_l: cython.long + this_v: cython.float + # pre_v: cython.float + v: cython.float + q: cython.float + pre_q: cython.float + f: cython.float + unique_values: list # calculate frequencies of each p-score for chrom in sorted(self.get_chr_names()): pre_p = 0 - [pos_array, pscore_array] = self.__data[ chrom ] + [pos_array, pscore_array] = self.__data[chrom] pn = iter(pos_array).__next__ vn = iter(pscore_array).__next__ - for i in range( len( pos_array ) ): + for i in range(len(pos_array)): this_p = pn() this_v = vn() this_l = this_p - pre_p if this_v in pvalue_stat: - pvalue_stat[ this_v ] += this_l + pvalue_stat[this_v] += this_l else: - pvalue_stat[ this_v ] = this_l + pvalue_stat[this_v] = this_l pre_p = this_p - nhcal += len( pos_array ) + # nhcal += len(pos_array) - nhval = 0 + # nhval = 0 - N = sum(pvalue_stat.values()) # total length - k = 1 # rank + N = sum(pvalue_stat.values()) # total length + k = 1 # rank f = -log10(N) - pre_v = -2147483647 - pre_l = 0 + # pre_v = -2147483647 + # pre_l = 0 pre_q = 2147483647 # save the previous q-value # calculate qscore for each pscore @@ -914,40 +1032,43 @@ def __init__ (self, float32_t baseline_value=0 ): unique_values = sorted(pvalue_stat.keys(), reverse=True) for i in range(len(unique_values)): v = unique_values[i] - l = pvalue_stat[v] + # l = pvalue_stat[v] q = v + (log10(k) + f) - q = max(0,min(pre_q,q)) # make q-score monotonic - pqtable[ v ] = q - pre_v = v + q = max(0, min(pre_q, q)) # make q-score monotonic + pqtable[v] = q + # pre_v = v pre_q = q - k+=l + # k += l nhcal += 1 # convert pscore to qscore for chrom in sorted(self.get_chr_names()): - [pos_array, pscore_array] = self.__data[ chrom ] + [pos_array, pscore_array] = self.__data[chrom] - for i in range( len( pos_array ) ): - pscore_array[ i ] = pqtable[ pscore_array[ i ] ] + for i in range(len(pos_array)): + pscore_array[i] = pqtable[pscore_array[i]] self.merge_regions() return - - cpdef object extract_value ( self, object bdgTrack2 ): + @cython.ccall + def extract_value(self, bdgTrack2): """Extract values from regions defined in bedGraphTrackI class object `bdgTrack2`. """ - cdef: - int32_t pre_p, p1, p2, i - float32_t v1, v2 - bytes chrom - object ret - - assert isinstance(bdgTrack2,bedGraphTrackI), "not a bedGraphTrackI object" - - ret = [ [], pyarray('f',[]), pyarray('L',[]) ] # 1: region in bdgTrack2; 2: value; 3: length with the value + pre_p: cython.int + p1: cython.int + p2: cython.int + i: cython.int + v1: cython.float + v2: cython.float + chrom: bytes + + assert isinstance(bdgTrack2, bedGraphTrackI), "not a bedGraphTrackI object" + + # 1: region in bdgTrack2; 2: value; 3: length with the value + ret = [[], pyarray('f', []), pyarray('L', [])] radd = ret[0].append vadd = ret[1].append ladd = ret[2].append @@ -955,16 +1076,21 @@ def __init__ (self, float32_t baseline_value=0 ): chr1 = set(self.get_chr_names()) chr2 = set(bdgTrack2.get_chr_names()) common_chr = chr1.intersection(chr2) - for i in range( len( common_chr ) ): + for i in range(len(common_chr)): chrom = common_chr.pop() - (p1s,v1s) = self.get_data_by_chr(chrom) # arrays for position and values - p1n = iter(p1s).__next__ # assign the next function to a viable to speed up + (p1s, v1s) = self.get_data_by_chr(chrom) # arrays for position and values + # assign the next function to a viable to speed up + p1n = iter(p1s).__next__ v1n = iter(v1s).__next__ - (p2s,v2s) = bdgTrack2.get_data_by_chr(chrom) # arrays for position and values - p2n = iter(p2s).__next__ # assign the next function to a viable to speed up + # arrays for position and values + (p2s, v2s) = bdgTrack2.get_data_by_chr(chrom) + # assign the next function to a viable to speed up + p2n = iter(p2s).__next__ v2n = iter(v2s).__next__ - pre_p = 0 # remember the previous position in the new bedGraphTrackI object ret + # remember the previous position in the new bedGraphTrackI + # object ret + pre_p = 0 try: p1 = p1n() v1 = v1n() @@ -975,7 +1101,7 @@ def __init__ (self, float32_t baseline_value=0 ): while True: if p1 < p2: # clip a region from pre_p to p1, then set pre_p as p1. - if v2>0: + if v2 > 0: radd(str(chrom)+"."+str(pre_p)+"."+str(p1)) vadd(v1) ladd(p1-pre_p) @@ -984,8 +1110,9 @@ def __init__ (self, float32_t baseline_value=0 ): p1 = p1n() v1 = v1n() elif p2 < p1: - # clip a region from pre_p to p2, then set pre_p as p2. - if v2>0: + # clip a region from pre_p to p2, then set + # pre_p as p2. + if v2 > 0: radd(str(chrom)+"."+str(pre_p)+"."+str(p2)) vadd(v1) ladd(p2-pre_p) @@ -995,7 +1122,7 @@ def __init__ (self, float32_t baseline_value=0 ): v2 = v2n() elif p1 == p2: # from pre_p to p1 or p2, then set pre_p as p1 or p2. - if v2>0: + if v2 > 0: radd(str(chrom)+"."+str(pre_p)+"."+str(p1)) vadd(v1) ladd(p1-pre_p) @@ -1011,7 +1138,8 @@ def __init__ (self, float32_t baseline_value=0 ): return ret - cpdef object extract_value_hmmr ( self, object bdgTrack2 ): + @cython.ccall + def extract_value_hmmr(self, bdgTrack2): """Extract values from regions defined in bedGraphTrackI class object `bdgTrack2`. @@ -1023,15 +1151,19 @@ def __init__ (self, float32_t baseline_value=0 ): 'mark_bin' -- the bins in the same region will have the same value. """ - cdef: - int32_t pre_p, p1, p2, i - float32_t v1, v2 - bytes chrom - list ret - - assert isinstance(bdgTrack2,bedGraphTrackI), "not a bedGraphTrackI object" - - ret = [ [], pyarray('f',[]), pyarray('i',[]) ] # 0: bin location (chrom, position); 1: value; 2: number of bins in this region + # pre_p: cython.int + p1: cython.int + p2: cython.int + i: cython.int + v1: cython.float + v2: cython.float + chrom: bytes + ret: list + + assert isinstance(bdgTrack2, bedGraphTrackI), "not a bedGraphTrackI object" + + # 0: bin location (chrom, position); 1: value; 2: number of bins in this region + ret = [[], pyarray('f', []), pyarray('i', [])] padd = ret[0].append vadd = ret[1].append ladd = ret[2].append @@ -1039,16 +1171,22 @@ def __init__ (self, float32_t baseline_value=0 ): chr1 = set(self.get_chr_names()) chr2 = set(bdgTrack2.get_chr_names()) common_chr = sorted(list(chr1.intersection(chr2))) - for i in range( len( common_chr ) ): + for i in range(len(common_chr)): chrom = common_chr.pop() - (p1s,v1s) = self.get_data_by_chr(chrom) # arrays for position and values - p1n = iter(p1s).__next__ # assign the next function to a viable to speed up + # arrays for position and values + (p1s, v1s) = self.get_data_by_chr(chrom) + # assign the next function to a viable to speed up + p1n = iter(p1s).__next__ v1n = iter(v1s).__next__ - (p2s,v2s) = bdgTrack2.get_data_by_chr(chrom) # arrays for position and values - p2n = iter(p2s).__next__ # assign the next function to a viable to speed up + # arrays for position and values + (p2s, v2s) = bdgTrack2.get_data_by_chr(chrom) + # assign the next function to a viable to speed up + p2n = iter(p2s).__next__ v2n = iter(v2s).__next__ - pre_p = 0 # remember the previous position in the new bedGraphTrackI object ret + # remember the previous position in the new bedGraphTrackI + # object ret + # pre_p = 0 try: p1 = p1n() v1 = v1n() @@ -1060,31 +1198,31 @@ def __init__ (self, float32_t baseline_value=0 ): if p1 < p2: # clip a region from pre_p to p1, then set pre_p as p1. # in this case, we don't output any - #if v2>0: + # if v2>0: # radd(str(chrom)+"."+str(pre_p)+"."+str(p1)) # vadd(v1) # ladd(p1-pre_p) - pre_p = p1 + # pre_p = p1 # call for the next p1 and v1 p1 = p1n() v1 = v1n() elif p2 < p1: # clip a region from pre_p to p2, then set pre_p as p2. - if v2 != 0: #0 means it's a gap region, we should have value > 1 - padd( (chrom, p2) ) + if v2 != 0: # 0 means it's a gap region, we should have value > 1 + padd((chrom, p2)) vadd(v1) ladd(int(v2)) - pre_p = p2 + # pre_p = p2 # call for the next p2 and v2 p2 = p2n() v2 = v2n() elif p1 == p2: # from pre_p to p1 or p2, then set pre_p as p1 or p2. - if v2 != 0: #0 means it's a gap region, we should have 1 or -1 - padd( (chrom, p2) ) + if v2 != 0: # 0 means it's a gap region, we should have 1 or -1 + padd((chrom, p2)) vadd(v1) ladd(int(v2)) - pre_p = p1 + # pre_p = p1 # call for the next p1, v1, p2, v2. p1 = p1n() v1 = v1n() @@ -1096,7 +1234,10 @@ def __init__ (self, float32_t baseline_value=0 ): return ret - cpdef make_ScoreTrackII_for_macs (self, object bdgTrack2, float32_t depth1 = 1.0, float32_t depth2 = 1.0 ): + @cython.ccall + def make_ScoreTrackII_for_macs(self, bdgTrack2, + depth1: float = 1.0, + depth2: float = 1.0): """A modified overlie function for MACS v2. effective_depth_in_million: sequencing depth in million after @@ -1108,35 +1249,43 @@ def __init__ (self, float32_t baseline_value=0 ): Return value is a ScoreTrackII object. """ - cdef: - int32_t pre_p, p1, p2 - float32_t v1, v2 - bytes chrom - object ret + # pre_p: cython.int + p1: cython.int + p2: cython.int + v1: cython.float + v2: cython.float + chrom: bytes - assert isinstance(bdgTrack2,bedGraphTrackI), "bdgTrack2 is not a bedGraphTrackI object" + assert isinstance(bdgTrack2, bedGraphTrackI), "bdgTrack2 is not a bedGraphTrackI object" - ret = ScoreTrackII( treat_depth = depth1, ctrl_depth = depth2 ) + ret = ScoreTrackII(treat_depth=depth1, + ctrl_depth=depth2) retadd = ret.add chr1 = set(self.get_chr_names()) chr2 = set(bdgTrack2.get_chr_names()) common_chr = chr1.intersection(chr2) for chrom in sorted(common_chr): - - (p1s,v1s) = self.get_data_by_chr(chrom) # arrays for position and values - p1n = iter(p1s).__next__ # assign the next function to a viable to speed up + # arrays for position and values + (p1s, v1s) = self.get_data_by_chr(chrom) + # assign the next function to a viable to speed up + p1n = iter(p1s).__next__ v1n = iter(v1s).__next__ - - (p2s,v2s) = bdgTrack2.get_data_by_chr(chrom) # arrays for position and values - p2n = iter(p2s).__next__ # assign the next function to a viable to speed up + # arrays for position and values + (p2s, v2s) = bdgTrack2.get_data_by_chr(chrom) + # assign the next function to a viable to speed up + p2n = iter(p2s).__next__ v2n = iter(v2s).__next__ - chrom_max_len = len(p1s)+len(p2s) # this is the maximum number of locations needed to be recorded in scoreTrackI for this chromosome. + # this is the maximum number of locations needed to be + # recorded in scoreTrackI for this chromosome. + chrom_max_len = len(p1s)+len(p2s) - ret.add_chromosome(chrom,chrom_max_len) + ret.add_chromosome(chrom, chrom_max_len) - pre_p = 0 # remember the previous position in the new bedGraphTrackI object ret + # remember the previous position in the new bedGraphTrackI + # object ret + # pre_p = 0 try: p1 = p1n() @@ -1148,22 +1297,22 @@ def __init__ (self, float32_t baseline_value=0 ): while True: if p1 < p2: # clip a region from pre_p to p1, then set pre_p as p1. - retadd( chrom, p1, v1, v2 ) - pre_p = p1 + retadd(chrom, p1, v1, v2) + # pre_p = p1 # call for the next p1 and v1 p1 = p1n() v1 = v1n() elif p2 < p1: # clip a region from pre_p to p2, then set pre_p as p2. - retadd( chrom, p2, v1, v2 ) - pre_p = p2 + retadd(chrom, p2, v1, v2) + # pre_p = p2 # call for the next p2 and v2 p2 = p2n() v2 = v2n() elif p1 == p2: # from pre_p to p1 or p2, then set pre_p as p1 or p2. - retadd( chrom, p1, v1, v2 ) - pre_p = p1 + retadd(chrom, p1, v1, v2) + # pre_p = p1 # call for the next p1, v1, p2, v2. p1 = p1n() v1 = v1n() @@ -1174,13 +1323,19 @@ def __init__ (self, float32_t baseline_value=0 ): pass ret.finalize() - #ret.merge_regions() + # ret.merge_regions() return ret - cpdef str cutoff_analysis ( self, int32_t max_gap, int32_t min_length, int32_t steps = 100, float32_t min_score = 0, float32_t max_score = 1000 ): + @cython.ccall + def cutoff_analysis(self, + max_gap: cython.int, + min_length: cython.int, + steps: cython.int = 100, + min_score: cython.float = 0, + max_score: cython.float = 1000) -> str: """ Cutoff analysis function for bedGraphTrackI object. - + This function will try all possible cutoff values on the score column to call peaks. Then will give a report of a number of metrics (number of peaks, total length of peaks, average @@ -1196,7 +1351,7 @@ def __init__ (self, float32_t baseline_value=0 ): max_gap : int32_t Maximum allowed gap between consecutive positions above cutoff - + min_length : int32_t Minimum length of peak steps: int32_t It will be used to calculate 'step' to increase from min_v to @@ -1228,47 +1383,61 @@ def __init__ (self, float32_t baseline_value=0 ): can add more ways to analyze the result. Also, we can let this function return a list of dictionary or data.frame in that way, instead of str object. - """ - cdef: - set chrs - list peak_content, ret_list, cutoff_list, cutoff_npeaks, cutoff_lpeaks - bytes chrom - str ret - float32_t cutoff - int64_t total_l, total_p, i, n, ts, te, lastp, tl, peak_length - #dict cutoff_npeaks, cutoff_lpeaks - float32_t s, midvalue + chrs: set + peak_content: list + ret_list: list + cutoff_list: list + cutoff_npeaks: list + cutoff_lpeaks: list + chrom: bytes + ret: str + cutoff: cython.float + total_l: cython.long + total_p: cython.long + i: cython.long + n: cython.long + ts: cython.long + te: cython.long + lastp: cython.long + tl: cython.long + peak_length: cython.long + # dict cutoff_npeaks, cutoff_lpeaks + s: cython.float chrs = self.get_chr_names() - #midvalue = self.minvalue/2 + self.maxvalue/2 - #s = float(self.minvalue - midvalue)/steps - minv = max( min_score, self.minvalue ) - maxv = min( self.maxvalue, max_score ) + # midvalue = self.minvalue/2 + self.maxvalue/2 + # s = float(self.minvalue - midvalue)/steps + minv = max(min_score, self.minvalue) + maxv = min(self.maxvalue, max_score) s = float(maxv - minv)/steps # a list of possible cutoff values from minv to maxv with step of s cutoff_list = [round(value, 3) for value in np.arange(minv, maxv, s)] - cutoff_npeaks = [0] * len( cutoff_list ) - cutoff_lpeaks = [0] * len( cutoff_list ) + cutoff_npeaks = [0] * len(cutoff_list) + cutoff_lpeaks = [0] * len(cutoff_list) for chrom in sorted(chrs): - ( pos_array, score_array ) = self.__data[ chrom ] - pos_array = np.array( self.__data[ chrom ][ 0 ] ) - score_array = np.array( self.__data[ chrom ][ 1 ] ) + (pos_array, score_array) = self.__data[chrom] + pos_array = np.array(self.__data[chrom][0]) + score_array = np.array(self.__data[chrom][1]) - for n in range( len( cutoff_list ) ): - cutoff = cutoff_list[ n ] + for n in range(len(cutoff_list)): + cutoff = cutoff_list[n] total_l = 0 # total length of peaks total_p = 0 # total number of peaks - # get the regions with scores above cutoffs - above_cutoff = np.nonzero( score_array > cutoff )[0]# this is not an optimized method. It would be better to store score array in a 2-D ndarray? - above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff - above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff + # get the regions with scores above cutoffs. This is + # not an optimized method. It would be better to store + # score array in a 2-D ndarray? + above_cutoff = np.nonzero(score_array > cutoff)[0] + # end positions of regions where score is above cutoff + above_cutoff_endpos = pos_array[above_cutoff] + # start positions of regions where score is above cutoff + above_cutoff_startpos = pos_array[above_cutoff-1] if above_cutoff_endpos.size == 0: continue @@ -1279,62 +1448,66 @@ def __init__ (self, float32_t baseline_value=0 ): ts = acs_next() te = ace_next() - peak_content = [( ts, te ), ] + peak_content = [(ts, te),] lastp = te - for i in range( 1, above_cutoff_startpos.size ): + for i in range(1, above_cutoff_startpos.size): ts = acs_next() te = ace_next() tl = ts - lastp if tl <= max_gap: - peak_content.append( ( ts, te ) ) + peak_content.append((ts, te)) else: - peak_length = peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] - if peak_length >= min_length: # if the peak is too small, reject it - total_l += peak_length + peak_length = peak_content[-1][1] - peak_content[0][0] + # if the peak is too small, reject it + if peak_length >= min_length: + total_l += peak_length total_p += 1 - peak_content = [ ( ts, te ), ] + peak_content = [(ts, te),] lastp = te if peak_content: - peak_length = peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] - if peak_length >= min_length: # if the peak is too small, reject it - total_l += peak_length + peak_length = peak_content[-1][1] - peak_content[0][0] + # if the peak is too small, reject it + if peak_length >= min_length: + total_l += peak_length total_p += 1 - cutoff_lpeaks[ n ] += total_l - cutoff_npeaks[ n ] += total_p - + cutoff_lpeaks[n] += total_l + cutoff_npeaks[n] += total_p + # prepare the returnning text ret_list = ["score\tnpeaks\tlpeaks\tavelpeak\n"] - for n in range( len( cutoff_list )-1, -1, -1 ): - cutoff = cutoff_list[ n ] - if cutoff_npeaks[ n ] > 0: - ret_list.append("%.2f\t%d\t%d\t%.2f\n" % ( cutoff, cutoff_npeaks[ n ], \ - cutoff_lpeaks[ n ], \ - cutoff_lpeaks[ n ]/cutoff_npeaks[ n ] )) + for n in range(len(cutoff_list)-1, -1, -1): + cutoff = cutoff_list[n] + if cutoff_npeaks[n] > 0: + ret_list.append("%.2f\t%d\t%d\t%.2f\n" % (cutoff, + cutoff_npeaks[n], + cutoff_lpeaks[n], + cutoff_lpeaks[n]/cutoff_npeaks[n])) ret = ''.join(ret_list) return ret -cdef np.ndarray calculate_elbows( np.ndarray values, float32_t threshold=0.01): - # although this function is supposed to find elbow pts for cutoff analysis, - # however, in reality, it barely works... - cdef: - np.ndarray deltas, slopes, delta_slopes, elbows - np.float32_t avg_delta_slope - + +@cython.cfunc +def calculate_elbows(values: cnp.ndarray, + threshold: cython.float = 0.01) -> cnp.ndarray: + # although this function is supposed to find elbow pts for cutoff + # analysis, however, in reality, it barely works... + deltas: cnp.ndarray + slopes: cnp.ndarray + delta_slopes: cnp.ndarray + elbows: cnp.ndarray + avg_delta_slope: cython.float + # Calculate the difference between each point and the first point deltas = values - values[0] - # Calculate the slope between each point and the last point slopes = deltas / (values[-1] - values[0]) - # Calculate the change in slope delta_slopes = np.diff(slopes) - # Calculate the average change in slope avg_delta_slope = np.mean(delta_slopes) - - # Find all points where the change in slope is significantly larger than the average + # Find all points where the change in slope is significantly + # larger than the average elbows = np.where(delta_slopes > avg_delta_slope + threshold)[0] - return elbows diff --git a/MACS3/Signal/HMMR_Signal_Processing.py b/MACS3/Signal/HMMR_Signal_Processing.py index b6185663..a6c4a8cb 100644 --- a/MACS3/Signal/HMMR_Signal_Processing.py +++ b/MACS3/Signal/HMMR_Signal_Processing.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-04 10:25:29 Tao Liu> +# Time-stamp: <2024-10-14 17:04:27 Tao Liu> """Module description: @@ -137,7 +137,7 @@ def generate_digested_signals(petrack, weight_mapping: list) -> list: certain_signals = ret_digested_signals[i] bdg = bedGraphTrackI() for chrom in sorted(certain_signals.keys()): - bdg.add_chrom_data_hmmr_PV(chrom, certain_signals[chrom]) + bdg.add_chrom_data_PV(chrom, certain_signals[chrom]) ret_bedgraphs.append(bdg) return ret_bedgraphs diff --git a/MACS3/Signal/PairedEndTrack.py b/MACS3/Signal/PairedEndTrack.py index ed056a9b..d8a7a85f 100644 --- a/MACS3/Signal/PairedEndTrack.py +++ b/MACS3/Signal/PairedEndTrack.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-14 13:15:32 Tao Liu> +# Time-stamp: <2024-10-14 21:13:35 Tao Liu> """Module for filter duplicate tags from paired-end data @@ -24,7 +24,8 @@ over_two_pv_array, se_all_in_one_pileup) from MACS3.Signal.BedGraph import bedGraphTrackI -from MACS3.Signal.PileupV2 import pileup_from_LR_hmmratac +from MACS3.Signal.PileupV2 import (pileup_from_LR_hmmratac, + pileup_from_LRC) # ------------------------------------ # Other modules # ------------------------------------ @@ -56,7 +57,7 @@ class PETrackI: locations = cython.declare(dict, visibility="public") size = cython.declare(dict, visibility="public") buf_size = cython.declare(dict, visibility="public") - sorted = cython.declare(bool, visibility="public") + is_sorted = cython.declare(bool, visibility="public") total = cython.declare(cython.ulong, visibility="public") annotation = cython.declare(str, visibility="public") # rlengths: reference chromosome lengths dictionary @@ -64,27 +65,28 @@ class PETrackI: buffer_size = cython.declare(cython.long, visibility="public") length = cython.declare(cython.long, visibility="public") average_template_length = cython.declare(cython.float, visibility="public") - destroyed: bool + is_destroyed: bool def __init__(self, anno: str = "", buffer_size: cython.long = 100000): """fw is the fixed-width for all locations. """ # dictionary with chrname as key, nparray with - # [('l','int32'),('r','int32')] as value + # [('l','i4'),('r','i4')] as value self.locations = {} # dictionary with chrname as key, size of the above nparray as value # size is to remember the size of the fragments added to this chromosome self.size = {} # dictionary with chrname as key, size of the above nparray as value self.buf_size = {} - self.sorted = False + self.is_sorted = False self.total = 0 # total fragments self.annotation = anno # need to be figured out self.rlengths = {} self.buffer_size = buffer_size self.length = 0 self.average_template_length = 0.0 + self.is_destroyed = False @cython.ccall def add_loc(self, chromosome: bytes, @@ -130,7 +132,7 @@ def destroy(self): refcheck=False) self.locations[chromosome] = None self.locations.pop(chromosome) - self.destroyed = True + self.is_destroyed = True return @cython.ccall @@ -187,7 +189,7 @@ def finalize(self): self.locations[c].sort(order=['l', 'r']) self.total += self.size[c] - self.sorted = True + self.is_sorted = True self.average_template_length = cython.cast(cython.float, self.length) / self.total return @@ -219,7 +221,7 @@ def sort(self): for c in chrnames: self.locations[c].sort(order=['l', 'r']) # sort by the leftmost location - self.sorted = True + self.is_sorted = True return @cython.ccall @@ -286,7 +288,7 @@ def filter_dup(self, maxnum: cython.int = -1): if maxnum < 0: return # condition to return if not filtering - if not self.sorted: + if not self.is_sorted: self.sort() self.total = 0 @@ -539,7 +541,7 @@ def pileup_a_chromosome_c(self, three_shift: cython.long rlength: cython.long = self.get_rlengths()[chrom] - if not self.sorted: + if not self.is_sorted: self.sort() assert len(ds) == len(scale_factor_s), "ds and scale_factor_s must have the same length!" @@ -619,10 +621,12 @@ def pileup_bdg(self, def pileup_bdg_hmmr(self, mapping: list, baseline_value: cython.float = 0.0) -> list: - """pileup all chromosomes, and return a list of four - bedGraphTrackI objects: short, mono, di, and tri nucleosomal - signals. + """pileup all chromosomes, and return a list of four p-v + ndarray objects: short, mono, di, and tri nucleosomal signals. + This is specifically designed for hmmratac + HMM_SignalProcessing.py. Not a general function. + The idea is that for each fragment length, we generate four bdg using four weights from four distributions. Then we add all sets of four bdgs together. @@ -650,40 +654,66 @@ def pileup_bdg_hmmr(self, @cython.cclass -class PETrackII(PETrackI): - """Documentation for PEtrac +class PETrackII: + """Paired-end track class for fragment files from single-cell + ATAC-seq experiments. We will store data of start, end, barcode, + and count from the fragment files. + + * I choose not to inherit PETrackI because there would be a lot of + differences. """ + locations = cython.declare(dict, visibility="public") # add another dict for storing barcode for each fragment we will # first convert barcode into integer and remember them in the - # barcode_dict, which will map key:barcode -> value:integer + # barcode_dict, which will map the rule to numerize + # key:bytes as value:4bytes_integer barcodes = cython.declare(dict, visibility="public") barcode_dict = cython.declare(dict, visibility="public") - # the last number for barcodes, used to map barcode into integer + # the last number for barcodes, used to map barcode to integer barcode_last_n: cython.int - def __init__(self): - super().__init__() + size = cython.declare(dict, visibility="public") + buf_size = cython.declare(dict, visibility="public") + is_sorted = cython.declare(bool, visibility="public") + total = cython.declare(cython.ulong, visibility="public") + annotation = cython.declare(str, visibility="public") + # rlengths: reference chromosome lengths dictionary + rlengths = cython.declare(dict, visibility="public") + buffer_size = cython.declare(cython.long, visibility="public") + length = cython.declare(cython.long, visibility="public") + average_template_length = cython.declare(cython.float, visibility="public") + is_destroyed: bool + + def __init__(self, anno: str = "", buffer_size: cython.long = 100000): + # dictionary with chrname as key, nparray with + # [('l','i4'),('r','i4'),('c','u1')] as value + self.locations = {} + # dictionary with chrname as key, size of the above nparray as value + # size is to remember the size of the fragments added to this chromosome + self.size = {} + # dictionary with chrname as key, size of the above nparray as value + self.buf_size = {} + self.is_sorted = False + self.total = 0 # total fragments + self.annotation = anno # need to be figured out + self.rlengths = {} + self.buffer_size = buffer_size + self.length = 0 + self.average_template_length = 0.0 + self.is_destroyed = False + self.barcodes = {} self.barcode_dict = {} self.barcode_last_n = 0 @cython.ccall - def add_loc(self, chromosome: bytes, - start: cython.int, end: cython.int): - raise NotImplementedError("This function is disabled PETrackII") - - @cython.ccall - def filter_dup(self, maxnum: cython.int = -1): - raise NotImplementedError("This function is disabled PETrackII") - - @cython.ccall - def add_frag(self, - chromosome: bytes, - start: cython.int, - end: cython.int, - barcode: bytes, - count: cython.uchar): + def add_loc(self, + chromosome: bytes, + start: cython.int, + end: cython.int, + barcode: bytes, + count: cython.uchar): """Add a location to the list according to the sequence name. chromosome: mostly the chromosome name @@ -747,9 +777,42 @@ def destroy(self): self.barcodes[chromosome] = None self.barcodes.pop(chromosome) self.barcode_dict = {} - self.destroyed = True + self.is_destroyed = True return + @cython.ccall + def set_rlengths(self, rlengths: dict) -> bool: + """Set reference chromosome lengths dictionary. + + Only the chromosome existing in this petrack object will be updated. + + If a chromosome in this petrack is not covered by given + rlengths, and it has no associated length, it will be set as + maximum integer. + """ + valid_chroms: set + missed_chroms: set + chrom: bytes + + valid_chroms = set(self.locations.keys()).intersection(rlengths.keys()) + for chrom in sorted(valid_chroms): + self.rlengths[chrom] = rlengths[chrom] + missed_chroms = set(self.locations.keys()).difference(rlengths.keys()) + for chrom in sorted(missed_chroms): + self.rlengths[chrom] = INT_MAX + return True + + @cython.ccall + def get_rlengths(self) -> dict: + """Get reference chromosome lengths dictionary. + + If self.rlengths is empty, create a new dict where the length of + chromosome will be set as the maximum integer. + """ + if not self.rlengths: + self.rlengths = dict([(k, INT_MAX) for k in self.locations.keys()]) + return self.rlengths + @cython.ccall def finalize(self): """Resize np arrays for 5' positions and sort them in place @@ -774,11 +837,29 @@ def finalize(self): self.barcodes[c] = self.barcodes[c][indices] self.total += np.sum(self.locations[c]['c']) # self.size[c] - self.sorted = True + self.is_sorted = True self.average_template_length = cython.cast(cython.float, self.length) / self.total return + @cython.ccall + def get_locations_by_chr(self, chromosome: bytes): + """Return a np array of left/right/count for certain chromosome. + + """ + if chromosome in self.locations: + return self.locations[chromosome] + else: + raise Exception("No such chromosome name (%s) in TrackI object!\n" % (chromosome)) + + @cython.ccall + def get_chr_names(self) -> set: + """Return all the chromosome names in this track object as a + python set. + + """ + return set(self.locations.keys()) + @cython.ccall def sort(self): """Naive sorting for locations. @@ -794,7 +875,7 @@ def sort(self): indices = np.argsort(self.locations[c], order=['l', 'r']) self.locations[c] = self.locations[c][indices] self.barcodes[c] = self.barcodes[c][indices] - self.sorted = True + self.is_sorted = True return @cython.ccall @@ -849,7 +930,7 @@ def subset(self, selected_barcodes: set): """Make a subset of PETrackII with only the given barcodes. Note: the selected_barcodes is a set of barcodes in python - bytes. For example, b"ATCTGCTAGTCTACAT" + bytes. For example, {b"ATCTGCTAGTCTACAT", b"ATTCTCGATGCAGTCA"} """ indices: cnp.ndarray @@ -875,23 +956,20 @@ def subset(self, selected_barcodes: set): # pass some values from self to ret ret.annotation = self.annotation - ret.sorted = self.sorted + ret.is_sorted = self.is_sorted ret.rlengths = self.rlengths ret.buffer_size = self.buffer_size ret.total = 0 ret.length = 0 ret.average_template_length = 0 - ret.destroyed = True + ret.is_destroyed = True for chromosome in sorted(chrs): - indices = np.where(np.isin(self.barcodes[chromosome], list(selected_barcodes_n)))[0] - print(chrs, indices) + indices = np.where(np.isin(self.barcodes[chromosome], + list(selected_barcodes_n)))[0] ret.barcodes[chromosome] = self.barcodes[chromosome][indices] - print(ret.barcodes) ret.locations[chromosome] = self.locations[chromosome][indices] - print(ret.locations) ret.size[chromosome] = len(ret.locations[chromosome]) - print(ret.size) ret.buf_size[chromosome] = ret.size[chromosome] ret.total += np.sum(ret.locations[chromosome]['c']) ret.length += np.sum((ret.locations[chromosome]['r'] - @@ -900,58 +978,24 @@ def subset(self, selected_barcodes: set): ret.average_template_length = ret.length / ret.total return ret - @cython.ccall - def sample_percent(self, percent: cython.float, seed: cython.int = -1): - raise NotImplementedError("This function is disabled PETrackII") - - @cython.ccall - def sample_percent_copy(self, percent: cython.float, seed: cython.int = -1): - raise NotImplementedError("This function is disabled PETrackII") - - @cython.ccall - def sample_num(self, samplesize: cython.ulong, seed: cython.int = -1): - raise NotImplementedError("This function is disabled PETrackII") - - @cython.ccall - def sample_num_copy(self, samplesize: cython.ulong, seed: cython.int = -1): - raise NotImplementedError("This function is disabled PETrackII") - @cython.ccall def pileup_a_chromosome(self, - chrom: bytes, - scale_factor_s: list, - baseline_value: cython.float = 0.0) -> list: - """pileup a certain chromosome, return [p,v] (end position and - pileup value) list. - - scale_factor_s : linearly scale the pileup value applied to - each d in ds. The list should have the same - length as ds. - - baseline_value : a value to be filled for missing values, and - will be the minimum pileup. - + chrom: bytes) -> cnp.ndarray: + """pileup a certain chromosome, return p-v ndarray (end + position and pileup value). """ - tmp_pileup: list - prev_pileup: list - scale_factor: cython.float - - prev_pileup = None - - for i in range(len(scale_factor_s)): - scale_factor = scale_factor_s[i] + return pileup_from_LRC(self.locations[chrom]) - # Can't directly pass partial nparray there since that will mess up with pointer calculation. - tmp_pileup = quick_pileup(np.sort(self.locations[chrom]['l']), - np.sort(self.locations[chrom]['r']), - scale_factor, baseline_value) - - if prev_pileup: - prev_pileup = over_two_pv_array(prev_pileup, - tmp_pileup, - func="max") - else: - prev_pileup = tmp_pileup + @cython.ccall + def pileup_bdg(self): + """Pileup all chromosome and return a bdg object. + """ + bdg: bedGraphTrackI + pv: cnp.ndarray - return prev_pileup + bdg = bedGraphTrackI() + for chrom in self.get_chr_names(): + pv = pileup_from_LRC(self.locations[chrom]) + bdg.add_chrom_data_PV(chrom, pv) + return bdg diff --git a/MACS3/Signal/Pileup.py b/MACS3/Signal/Pileup.py index fcff7ce7..dbb674fe 100644 --- a/MACS3/Signal/Pileup.py +++ b/MACS3/Signal/Pileup.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-14 13:42:18 Tao Liu> +# Time-stamp: <2024-10-14 20:08:53 Tao Liu> """Module Description: For pileup functions. @@ -24,7 +24,6 @@ # ------------------------------------ import numpy as np import cython.cimports.numpy as cnp -from cython.cimports.numpy import int32_t, float32_t from cython.cimports.cpython import bool # ------------------------------------ @@ -60,7 +59,7 @@ def fix_coordinates(poss: cnp.ndarray, rlength: cython.int) -> cnp.ndarray: """Fix the coordinates. """ i: cython.long - ptr: cython.pointer(int32_t) = cython.cast(cython.pointer(int32_t), poss.data) # pointer + ptr: cython.pointer(cython.int) = cython.cast(cython.pointer(cython.int), poss.data) # pointer #ptr = poss.data @@ -276,10 +275,10 @@ def se_all_in_one_pileup(plus_tags: cnp.ndarray, ret_v: cnp.ndarray # pointers are used for numpy arrays - start_poss_ptr: cython.pointer(int32_t) - end_poss_ptr: cython.pointer(int32_t) - ret_p_ptr: cython.pointer(int32_t) - ret_v_ptr: cython.pointer(float32_t) + start_poss_ptr: cython.pointer(cython.int) + end_poss_ptr: cython.pointer(cython.int) + ret_p_ptr: cython.pointer(cython.int) + ret_v_ptr: cython.pointer(cython.float) start_poss = np.concatenate((plus_tags-five_shift, minus_tags-three_shift)) end_poss = np.concatenate((plus_tags+three_shift, minus_tags+five_shift)) @@ -294,14 +293,14 @@ def se_all_in_one_pileup(plus_tags: cnp.ndarray, lx = start_poss.shape[0] - start_poss_ptr = cython.cast(cython.pointer(int32_t), start_poss.data) # start_poss.data - end_poss_ptr = cython.cast(cython.pointer(int32_t), end_poss.data) # end_poss.data + start_poss_ptr = cython.cast(cython.pointer(cython.int), start_poss.data) # start_poss.data + end_poss_ptr = cython.cast(cython.pointer(cython.int), end_poss.data) # end_poss.data ret_p = np.zeros(2 * lx, dtype="i4") ret_v = np.zeros(2 * lx, dtype="f4") - ret_p_ptr = cython.cast(cython.pointer(int32_t), ret_p.data) - ret_v_ptr = cython.cast(cython.pointer(float32_t), ret_v.data) + ret_p_ptr = cython.cast(cython.pointer(cython.int), ret_p.data) + ret_v_ptr = cython.cast(cython.pointer(cython.float), ret_v.data) tmp = [ret_p, ret_v] # for (endpos,value) @@ -421,19 +420,19 @@ def quick_pileup(start_poss: cnp.ndarray, tmp: list # pointers are used for numpy arrays - start_poss_ptr: cython.pointer(int32_t) - end_poss_ptr: cython.pointer(int32_t) - ret_p_ptr: cython.pointer(int32_t) - ret_v_ptr: cython.pointer(float32_t) + start_poss_ptr: cython.pointer(cython.int) + end_poss_ptr: cython.pointer(cython.int) + ret_p_ptr: cython.pointer(cython.int) + ret_v_ptr: cython.pointer(cython.float) - start_poss_ptr = cython.cast(cython.pointer(int32_t), start_poss.data) # start_poss.data - end_poss_ptr = cython.cast(cython.pointer(int32_t), end_poss.data) # end_poss.data + start_poss_ptr = cython.cast(cython.pointer(cython.int), start_poss.data) # start_poss.data + end_poss_ptr = cython.cast(cython.pointer(cython.int), end_poss.data) # end_poss.data ret_p = np.zeros(l, dtype="i4") ret_v = np.zeros(l, dtype="f4") - ret_p_ptr = cython.cast(cython.pointer(int32_t), ret_p.data) - ret_v_ptr = cython.cast(cython.pointer(float32_t), ret_v.data) + ret_p_ptr = cython.cast(cython.pointer(cython.int), ret_p.data) + ret_v_ptr = cython.cast(cython.pointer(cython.float), ret_v.data) tmp = [ret_p, ret_v] # for (endpos,value) @@ -530,23 +529,23 @@ def naive_quick_pileup(sorted_poss: cnp.ndarray, extension: int) -> list: ret_v: cnp.ndarray # pointers are used for numpy arrays - start_poss_ptr: cython.pointer(int32_t) - end_poss_ptr: cython.pointer(int32_t) - ret_p_ptr: cython.pointer(int32_t) - ret_v_ptr: cython.pointer(float32_t) + start_poss_ptr: cython.pointer(cython.int) + end_poss_ptr: cython.pointer(cython.int) + ret_p_ptr: cython.pointer(cython.int) + ret_v_ptr: cython.pointer(cython.float) start_poss = sorted_poss - extension start_poss[start_poss < 0] = 0 end_poss = sorted_poss + extension - start_poss_ptr = cython.cast(cython.pointer(int32_t), start_poss.data) # start_poss.data - end_poss_ptr = cython.cast(cython.pointer(int32_t), end_poss.data) # end_poss.data + start_poss_ptr = cython.cast(cython.pointer(cython.int), start_poss.data) # start_poss.data + end_poss_ptr = cython.cast(cython.pointer(cython.int), end_poss.data) # end_poss.data ret_p = np.zeros(2*l, dtype="i4") ret_v = np.zeros(2*l, dtype="f4") - ret_p_ptr = cython.cast(cython.pointer(int32_t), ret_p.data) - ret_v_ptr = cython.cast(cython.pointer(float32_t), ret_v.data) + ret_p_ptr = cython.cast(cython.pointer(cython.int), ret_p.data) + ret_v_ptr = cython.cast(cython.pointer(cython.float), ret_v.data) if l == 0: raise Exception("length is 0") @@ -627,7 +626,7 @@ def over_two_pv_array(pv_array1: list, pv_array2: list, func: str = "max") -> li available operations are 'max', 'min', and 'mean' """ - #pre_p: cython.int + # pre_p: cython.int l1: cython.long l2: cython.long @@ -643,12 +642,12 @@ def over_two_pv_array(pv_array1: list, pv_array2: list, func: str = "max") -> li ret_v: cnp.ndarray # pointers are used for numpy arrays - a1_pos_ptr: cython.pointer(int32_t) - a2_pos_ptr: cython.pointer(int32_t) - ret_pos_ptr: cython.pointer(int32_t) - a1_v_ptr: cython.pointer(float32_t) - a2_v_ptr: cython.pointer(float32_t) - ret_v_ptr: cython.pointer(float32_t) + a1_pos_ptr: cython.pointer(cython.int) + a2_pos_ptr: cython.pointer(cython.int) + ret_pos_ptr: cython.pointer(cython.int) + a1_v_ptr: cython.pointer(cython.float) + a2_v_ptr: cython.pointer(cython.float) + ret_v_ptr: cython.pointer(cython.float) if func == "max": f = max @@ -661,15 +660,15 @@ def over_two_pv_array(pv_array1: list, pv_array2: list, func: str = "max") -> li [a1_pos, a1_v] = pv_array1 [a2_pos, a2_v] = pv_array2 - ret_pos = np.zeros(a1_pos.shape[0] + a2_pos.shape[0], dtype="int32") - ret_v = np.zeros(a1_pos.shape[0] + a2_pos.shape[0], dtype="float32") - - a1_pos_ptr = cython.cast(cython.pointer(int32_t), a1_pos.data) - a1_v_ptr = cython.cast(cython.pointer(float32_t), a1_v.data) - a2_pos_ptr = cython.cast(cython.pointer(int32_t), a2_pos.data) - a2_v_ptr = cython.cast(cython.pointer(float32_t), a2_v.data) - ret_pos_ptr = cython.cast(cython.pointer(int32_t), ret_pos.data) - ret_v_ptr = cython.cast(cython.pointer(float32_t), ret_v.data) + ret_pos = np.zeros(a1_pos.shape[0] + a2_pos.shape[0], dtype="i4") + ret_v = np.zeros(a1_pos.shape[0] + a2_pos.shape[0], dtype="f4") + + a1_pos_ptr = cython.cast(cython.pointer(cython.int), a1_pos.data) + a1_v_ptr = cython.cast(cython.pointer(cython.float), a1_v.data) + a2_pos_ptr = cython.cast(cython.pointer(cython.int), a2_pos.data) + a2_v_ptr = cython.cast(cython.pointer(cython.float), a2_v.data) + ret_pos_ptr = cython.cast(cython.pointer(cython.int), ret_pos.data) + ret_v_ptr = cython.cast(cython.pointer(cython.float), ret_v.data) l1 = a1_pos.shape[0] l2 = a2_pos.shape[0] diff --git a/MACS3/Signal/PileupV2.py b/MACS3/Signal/PileupV2.py index 62f065f4..299ecf6e 100644 --- a/MACS3/Signal/PileupV2.py +++ b/MACS3/Signal/PileupV2.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-04 23:59:48 Tao Liu> +# Time-stamp: <2024-10-14 21:19:00 Tao Liu> """Module Description: @@ -34,33 +34,19 @@ for i from 0 to 2N in PV_sorted: 1: z = z + v_i 2: e = p_i - 3: save the pileup from position s to e is z -- in bedGraph style is to only save (e, z) + 3: save the pileup from position s to e is z, + in bedGraph style is to only save (e, z) 4: s = e This code is free software; you can redistribute it and/or modify it under the terms of the BSD License (see the file LICENSE included with the distribution). - """ - -# ------------------------------------ -# python modules -# ------------------------------------ - -# ------------------------------------ -# MACS3 modules -# ------------------------------------ - -# ------------------------------------ -# Other modules # ------------------------------------ import numpy as np import cython import cython.cimports.numpy as cnp -from cython.cimports.numpy import int32_t, float32_t, uint64_t -# ------------------------------------ -# C lib # ------------------------------------ # from cython.cimports.libc.stdlib import malloc, free, qsort @@ -70,7 +56,7 @@ @cython.ccall -def mapping_function_always_1(L: int32_t, R: int32_t) -> float32_t: +def mapping_function_always_1(L: cython.int, R: cython.int) -> cython.float: # always return 1, useful while the weight is already 1, or in # case of simply piling up fragments for coverage. return 1.0 @@ -86,81 +72,6 @@ def clean_up_ndarray(x: cnp.ndarray): x.resize(0, refcheck=False) return -# ------------------------------------ -# public python functions -# ------------------------------------ - - -@cython.ccall -def pileup_from_LR_hmmratac(LR_array: cnp.ndarray, - mapping_dict: dict) -> cnp.ndarray: - # this function is specifically designed for piling up fragments - # for `hmmratac`. - # - # As for `hmmratac`, the weight depends on the length of the - # fragment, aka, value of R-L. Therefore, we need a mapping_dict - # for mapping length to weight. - l_LR: uint64_t - l_PV: uint64_t - i: uint64_t - L: int32_t - R: int32_t - PV: cnp.ndarray - pileup: cnp.ndarray - - l_LR = LR_array.shape[0] - l_PV = 2 * l_LR - PV = np.zeros(shape=l_PV, dtype=[('p', 'uint32'), ('v', 'float32')]) - for i in range(l_LR): - (L, R) = LR_array[i] - PV[i*2] = (L, mapping_dict[R - L]) - PV[i*2 + 1] = (R, -1 * mapping_dict[R - L]) - PV.sort(order='p') - pileup = pileup_PV(PV) - clean_up_ndarray(PV) - return pileup - - -@cython.ccall -def pileup_from_LR(LR_array: cnp.ndarray, - mapping_func=mapping_function_always_1) -> cnp.ndarray: - """This function will pile up the ndarray containing left and - right positions, which is typically from PETrackI object. It's - useful when generating the pileup of a single chromosome is - needed. - - User needs to provide a numpy array of left and right positions, - with dtype=[('l','int32'),('r','int32')]. User also needs to - provide a mapping function to map the left and right position to - certain weight. - - """ - PV_array: cnp.ndarray - pileup: cnp.ndarray - - PV_array = make_PV_from_LR(LR_array, mapping_func=mapping_func) - pileup = pileup_PV(PV_array) - clean_up_ndarray(PV_array) - return pileup - - -@cython.ccall -def pileup_from_PN(P_array: cnp.ndarray, N_array: cnp.ndarray, - extsize: cython.int) -> cnp.ndarray: - """This function will pile up the ndarray containing plus - (positive) and minus (negative) positions of all reads, which is - typically from FWTrackI object. It's useful when generating the - pileup of a single chromosome is needed. - - """ - PV_array: cnp.ndarray - pileup: cnp.ndarray - - PV_array = make_PV_from_PN(P_array, N_array, extsize) - pileup = pileup_PV(PV_array) - clean_up_ndarray(PV_array) - return pileup - @cython.cfunc def make_PV_from_LR(LR_array: cnp.ndarray, @@ -170,16 +81,16 @@ def make_PV_from_LR(LR_array: cnp.ndarray, `mapping_func( L, R )` or simply 1 if mapping_func is the default. LR array is an np.ndarray as with dtype - [('l','int32'),('r','int32')] with length of N + [('l','i4'),('r','i4')] with length of N PV array is an np.ndarray with - dtype=[('p','uint32'),('v','float32')] with length of 2N + dtype=[('p','u4'),('v','f4')] with length of 2N """ - l_LR: uint64_t - l_PV: uint64_t - i: uint64_t - L: int32_t - R: int32_t + l_LR: cython.ulong + l_PV: cython.ulong + i: cython.ulong + L: cython.int + R: cython.int PV: cnp.ndarray l_LR = LR_array.shape[0] @@ -193,6 +104,38 @@ def make_PV_from_LR(LR_array: cnp.ndarray, return PV +@cython.cfunc +def make_PV_from_LRC(LRC_array: cnp.ndarray, + mapping_func=mapping_function_always_1) -> cnp.ndarray: + """Make sorted PV array from a LR array for certain chromosome in a + PETrackII object. The V/weight will be assigned as + `mapping_func( L, R )` or simply 1 if mapping_func is the default. + + LRC array is an np.ndarray as with dtype + [('l','i4'),('r','i4'),('c','u1')] with length of N + + PV array is an np.ndarray with + dtype=[('p','u4'),('v','f4')] with length of 2N + """ + l_LRC: cython.ulong + l_PV: cython.ulong + i: cython.ulong + L: cython.int + R: cython.int + C: cython.uchar + PV: cnp.ndarray + + l_LRC = LRC_array.shape[0] + l_PV = 2 * l_LRC + PV = np.zeros(shape=l_PV, dtype=[('p', 'u4'), ('v', 'f4')]) + for i in range(l_LRC): + (L, R, C) = LRC_array[i] + PV[i*2] = (L, C*mapping_func(L, R)) + PV[i*2 + 1] = (R, -1.0 * C * mapping_func(L, R)) + PV.sort(order='p') + return PV + + @cython.cfunc def make_PV_from_PN(P_array: cnp.ndarray, N_array: cnp.ndarray, extsize: cython.int) -> cnp.ndarray: @@ -202,22 +145,22 @@ def make_PV_from_PN(P_array: cnp.ndarray, N_array: cnp.ndarray, in this case since all positions should be extended with a fixed 'extsize'. - P_array or N_array is an np.ndarray with dtype='int32' + P_array or N_array is an np.ndarray with dtype='i4' PV array is an np.ndarray with - dtype=[('p','uint32'),('v','float32')] with length of 2N + dtype=[('p','u4'),('v','f4')] with length of 2N """ - l_PN: uint64_t - l_PV: uint64_t - i: uint64_t - L: int32_t - R: int32_t + l_PN: cython.ulong + l_PV: cython.ulong + i: cython.ulong + L: cython.int + R: cython.int PV: cnp.ndarray l_PN = P_array.shape[0] assert l_PN == N_array.shape[0] l_PV = 4 * l_PN - PV = np.zeros(shape=l_PV, dtype=[('p', 'uint32'), ('v', 'float32')]) + PV = np.zeros(shape=l_PV, dtype=[('p', 'u4'), ('v', 'f4')]) for i in range(l_PN): L = P_array[i] R = L + extsize @@ -246,24 +189,29 @@ def pileup_PV(PV_array: cnp.ndarray) -> cnp.ndarray: save the pileup from position s to e is z -- in bedGraph style is to only save (e, z) s = e """ - z: float32_t - v: float32_t - pre_z: float32_t - s: uint64_t - e: uint64_t - i: uint64_t - c: uint64_t - pileup_PV: cnp.ndarray # this is in bedGraph style as in Pileup.pyx, p is the end of a region, and v is the pileup value + z: cython.float + v: cython.float + pre_z: cython.float + s: cython.ulong + e: cython.ulong + i: cython.ulong + c: cython.ulong + # this is in bedGraph style as in Pileup.pyx, p is the end of a + # region, and v is the pileup value. It's + pileup_PV: cnp.ndarray z = 0 pre_z = -10000 s = 0 - pileup_PV = np.zeros(shape=PV_array.shape[0], dtype=[('p', 'uint32'), ('v', 'float32')]) + pileup_PV = np.zeros(shape=PV_array.shape[0], dtype=[('p', 'u4'), + ('v', 'f4')]) c = 0 for i in range(PV_array.shape[0]): e = PV_array[i]['p'] v = PV_array[i]['v'] - if e != s: # make sure only to record the final value for the same position - if z == pre_z: # merge the p-v pair with the previous pair if the same v is found + # make sure only to record the final value for the same position + if e != s: + # merge the p-v pair with the previous pair if the same v is found + if z == pre_z: pileup_PV[c-1]['p'] = e else: pileup_PV[c] = (e, z) @@ -274,3 +222,102 @@ def pileup_PV(PV_array: cnp.ndarray) -> cnp.ndarray: pileup_PV.resize(c, refcheck=False) # assert z == 0 return pileup_PV + +# ------------------------------------ +# public python functions +# ------------------------------------ + + +@cython.ccall +def pileup_from_LR_hmmratac(LR_array: cnp.ndarray, + mapping_dict: dict) -> cnp.ndarray: + # this function is specifically designed for piling up fragments + # for `hmmratac`. + # + # As for `hmmratac`, the weight depends on the length of the + # fragment, aka, value of R-L. Therefore, we need a mapping_dict + # for mapping length to weight. + l_LR: cython.ulong + l_PV: cython.ulong + i: cython.ulong + L: cython.int + R: cython.int + PV: cnp.ndarray + pileup: cnp.ndarray + + l_LR = LR_array.shape[0] + l_PV = 2 * l_LR + PV = np.zeros(shape=l_PV, dtype=[('p', 'u4'), ('v', 'f4')]) + for i in range(l_LR): + (L, R) = LR_array[i] + PV[i*2] = (L, mapping_dict[R - L]) + PV[i*2 + 1] = (R, -1 * mapping_dict[R - L]) + PV.sort(order='p') + pileup = pileup_PV(PV) + clean_up_ndarray(PV) + return pileup + + +@cython.ccall +def pileup_from_LR(LR_array: cnp.ndarray, + mapping_func=mapping_function_always_1) -> cnp.ndarray: + """This function will pile up the ndarray containing left and + right positions, which is typically from PETrackI object. It's + useful when generating the pileup of a single chromosome is + needed. + + User needs to provide a numpy array of left and right positions, + with dtype=[('l','i4'),('r','i4')]. User also needs to + provide a mapping function to map the left and right position to + certain weight. + + """ + PV_array: cnp.ndarray + pileup: cnp.ndarray + + PV_array = make_PV_from_LR(LR_array, mapping_func=mapping_func) + pileup = pileup_PV(PV_array) + clean_up_ndarray(PV_array) + return pileup + + +@cython.ccall +def pileup_from_LRC(LRC_array: cnp.ndarray, + mapping_func=mapping_function_always_1) -> cnp.ndarray: + """This function will pile up the ndarray containing left and + right positions and the counts, which is typically from PETrackII + object. It's useful when generating the pileup of a single + chromosome is needed. + + User needs to provide a numpy array of left and right positions + and the counts, with + dtype=[('l','i4'),('r','i4'),('c','u1')]. User also needs to + provide a mapping function to map the left and right position to + certain weight. + + """ + PV_array: cnp.ndarray + pileup: cnp.ndarray + + PV_array = make_PV_from_LRC(LRC_array, mapping_func=mapping_func) + pileup = pileup_PV(PV_array) + clean_up_ndarray(PV_array) + return pileup + + +@cython.ccall +def pileup_from_PN(P_array: cnp.ndarray, N_array: cnp.ndarray, + extsize: cython.int) -> cnp.ndarray: + """This function will pile up the ndarray containing plus + (positive) and minus (negative) positions of all reads, which is + typically from FWTrackI object. It's useful when generating the + pileup of a single chromosome is needed. + + """ + PV_array: cnp.ndarray + pileup: cnp.ndarray + + PV_array = make_PV_from_PN(P_array, N_array, extsize) + pileup = pileup_PV(PV_array) + clean_up_ndarray(PV_array) + return pileup diff --git a/setup.py b/setup.py index f0dc85b6..cce3c579 100644 --- a/setup.py +++ b/setup.py @@ -124,7 +124,7 @@ def main(): include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.BedGraph", - ["MACS3/Signal/BedGraph.pyx"], + ["MACS3/Signal/BedGraph.py"], libraries=["m"], include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), diff --git a/test/test_HMMR_poisson.py b/test/test_HMMR_poisson.py index 58efdb76..a71f1f99 100644 --- a/test/test_HMMR_poisson.py +++ b/test/test_HMMR_poisson.py @@ -1,14 +1,10 @@ - import unittest -import pytest # from MACS3.Signal.HMMR_HMM import * import numpy as np import numpy.testing as npt -import numpy as np -import hmmlearn from hmmlearn.hmm import PoissonHMM -from sklearn import cluster -import json +# from sklearn import cluster +# import json # class hmmlearn.hmm.PoissonHMM(n_components=1, startprob_prior=1.0, transmat_prior=1.0, lambdas_prior=0.0, lambdas_weight=0.0,  # algorithm='viterbi', random_state=None, n_iter=10, tol=0.01, verbose=False, params='stl', init_params='stl', implementation='log') @@ -16,21 +12,28 @@ # means_prior=0, means_weight=0, covars_prior=0.01, covars_weight=1, algorithm='viterbi', random_state=None, n_iter=10, tol=0.01, verbose=False,  # params='stmc', init_params='stmc', implementation='log') -def hmm_training (training_data, training_data_lengths, n_states = 3, random_seed = 12345): + +def hmm_training(training_data, training_data_lengths, n_states=3, random_seed=12345): rs = np.random.RandomState(np.random.MT19937(np.random.SeedSequence(random_seed))) - hmm_model = PoissonHMM( n_components= n_states, random_state = rs, verbose = False ) - hmm_model = hmm_model.fit( training_data, training_data_lengths ) + hmm_model = PoissonHMM(n_components=n_states, random_state=rs, verbose=False) + hmm_model = hmm_model.fit(training_data, training_data_lengths) assert hmm_model.n_features == 4 return hmm_model -def hmm_predict( signals, lens, hmm_model ): - predictions = hmm_model.predict_proba( signals, lens ) + +def hmm_predict(signals, lens, hmm_model): + predictions = hmm_model.predict_proba(signals, lens) return predictions + class Test_HMM_train_poisson(unittest.TestCase): - def setUp( self ): - self.training_data = np.loadtxt("test/large_training_data.txt", delimiter="\t", dtype="float", usecols=(2,3,4,5)).astype(int).tolist() - self.training_data_lengths = np.loadtxt('test/large_training_lengths.txt', dtype="int").tolist() + def setUp(self): + self.training_data = np.loadtxt("test/large_training_data.txt", + delimiter="\t", + dtype="float", + usecols=(2, 3, 4, 5)).astype(int).tolist() + self.training_data_lengths = np.loadtxt('test/large_training_lengths.txt', + dtype="int").tolist() self.expected_converged = True self.not_expected_transmat = None self.n_features = 4 @@ -38,40 +41,49 @@ def setUp( self ): self.transmat = [[9.87606722e-01, 1.23932782e-02, 1.75299652e-11], [1.76603580e-02, 9.64232293e-01, 1.81073490e-02], [4.87992301e-14, 2.70319349e-02, 9.72968065e-01]] - self.lambdas = [[ 0.03809295, 0.62378578, 0.68739807, 0. ], - [ 0.23243362, 3.4420467, 4.256037, 0. ], - [ 2.58132377, 11.45924282, 8.13706237, 0. ]] + self.lambdas = [[0.03809295, 0.62378578, 0.68739807, 0.], + [0.23243362, 3.4420467, 4.256037, 0.], + [2.58132377, 11.45924282, 8.13706237, 0.]] # for prediction - self.prediction_data = np.loadtxt("test/small_prediction_data.txt", delimiter="\t", dtype="float", usecols=(2,3,4,5)).astype(int).tolist() - self.prediction_data_lengths = np.loadtxt('test/small_prediction_lengths.txt', dtype="int").tolist() - self.predictions = np.loadtxt('test/small_prediction_results_poisson.txt', delimiter="\t", dtype="float").tolist() + self.prediction_data = np.loadtxt("test/small_prediction_data.txt", + delimiter="\t", + dtype="float", + usecols=(2, 3, 4, 5)).astype(int).tolist() + self.prediction_data_lengths = np.loadtxt('test/small_prediction_lengths.txt', + dtype="int").tolist() + self.predictions = np.loadtxt('test/small_prediction_results_poisson.txt', + delimiter="\t", dtype="float").tolist() - def test_training( self ): + def test_training(self): # test hmm_training: - model = hmm_training(training_data = self.training_data, training_data_lengths = self.training_data_lengths, n_states = 3, random_seed = 12345) - print(model.startprob_) - print(model.transmat_) - print(model.lambdas_) - print(model.n_features) - self.assertEqual( model.monitor_.converged, self.expected_converged ) - self.assertNotEqual( model.transmat_.tolist(), self.not_expected_transmat ) - npt.assert_allclose( model.startprob_.tolist(), self.startprob ) + model = hmm_training(training_data=self.training_data, + training_data_lengths=self.training_data_lengths, + n_states=3, + random_seed=12345) + # print(model.startprob_) + # print(model.transmat_) + # print(model.lambdas_) + # print(model.n_features) + self.assertEqual(model.monitor_.converged, self.expected_converged) + self.assertNotEqual(model.transmat_.tolist(), self.not_expected_transmat) + npt.assert_allclose(model.startprob_.tolist(), self.startprob) npt.assert_allclose(model.transmat_, self.transmat) npt.assert_allclose(model.lambdas_, self.lambdas) npt.assert_allclose(model.n_features, self.n_features) - def test_predict( self ): + def test_predict(self): # test hmm_predict - hmm_model = PoissonHMM( n_components=3 ) + hmm_model = PoissonHMM(n_components=3) hmm_model.startprob_ = np.array(self.startprob) hmm_model.transmat_ = np.array(self.transmat) hmm_model.lambdas_ = np.array(self.lambdas) hmm_model.n_features = self.n_features - predictions = hmm_predict( self.prediction_data, self.prediction_data_lengths, hmm_model ) + predictions = hmm_predict(self.prediction_data, + self.prediction_data_lengths, + hmm_model) - # This is to write the prediction results into a file for 'correct' answer + # This is to write the prediction results into a file for 'correct' answer # with open("test/small_prediction_results_poisson.txt","w") as f: # for x,y,z in predictions: - # f.write( str(x)+"\t"+str(y)+"\t"+str(z)+"\n") - - npt.assert_allclose( predictions, self.predictions ) + # f.write(str(x)+"\t"+str(y)+"\t"+str(z)+"\n") + npt.assert_allclose(predictions, self.predictions) diff --git a/test/test_PairedEndTrack.py b/test/test_PairedEndTrack.py index c120f7c9..867623a0 100644 --- a/test/test_PairedEndTrack.py +++ b/test/test_PairedEndTrack.py @@ -1,14 +1,13 @@ #!/usr/bin/env python -# Time-stamp: <2024-10-11 16:20:17 Tao Liu> +# Time-stamp: <2024-10-14 21:55:05 Tao Liu> import unittest - from MACS3.Signal.PairedEndTrack import PETrackI, PETrackII +import numpy as np class Test_PETrackI(unittest.TestCase): def setUp(self): - self.input_regions = [(b"chrY", 0, 100), (b"chrY", 70, 270), (b"chrY", 70, 100), @@ -92,23 +91,39 @@ def setUp(self): (b"chrY", 50, 160, b"0w#AAACGAACAAGTAAGA", 2), (b"chrY", 100, 170, b"0w#AAACGAACAAGTAAGA", 3) ] + self.pileup_p = np.array([10, 50, 70, 80, 85, 100, 110, 160, 170, 180, 190], dtype="i4") + self.pileup_v = np.array([3.0, 4.0, 6.0, 9.0, 11.0, 15.0, 19.0, 18.0, 16.0, 10.0, 6.0], dtype="f4") + self.subset_pileup_p = np.array([10, 50, 70, 80, 85, 100, 110, 160, 170, 180, 190], dtype="i4") + self.subset_pileup_v = np.array([1.0, 2.0, 4.0, 6.0, 7.0, 8.0, 13.0, 12.0, 10.0, 5.0, 4.0], dtype="f4") self.t = sum([(x[2]-x[1]) * x[4] for x in self.input_regions]) def test_add_frag(self): pe = PETrackII() for (c, l, r, b, C) in self.input_regions: - pe.add_frag(c, l, r, b, C) + pe.add_loc(c, l, r, b, C) pe.finalize() # roughly check the numbers... self.assertEqual(pe.total, 22) self.assertEqual(pe.length, self.t) - def test_subset(self): - pe = PETrackII() - for (c, l, r, b, C) in self.input_regions: - pe.add_frag(c, l, r, b, C) - pe.finalize() - pe_subset = pe.subset(set([b'0w#AAACGAACAAGTAACA', b"0w#AAACGAACAAGTAAGA"])) + # subset + pe_subset = pe.subset({b'0w#AAACGAACAAGTAACA', b"0w#AAACGAACAAGTAAGA"}) # roughly check the numbers... self.assertEqual(pe_subset.total, 14) self.assertEqual(pe_subset.length, 1305) + + def test_pileup(self): + pe = PETrackII() + for (c, l, r, b, C) in self.input_regions: + pe.add_loc(c, l, r, b, C) + pe.finalize() + bdg = pe.pileup_bdg() + d = bdg.get_data_by_chr(b'chrY') # (p, v) of ndarray + np.testing.assert_array_equal(d[0], self.pileup_p) + np.testing.assert_array_equal(d[1], self.pileup_v) + + pe_subset = pe.subset({b'0w#AAACGAACAAGTAACA', b"0w#AAACGAACAAGTAAGA"}) + bdg = pe_subset.pileup_bdg() + d = bdg.get_data_by_chr(b'chrY') # (p, v) of ndarray + np.testing.assert_array_equal(d[0], self.subset_pileup_p) + np.testing.assert_array_equal(d[1], self.subset_pileup_v) diff --git a/test/test_PeakIO.py b/test/test_PeakIO.py index a7af5a52..0a543f8a 100644 --- a/test/test_PeakIO.py +++ b/test/test_PeakIO.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Time-stamp: <2022-09-14 13:33:37 Tao Liu> +# Time-stamp: <2024-10-14 21:32:21 Tao Liu> import unittest import sys @@ -49,7 +49,7 @@ def test_exclude(self): r1.exclude(r2) result = str(r1) expected = str(self.exclude2from1) - print( "result:\n",result ) - print( "expected:\n", expected ) + # print( "result:\n",result ) + # print( "expected:\n", expected ) self.assertEqual( result, expected ) diff --git a/test/test_Pileup.py b/test/test_Pileup.py index 6abefa48..02b9b198 100644 --- a/test/test_Pileup.py +++ b/test/test_Pileup.py @@ -155,7 +155,7 @@ def test_pileup_1(self): self.param_1["extension"] ) result = [] (p,v) = pileup - print(p, v) + # print(p, v) pnext = iter(p).__next__ vnext = iter(v).__next__ pre = 0 @@ -217,7 +217,7 @@ def test_max(self): pileup = over_two_pv_array ( self.pv1, self.pv2, func="max" ) result = [] (p,v) = pileup - print(p, v) + # print(p, v) pnext = iter(p).__next__ vnext = iter(v).__next__ pre = 0 @@ -233,7 +233,7 @@ def test_min(self): pileup = over_two_pv_array ( self.pv1, self.pv2, func="min" ) result = [] (p,v) = pileup - print(p, v) + # print(p, v) pnext = iter(p).__next__ vnext = iter(v).__next__ pre = 0 @@ -249,7 +249,7 @@ def test_mean(self): pileup = over_two_pv_array ( self.pv1, self.pv2, func="mean" ) result = [] (p,v) = pileup - print(p, v) + # print(p, v) pnext = iter(p).__next__ vnext = iter(v).__next__ pre = 0 From d4481d3617f66906d9d14a8c4ddc41af0928067e Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Tue, 15 Oct 2024 11:29:57 -0400 Subject: [PATCH 05/13] more rewriting on pyx -> py --- MACS3/Signal/BedGraph.py | 2 +- MACS3/Signal/PeakDetect.py | 412 ++++++++++++++ MACS3/Signal/PeakDetect.pyx | 396 -------------- MACS3/Signal/PeakModel.py | 513 ++++++++++++++++++ MACS3/Signal/PeakModel.pyx | 418 -------------- ...gnalProcessing.pyx => SignalProcessing.py} | 264 +++++---- setup.py | 6 +- test/test_PairedEndTrack.py | 7 +- 8 files changed, 1088 insertions(+), 930 deletions(-) create mode 100644 MACS3/Signal/PeakDetect.py delete mode 100644 MACS3/Signal/PeakDetect.pyx create mode 100644 MACS3/Signal/PeakModel.py delete mode 100644 MACS3/Signal/PeakModel.pyx rename MACS3/Signal/{SignalProcessing.pyx => SignalProcessing.py} (61%) diff --git a/MACS3/Signal/BedGraph.py b/MACS3/Signal/BedGraph.py index 2abc6175..b16881f9 100644 --- a/MACS3/Signal/BedGraph.py +++ b/MACS3/Signal/BedGraph.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-14 19:32:34 Tao Liu> +# Time-stamp: <2024-10-14 23:47:21 Tao Liu> """Module for BedGraph data class. diff --git a/MACS3/Signal/PeakDetect.py b/MACS3/Signal/PeakDetect.py new file mode 100644 index 00000000..cea6f442 --- /dev/null +++ b/MACS3/Signal/PeakDetect.py @@ -0,0 +1,412 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-15 10:38:40 Tao Liu> + +"""Module Description: Detect peaks, main module + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file LICENSE included with +the distribution). +""" +# ------------------------------------ +# Python modules +# ------------------------------------ +import cython + +# ------------------------------------ +# MACS3 modules +# ------------------------------------ +# from MACS3.Utilities.Constants import * +from MACS3.Signal.CallPeakUnit import CallerFromAlignments + + +@cython.cfunc +def subpeak_letters(i: cython.short) -> bytes: + if i < 26: + return chr(97+i).encode() + else: + return subpeak_letters(i // 26) + chr(97 + (i % 26)).encode() + + +class PeakDetect: + """Class to do the peak calling. + + e.g + >>> from MACS3.cPeakDetect import cPeakDetect + >>> pd = PeakDetect(treat=treatdata, control=controldata, pvalue=pvalue_cutoff, d=100, gsize=3000000000) + >>> pd.call_peaks() + """ + def __init__(self, + opt=None, + treat=None, + control=None, + d=None, + maxgap=None, + minlen=None, + slocal=None, + llocal=None): + """Initialize the PeakDetect object. + + """ + self.opt = opt + self.info = opt.info + self.debug = opt.debug + self.warn = opt.warn + + self.treat = treat + self.control = control + self.ratio_treat2control = None + self.peaks = None + self.final_peaks = None + self.PE_MODE = opt.PE_MODE + self.scoretrack = None + + # self.femax = opt.femax + # self.femin = opt.femin + # self.festep = opt.festep + + self.log_pvalue = opt.log_pvalue # -log10pvalue + self.log_qvalue = opt.log_qvalue # -log10qvalue + if d is not None: + self.d = d + else: + self.d = self.opt.d + + if opt.maxgap: + self.maxgap = opt.maxgap + else: + self.maxgap = opt.tsize + + if opt.minlen: + self.minlen = opt.minlen + else: + self.minlen = self.d + + self.end_shift = self.opt.shift + self.gsize = opt.gsize + + self.nolambda = opt.nolambda + + if slocal is not None: + self.sregion = slocal + else: + self.sregion = opt.smalllocal + + if llocal is not None: + self.lregion = llocal + else: + self.lregion = opt.largelocal + + if (self.nolambda): + self.info("#3 !!!! DYNAMIC LAMBDA IS DISABLED !!!!") + # self.diag = opt.diag + # self.save_score = opt.store_score + # self.zwig_tr = opt.zwig_tr + # self.zwig_ctl= opt.zwig_ctl + + def call_peaks(self): + """Call peaks function. + + Scan the whole genome for peaks. RESULTS WILL BE SAVED IN + self.final_peaks and self.final_negative_peaks. + """ + if self.control: # w/ control + # if self.opt.broad: + # (self.peaks,self.broadpeaks) = self.__call_peaks_w_control() + # else: + self.peaks = self.__call_peaks_w_control() + else: # w/o control + # if self.opt.broad: + # (self.peaks,self.broadpeaks) = self.__call_peaks_wo_control() + # else: + self.peaks = self.__call_peaks_wo_control() + return self.peaks + + def __call_peaks_w_control(self): + """To call peaks with control data. + + A peak info type is a: dictionary + + key value: chromosome + + items: (peak start,peak end, peak length, peak summit, peak + height, number of tags in peak region, peak pvalue, peak + fold_enrichment) <-- tuple type + + While calculating pvalue: + + First, t and c will be adjusted by the ratio between total + reads in treatment and total reads in control, depending on + --to-small option. + + Then, t and c will be multiplied by the smallest peak size -- + self.d. + + Finally, a poisson CDF is applied to calculate one-side pvalue + for enrichment. + """ + lambda_bg: cython.float + treat_scale: cython.float + d: cython.float + ctrl_scale_s: list + ctrl_d_s: list + control_total: cython.long + # approx sum of treatment pileup values + treat_sum: cython.long + # approx sum of control pileup values + control_sum: cython.long + + if self.PE_MODE: + d = self.treat.average_template_length + # in PE mode, entire fragment is counted as 1 in treatment + # whereas both ends of fragment are counted in + # control/input. + control_total = self.control.total * 2 + treat_sum = self.treat.length + control_sum = control_total * self.treat.average_template_length + self.ratio_treat2control = float(treat_sum)/control_sum + else: + d = self.d + control_total = self.control.total + treat_sum = self.treat.total * self.d + control_sum = self.control.total * self.d + self.ratio_treat2control = float(treat_sum)/control_sum + + if self.opt.ratio != 1.0: + self.ratio_treat2control = self.opt.ratio + + if self.opt.tocontrol: + # if MACS decides to scale treatment to control data + # because treatment is bigger + lambda_bg = float(control_sum) / self.gsize + treat_scale = 1/self.ratio_treat2control + else: + # if MACS decides to scale control to treatment because + # control sample is bigger + lambda_bg = float(treat_sum) / self.gsize + treat_scale = 1.0 + + # prepare d_s for control data + if self.sregion: + assert self.d <= self.sregion, f"{self.sregion:} can't be smaller than {self.d:}!" + if self.lregion: + assert self.d <= self.lregion, f"{self.lregion:} can't be smaller than {self.d:}!" + assert self.sregion <= self.lregion, f"{self.lregion:} can't be smaller than {self.sregion:}!" + + # Now prepare a list of extension sizes + ctrl_d_s = [self.d] # note, d doesn't make sense in PE mode. + # And a list of scaling factors for control + ctrl_scale_s = [] + + # d + if not self.opt.tocontrol: + # if user wants to scale everything to ChIP data + tmp_v = self.ratio_treat2control + else: + tmp_v = 1.0 + ctrl_scale_s.append(tmp_v) + + # slocal size local + if self.sregion: + ctrl_d_s.append(self.sregion) + if not self.opt.tocontrol: + # if user want to scale everything to ChIP data + tmp_v = float(self.d)/self.sregion*self.ratio_treat2control + else: + tmp_v = float(self.d)/self.sregion + ctrl_scale_s.append(tmp_v) + + # llocal size local + if self.lregion and self.lregion > self.sregion: + ctrl_d_s.append(self.lregion) + if not self.opt.tocontrol: + # if user want to scale everything to ChIP data + tmp_v = float(self.d)/self.lregion*self.ratio_treat2control + else: + tmp_v = float(self.d)/self.lregion + ctrl_scale_s.append(tmp_v) + + # if self.PE_MODE: # first d/scale are useless in PE mode + # ctrl_d_s = ctrl_d_s[1:] + # ctrl_scale_s = ctrl_scale_s[1:] + # print ctrl_d_s + # print ctrl_scale_s + if self.nolambda: + ctrl_d_s = [] + ctrl_scale_s = [] + + scorecalculator = CallerFromAlignments(self.treat, self.control, + d=d, ctrl_d_s=ctrl_d_s, + treat_scaling_factor=treat_scale, + ctrl_scaling_factor_s=ctrl_scale_s, + end_shift=self.end_shift, + lambda_bg=lambda_bg, + save_bedGraph=self.opt.store_bdg, + bedGraph_filename_prefix=self.opt.name, + bedGraph_treat_filename=self.opt.bdg_treat, + bedGraph_control_filename=self.opt.bdg_control, + save_SPMR=self.opt.do_SPMR, + cutoff_analysis_filename=self.opt.cutoff_analysis_file ) + + if self.opt.trackline: + scorecalculator.enable_trackline() + + # call peaks + call_summits = self.opt.call_summits + if call_summits: + self.info("#3 Going to call summits inside each peak ...") + + if self.log_pvalue is not None: + if self.opt.broad: + self.info("#3 Call broad peaks with given level1 -log10pvalue cutoff and level2: %.5f, %.5f..." % + (self.log_pvalue, self.opt.log_broadcutoff)) + peaks = scorecalculator.call_broadpeaks(['p',], + lvl1_cutoff_s=[self.log_pvalue,], + lvl2_cutoff_s=[self.opt.log_broadcutoff,], + min_length=self.minlen, + lvl1_max_gap=self.maxgap, + lvl2_max_gap=self.maxgap*4, + cutoff_analysis=self.opt.cutoff_analysis) + else: + self.info("#3 Call peaks with given -log10pvalue cutoff: %.5f ..." % self.log_pvalue) + peaks = scorecalculator.call_peaks(['p',], [self.log_pvalue,], + min_length=self.minlen, + max_gap=self.maxgap, + call_summits=call_summits, + cutoff_analysis=self.opt.cutoff_analysis) + elif self.log_qvalue is not None: + if self.opt.broad: + self.info("#3 Call broad peaks with given level1 -log10qvalue cutoff and level2: %f, %f..." % + (self.log_qvalue, self.opt.log_broadcutoff)) + peaks = scorecalculator.call_broadpeaks(['q',], + lvl1_cutoff_s=[self.log_qvalue,], + lvl2_cutoff_s=[self.opt.log_broadcutoff,], + min_length=self.minlen, + lvl1_max_gap=self.maxgap, + lvl2_max_gap=self.maxgap*4, + cutoff_analysis=self.opt.cutoff_analysis) + else: + peaks = scorecalculator.call_peaks(['q',], [self.log_qvalue,], + min_length=self.minlen, + max_gap=self.maxgap, + call_summits=call_summits, + cutoff_analysis=self.opt.cutoff_analysis) + scorecalculator.destroy() + return peaks + + def __call_peaks_wo_control(self): + """To call peaks without control data. + + A peak info type is a: dictionary + + key value: chromosome + + items: (peak start,peak end, peak length, peak summit, peak + height, number of tags in peak region, peak pvalue, peak + fold_enrichment) <-- tuple type + + While calculating pvalue: + + First, t and c will be adjusted by the ratio between total + reads in treatment and total reads in control, depending on + --to-small option. + + Then, t and c will be multiplied by the smallest peak size -- + self.d. + + Finally, a poisson CDF is applied to calculate one-side pvalue + for enrichment. + """ + lambda_bg: cython.float + treat_scale: cython.float = 1 + d: cython.float + ctrl_scale_s: list + ctrl_d_s: list + + if self.PE_MODE: + d = 0 + else: + d = self.d + treat_length = self.treat.length + treat_total = self.treat.total + + # global lambda + if self.PE_MODE: + # this an estimator, we should maybe test it for accuracy? + lambda_bg = treat_length / self.gsize + else: + lambda_bg = float(d) * treat_total / self.gsize + treat_scale = 1.0 + + # slocal and d-size local bias are not calculated! + # nothing done here. should this match w control?? + + if not self.nolambda: + if self.PE_MODE: + ctrl_scale_s = [float(treat_length) / (self.lregion*treat_total*2),] + else: + ctrl_scale_s = [float(self.d) / self.lregion,] + ctrl_d_s = [self.lregion,] + else: + ctrl_scale_s = [] + ctrl_d_s = [] + + scorecalculator = CallerFromAlignments(self.treat, None, + d=d, + ctrl_d_s=ctrl_d_s, + treat_scaling_factor=treat_scale, + ctrl_scaling_factor_s=ctrl_scale_s, + end_shift=self.end_shift, + lambda_bg=lambda_bg, + save_bedGraph=self.opt.store_bdg, + bedGraph_filename_prefix=self.opt.name, + bedGraph_treat_filename=self.opt.bdg_treat, + bedGraph_control_filename=self.opt.bdg_control, + save_SPMR=self.opt.do_SPMR, + cutoff_analysis_filename=self.opt.cutoff_analysis_file) + + if self.opt.trackline: + scorecalculator.enable_trackline() + + # call peaks + call_summits = self.opt.call_summits + if call_summits: + self.info("#3 Going to call summits inside each peak ...") + + if self.log_pvalue is not None: + if self.opt.broad: + self.info("#3 Call broad peaks with given level1 -log10pvalue cutoff and level2: %.5f, %.5f..." % + (self.log_pvalue, self.opt.log_broadcutoff)) + peaks = scorecalculator.call_broadpeaks(['p',], + lvl1_cutoff_s=[self.log_pvalue,], + lvl2_cutoff_s=[self.opt.log_broadcutoff,], + min_length=self.minlen, + lvl1_max_gap=self.maxgap, + lvl2_max_gap=self.maxgap*4, + cutoff_analysis=self.opt.cutoff_analysis) + else: + self.info("#3 Call peaks with given -log10pvalue cutoff: %.5f ..." % self.log_pvalue) + peaks = scorecalculator.call_peaks(['p',], [self.log_pvalue,], + min_length=self.minlen, + max_gap=self.maxgap, + call_summits=call_summits, + cutoff_analysis=self.opt.cutoff_analysis) + elif self.log_qvalue is not None: + if self.opt.broad: + self.info("#3 Call broad peaks with given level1 -log10qvalue cutoff and level2: %f, %f..." % + (self.log_qvalue, self.opt.log_broadcutoff)) + peaks = scorecalculator.call_broadpeaks(['q',], + lvl1_cutoff_s=[self.log_qvalue,], + lvl2_cutoff_s=[self.opt.log_broadcutoff,], + min_length=self.minlen, + lvl1_max_gap=self.maxgap, + lvl2_max_gap=self.maxgap*4, + cutoff_analysis=self.opt.cutoff_analysis) + else: + peaks = scorecalculator.call_peaks(['q',], [self.log_qvalue,], + min_length=self.minlen, + max_gap=self.maxgap, + call_summits=call_summits, + cutoff_analysis=self.opt.cutoff_analysis) + scorecalculator.destroy() + return peaks diff --git a/MACS3/Signal/PeakDetect.pyx b/MACS3/Signal/PeakDetect.pyx deleted file mode 100644 index 64372fe6..00000000 --- a/MACS3/Signal/PeakDetect.pyx +++ /dev/null @@ -1,396 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2020-11-24 17:39:12 Tao Liu> - -"""Module Description: Detect peaks, main module - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file LICENSE included with -the distribution). -""" -# ------------------------------------ -# Python modules -# ------------------------------------ -from itertools import groupby -from operator import itemgetter -import io -import gc # use garbage collectior - -# ------------------------------------ -# MACS3 modules -# ------------------------------------ -from MACS3.IO.PeakIO import PeakIO -from MACS3.IO.BedGraphIO import bedGraphIO -from MACS3.Utilities.Constants import * -from MACS3.Signal.CallPeakUnit import CallerFromAlignments - -cdef bytes subpeak_letters(short i): - if i < 26: - return chr(97+i).encode() - else: - return subpeak_letters(i // 26) + chr(97 + (i % 26)).encode() - -class PeakDetect: - """Class to do the peak calling. - - e.g - >>> from MACS3.cPeakDetect import cPeakDetect - >>> pd = PeakDetect(treat=treatdata, control=controldata, pvalue=pvalue_cutoff, d=100, gsize=3000000000) - >>> pd.call_peaks() - """ - def __init__ (self,opt = None,treat = None, control = None, d = None, - maxgap = None, minlen = None, slocal = None, llocal = None): - """Initialize the PeakDetect object. - - """ - self.opt = opt - self.info = opt.info - self.debug = opt.debug - self.warn = opt.warn - - self.treat = treat - self.control = control - self.ratio_treat2control = None - self.peaks = None - self.final_peaks = None - self.PE_MODE = opt.PE_MODE - self.scoretrack = None - - #self.femax = opt.femax - #self.femin = opt.femin - #self.festep = opt.festep - - self.log_pvalue = opt.log_pvalue # -log10pvalue - self.log_qvalue = opt.log_qvalue # -log10qvalue - if d != None: - self.d = d - else: - self.d = self.opt.d - - if opt.maxgap: - self.maxgap = opt.maxgap - else: - self.maxgap = opt.tsize - - if opt.minlen: - self.minlen = opt.minlen - else: - self.minlen = self.d - - self.end_shift = self.opt.shift - self.gsize = opt.gsize - - self.nolambda = opt.nolambda - - if slocal != None: - self.sregion = slocal - else: - self.sregion = opt.smalllocal - - if llocal != None: - self.lregion = llocal - else: - self.lregion = opt.largelocal - - if (self.nolambda): - self.info("#3 !!!! DYNAMIC LAMBDA IS DISABLED !!!!") - #self.diag = opt.diag - #self.save_score = opt.store_score - #self.zwig_tr = opt.zwig_tr - #self.zwig_ctl= opt.zwig_ctl - - def call_peaks (self): - """Call peaks function. - - Scan the whole genome for peaks. RESULTS WILL BE SAVED IN - self.final_peaks and self.final_negative_peaks. - """ - if self.control: # w/ control - #if self.opt.broad: - # (self.peaks,self.broadpeaks) = self.__call_peaks_w_control() - #else: - self.peaks = self.__call_peaks_w_control () - else: # w/o control - #if self.opt.broad: - # (self.peaks,self.broadpeaks) = self.__call_peaks_wo_control() - #else: - self.peaks = self.__call_peaks_wo_control () - return self.peaks - - def __call_peaks_w_control (self): - """To call peaks with control data. - - A peak info type is a: dictionary - - key value: chromosome - - items: (peak start,peak end, peak length, peak summit, peak - height, number of tags in peak region, peak pvalue, peak - fold_enrichment) <-- tuple type - - While calculating pvalue: - - First, t and c will be adjusted by the ratio between total - reads in treatment and total reads in control, depending on - --to-small option. - - Then, t and c will be multiplied by the smallest peak size -- - self.d. - - Finally, a poisson CDF is applied to calculate one-side pvalue - for enrichment. - """ - cdef: - int i - float lambda_bg, effective_depth_in_million - float treat_scale, d - list ctrl_scale_s, ctrl_d_s - long treat_total, control_total - long treat_sum # approx sum of treatment pileup values - long control_sum # approx sum of control pileup values - - treat_total = self.treat.total - - if self.PE_MODE: - d = self.treat.average_template_length - control_total = self.control.total * 2 # in PE mode, entire fragment is counted as 1 - # in treatment whereas both ends of fragment are counted in control/input. - treat_sum = self.treat.length - control_sum = control_total * self.treat.average_template_length - self.ratio_treat2control = float(treat_sum)/control_sum - else: - d = self.d - control_total = self.control.total - treat_sum = self.treat.total * self.d - control_sum = self.control.total * self.d - self.ratio_treat2control = float(treat_sum)/control_sum - - if self.opt.ratio != 1.0: - self.ratio_treat2control = self.opt.ratio - - if self.opt.tocontrol: - # if MACS decides to scale treatment to control data because treatment is bigger - effective_depth_in_million = control_total / 1000000.0 - lambda_bg = float( control_sum )/ self.gsize - treat_scale = 1/self.ratio_treat2control - else: - # if MACS decides to scale control to treatment because control sample is bigger - effective_depth_in_million = treat_total / 1000000.0 - lambda_bg = float( treat_sum )/ self.gsize - treat_scale = 1.0 - - # prepare d_s for control data - if self.sregion: - assert self.d <= self.sregion, f"{self.sregion:} can't be smaller than {self.d:}!" - if self.lregion: - assert self.d <= self.lregion , f"{self.lregion:} can't be smaller than {self.d:}!" - assert self.sregion <= self.lregion , f"{self.lregion:} can't be smaller than {self.sregion:}!" - - # Now prepare a list of extension sizes - ctrl_d_s = [ self.d ] # note, d doesn't make sense in PE mode. - # And a list of scaling factors for control - ctrl_scale_s = [] - - # d - if not self.opt.tocontrol: - # if user wants to scale everything to ChIP data - tmp_v = self.ratio_treat2control - else: - tmp_v = 1.0 - ctrl_scale_s.append( tmp_v ) - - # slocal size local - if self.sregion: - ctrl_d_s.append( self.sregion ) - if not self.opt.tocontrol: - # if user want to scale everything to ChIP data - tmp_v = float(self.d)/self.sregion*self.ratio_treat2control - else: - tmp_v = float(self.d)/self.sregion - ctrl_scale_s.append( tmp_v ) - - # llocal size local - if self.lregion and self.lregion > self.sregion: - ctrl_d_s.append( self.lregion ) - if not self.opt.tocontrol: - # if user want to scale everything to ChIP data - tmp_v = float(self.d)/self.lregion*self.ratio_treat2control - else: - tmp_v = float(self.d)/self.lregion - ctrl_scale_s.append( tmp_v ) - - #if self.PE_MODE: # first d/scale are useless in PE mode - # ctrl_d_s = ctrl_d_s[1:] - # ctrl_scale_s = ctrl_scale_s[1:] - # print ctrl_d_s - # print ctrl_scale_s - if self.nolambda: - ctrl_d_s = [] - ctrl_scale_s = [] - - scorecalculator = CallerFromAlignments( self.treat, self.control, - d = d, ctrl_d_s = ctrl_d_s, - treat_scaling_factor = treat_scale, - ctrl_scaling_factor_s = ctrl_scale_s, - end_shift = self.end_shift, - lambda_bg = lambda_bg, - save_bedGraph = self.opt.store_bdg, - bedGraph_filename_prefix = self.opt.name, - bedGraph_treat_filename = self.opt.bdg_treat, - bedGraph_control_filename = self.opt.bdg_control, - save_SPMR = self.opt.do_SPMR, - cutoff_analysis_filename = self.opt.cutoff_analysis_file ) - - if self.opt.trackline: scorecalculator.enable_trackline() - - # call peaks - call_summits = self.opt.call_summits - if call_summits: self.info("#3 Going to call summits inside each peak ...") - - if self.log_pvalue != None: - if self.opt.broad: - self.info("#3 Call broad peaks with given level1 -log10pvalue cutoff and level2: %.5f, %.5f..." % (self.log_pvalue,self.opt.log_broadcutoff) ) - peaks = scorecalculator.call_broadpeaks(['p',], - lvl1_cutoff_s=[self.log_pvalue,], - lvl2_cutoff_s=[self.opt.log_broadcutoff,], - min_length=self.minlen, - lvl1_max_gap=self.maxgap, - lvl2_max_gap=self.maxgap*4, - cutoff_analysis=self.opt.cutoff_analysis ) - else: - self.info("#3 Call peaks with given -log10pvalue cutoff: %.5f ..." % self.log_pvalue) - peaks = scorecalculator.call_peaks( ['p',], [self.log_pvalue,], - min_length=self.minlen, - max_gap=self.maxgap, - call_summits=call_summits, - cutoff_analysis=self.opt.cutoff_analysis ) - elif self.log_qvalue != None: - if self.opt.broad: - self.info("#3 Call broad peaks with given level1 -log10qvalue cutoff and level2: %f, %f..." % (self.log_qvalue,self.opt.log_broadcutoff) ) - peaks = scorecalculator.call_broadpeaks(['q',], - lvl1_cutoff_s=[self.log_qvalue,], - lvl2_cutoff_s=[self.opt.log_broadcutoff,], - min_length=self.minlen, - lvl1_max_gap=self.maxgap, - lvl2_max_gap=self.maxgap*4, - cutoff_analysis=self.opt.cutoff_analysis ) - else: - peaks = scorecalculator.call_peaks( ['q',], [self.log_qvalue,], - min_length=self.minlen, - max_gap=self.maxgap, - call_summits=call_summits, - cutoff_analysis=self.opt.cutoff_analysis ) - scorecalculator.destroy() - return peaks - - def __call_peaks_wo_control (self): - """To call peaks without control data. - - A peak info type is a: dictionary - - key value: chromosome - - items: (peak start,peak end, peak length, peak summit, peak - height, number of tags in peak region, peak pvalue, peak - fold_enrichment) <-- tuple type - - While calculating pvalue: - - First, t and c will be adjusted by the ratio between total - reads in treatment and total reads in control, depending on - --to-small option. - - Then, t and c will be multiplied by the smallest peak size -- - self.d. - - Finally, a poisson CDF is applied to calculate one-side pvalue - for enrichment. - """ - cdef float lambda_bg, effective_depth_in_million - cdef float treat_scale = 1 - cdef float d - cdef list ctrl_scale_s, ctrl_d_s - - if self.PE_MODE: d = 0 - else: d = self.d - treat_length = self.treat.length - treat_total = self.treat.total - - effective_depth_in_million = treat_total / 1000000.0 - - # global lambda - if self.PE_MODE: - # # this an estimator, we should maybe test it for accuracy? - lambda_bg = treat_length / self.gsize - else: - lambda_bg = float(d) * treat_total / self.gsize - treat_scale = 1.0 - - # slocal and d-size local bias are not calculated! - # nothing done here. should this match w control?? - - if not self.nolambda: - if self.PE_MODE: - ctrl_scale_s = [ float(treat_length) / (self.lregion*treat_total*2), ] - else: - ctrl_scale_s = [ float(self.d) / self.lregion, ] - ctrl_d_s = [ self.lregion, ] - else: - ctrl_scale_s = [] - ctrl_d_s = [] - - scorecalculator = CallerFromAlignments( self.treat, None, - d = d, ctrl_d_s = ctrl_d_s, - treat_scaling_factor = treat_scale, - ctrl_scaling_factor_s = ctrl_scale_s, - end_shift = self.end_shift, - lambda_bg = lambda_bg, - save_bedGraph = self.opt.store_bdg, - bedGraph_filename_prefix = self.opt.name, - bedGraph_treat_filename = self.opt.bdg_treat, - bedGraph_control_filename = self.opt.bdg_control, - save_SPMR = self.opt.do_SPMR, - cutoff_analysis_filename = self.opt.cutoff_analysis_file ) - - if self.opt.trackline: scorecalculator.enable_trackline() - - # call peaks - call_summits = self.opt.call_summits - if call_summits: self.info("#3 Going to call summits inside each peak ...") - - if self.log_pvalue != None: - if self.opt.broad: - self.info("#3 Call broad peaks with given level1 -log10pvalue cutoff and level2: %.5f, %.5f..." % (self.log_pvalue,self.opt.log_broadcutoff) ) - peaks = scorecalculator.call_broadpeaks(['p',], - lvl1_cutoff_s=[self.log_pvalue,], - lvl2_cutoff_s=[self.opt.log_broadcutoff,], - min_length=self.minlen, - lvl1_max_gap=self.maxgap, - lvl2_max_gap=self.maxgap*4, - cutoff_analysis=self.opt.cutoff_analysis ) - else: - self.info("#3 Call peaks with given -log10pvalue cutoff: %.5f ..." % self.log_pvalue) - peaks = scorecalculator.call_peaks( ['p',], [self.log_pvalue,], - min_length=self.minlen, - max_gap=self.maxgap, - call_summits=call_summits, - cutoff_analysis=self.opt.cutoff_analysis ) - elif self.log_qvalue != None: - if self.opt.broad: - self.info("#3 Call broad peaks with given level1 -log10qvalue cutoff and level2: %f, %f..." % (self.log_qvalue,self.opt.log_broadcutoff) ) - peaks = scorecalculator.call_broadpeaks(['q',], - lvl1_cutoff_s=[self.log_qvalue,], - lvl2_cutoff_s=[self.opt.log_broadcutoff,], - min_length=self.minlen, - lvl1_max_gap=self.maxgap, - lvl2_max_gap=self.maxgap*4, - cutoff_analysis=self.opt.cutoff_analysis ) - else: - peaks = scorecalculator.call_peaks( ['q',], [self.log_qvalue,], - min_length=self.minlen, - max_gap=self.maxgap, - call_summits=call_summits, - cutoff_analysis=self.opt.cutoff_analysis ) - scorecalculator.destroy() - return peaks - diff --git a/MACS3/Signal/PeakModel.py b/MACS3/Signal/PeakModel.py new file mode 100644 index 00000000..984e3e7b --- /dev/null +++ b/MACS3/Signal/PeakModel.py @@ -0,0 +1,513 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-15 10:20:32 Tao Liu> +"""Module Description: Build shifting model + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file LICENSE included with +the distribution). +""" + +# ------------------------------------ +# Python modules +# ------------------------------------ + +# ------------------------------------ +# MACS3 modules +# ------------------------------------ +# from MACS3.Utilities.Constants import * +from MACS3.Signal.Pileup import naive_quick_pileup, naive_call_peaks + +# ------------------------------------ +# Other modules +# ------------------------------------ +import cython +from cython.cimports.cpython import bool +import numpy as np +import cython.cimports.numpy as cnp + +# ------------------------------------ +# C lib +# ------------------------------------ + + +class NotEnoughPairsException(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +@cython.cclass +class PeakModel: + """Peak Model class. + """ + # this can be PETrackI or FWTrack + treatment: object + # genome size + gz: cython.double + max_pairnum: cython.int + umfold: cython.int + lmfold: cython.int + bw: cython.int + d_min: cython.int + tag_expansion_size: cython.int + + info: object + debug: object + warn: object + error: object + + summary: str + max_tags: cython.int + peaksize: cython.int + + plus_line = cython.declare(cnp.ndarray, visibility="public") + minus_line = cython.declare(cnp.ndarray, visibility="public") + shifted_line = cython.declare(cnp.ndarray, visibility="public") + xcorr = cython.declare(cnp.ndarray, visibility="public") + ycorr = cython.declare(cnp.ndarray, visibility="public") + + d = cython.declare(cython.int, visibility="public") + scan_window = cython.declare(cython.int, visibility="public") + min_tags = cython.declare(cython.int, visibility="public") + alternative_d = cython.declare(list, visibility="public") + + def __init__(self, opt, treatment, max_pairnum: cython.int = 500): + # , double gz = 0, int umfold=30, int lmfold=10, int bw=200, + # int ts = 25, int bg=0, bool quiet=False): + self.treatment = treatment + self.gz = opt.gsize + self.umfold = opt.umfold + self.lmfold = opt.lmfold + # opt.tsize| test 10bps. The reason is that we want the best + # 'lag' between left & right cutting sides. A tag will be + # expanded to 10bps centered at cutting point. + self.tag_expansion_size = 10 + # discard any predicted fragment sizes < d_min + self.d_min = opt.d_min + self.bw = opt.bw + self.info = opt.info + self.debug = opt.debug + self.warn = opt.warn + self.error = opt.warn + self.max_pairnum = max_pairnum + + @cython.ccall + def build(self): + """Build the model. Main function of PeakModel class. + + 1. prepare self.d, self.scan_window, self.plus_line, + self.minus_line and self.shifted_line. + + 2. find paired + and - strand peaks + + 3. find the best d using x-correlation + """ + paired_peakpos: dict + num_paired_peakpos: cython.long + c: bytes # chromosome + + self.peaksize = 2*self.bw + # mininum unique hits on single strand, decided by lmfold + self.min_tags = int(round(float(self.treatment.total) * + self.lmfold * + self.peaksize / self.gz / 2)) + # maximum unique hits on single strand, decided by umfold + self.max_tags = int(round(float(self.treatment.total) * + self.umfold * + self.peaksize / self.gz / 2)) + self.debug(f"#2 min_tags: {self.min_tags}; max_tags:{self.max_tags}; ") + self.info("#2 looking for paired plus/minus strand peaks...") + # find paired + and - strand peaks + paired_peakpos = self.__find_paired_peaks() + + num_paired_peakpos = 0 + for c in list(paired_peakpos.keys()): + num_paired_peakpos += len(paired_peakpos[c]) + + self.info("#2 Total number of paired peaks: %d" % (num_paired_peakpos)) + + if num_paired_peakpos < 100: + self.error(f"#2 MACS3 needs at least 100 paired peaks at + and - strand to build the model, but can only find {num_paired_peakpos}! Please make your MFOLD range broader and try again. If MACS3 still can't build the model, we suggest to use --nomodel and --extsize 147 or other fixed number instead.") + self.error("#2 Process for pairing-model is terminated!") + raise NotEnoughPairsException("No enough pairs to build model") + + # build model, find the best d using cross-correlation + self.__paired_peak_model(paired_peakpos) + + def __str__(self): + """For debug... + + """ + return """ +Summary of Peak Model: + Baseline: %d + Upperline: %d + Fragment size: %d + Scan window size: %d +""" % (self.min_tags, self.max_tags, self.d, self.scan_window) + + @cython.cfunc + def __find_paired_peaks(self) -> dict: + """Call paired peaks from fwtrackI object. + + Return paired peaks center positions. + """ + i: cython.int + chrs: list + chrom: bytes + plus_tags: cnp.ndarray(cython.int, ndim=1) + minus_tags: cnp.ndarray(cython.int, ndim=1) + plus_peaksinfo: list + minus_peaksinfo: list + paired_peaks_pos: dict # return + + chrs = list(self.treatment.get_chr_names()) + chrs.sort() + paired_peaks_pos = {} + for i in range(len(chrs)): + chrom = chrs[i] + self.debug(f"Chromosome: {chrom}") + # extract tag positions + [plus_tags, minus_tags] = self.treatment.get_locations_by_chr(chrom) + # look for + strand peaks + plus_peaksinfo = self.__naive_find_peaks(plus_tags) + self.debug("Number of unique tags on + strand: %d" % (plus_tags.shape[0])) + self.debug("Number of peaks in + strand: %d" % (len(plus_peaksinfo))) + if plus_peaksinfo: + self.debug(f"plus peaks: first - {plus_peaksinfo[0]} ... last - {plus_peaksinfo[-1]}") + # look for - strand peaks + minus_peaksinfo = self.__naive_find_peaks(minus_tags) + self.debug("Number of unique tags on - strand: %d" % (minus_tags.shape[0])) + self.debug("Number of peaks in - strand: %d" % (len(minus_peaksinfo))) + if minus_peaksinfo: + self.debug(f"minus peaks: first - {minus_peaksinfo[0]} ... last - {minus_peaksinfo[-1]}") + if not plus_peaksinfo or not minus_peaksinfo: + self.debug("Chrom %s is discarded!" % (chrom)) + continue + else: + paired_peaks_pos[chrom] = self.__find_pair_center(plus_peaksinfo, minus_peaksinfo) + self.debug("Number of paired peaks in this chromosome: %d" % (len(paired_peaks_pos[chrom]))) + return paired_peaks_pos + + @cython.cfunc + def __naive_find_peaks(self, + taglist: cnp.ndarray(cython.int, ndim=1)) -> list: + """Naively call peaks based on tags counting. + + Return peak positions and the tag number in peak region by a tuple list[(pos,num)]. + """ + peak_info: list + pileup_array: list + + # store peak pos in every peak region and unique tag number in + # every peak region + peak_info = [] + + # less than 2 tags, no need to call peaks, return [] + if taglist.shape[0] < 2: + return peak_info + + # build pileup by extending both side to half peak size + pileup_array = naive_quick_pileup(taglist, int(self.peaksize/2)) + peak_info = naive_call_peaks(pileup_array, + self.min_tags, + self.max_tags) + + return peak_info + + @cython.cfunc + def __paired_peak_model(self, paired_peakpos: dict): + """Use paired peak positions and treatment tag positions to + build the model. + + Modify self.(d, model_shift size and scan_window size. and + extra, plus_line, minus_line and shifted_line for plotting). + + """ + window_size: cython.int + i: cython.int + chroms: list + paired_peakpos_chrom: object + + tags_plus: cnp.ndarray(cython.int, ndim=1) + tags_minus: cnp.ndarray(cython.int, ndim=1) + plus_start: cnp.ndarray(cython.int, ndim=1) + plus_end: cnp.ndarray(cython.int, ndim=1) + minus_start: cnp.ndarray(cython.int, ndim=1) + minus_end: cnp.ndarray(cython.int, ndim=1) + plus_line: cnp.ndarray(cython.int, ndim=1) + minus_line: cnp.ndarray(cython.int, ndim=1) + + plus_data: cnp.ndarray + minus_data: cnp.ndarray + xcorr: cnp.ndarray + ycorr: cnp.ndarray + i_l_max: cnp.ndarray + + window_size = 1+2*self.peaksize+self.tag_expansion_size + # for plus strand pileup + self.plus_line = np.zeros(window_size, dtype="i4") + # for minus strand pileup + self.minus_line = np.zeros(window_size, dtype="i4") + # for fast pileup + plus_start = np.zeros(window_size, dtype="i4") + # for fast pileup + plus_end = np.zeros(window_size, dtype="i4") + # for fast pileup + minus_start = np.zeros(window_size, dtype="i4") + # for fast pileup + minus_end = np.zeros(window_size, dtype="i4") + self.debug("start model_add_line...") + chroms = list(paired_peakpos.keys()) + + for i in range(len(chroms)): + paired_peakpos_chrom = paired_peakpos[chroms[i]] + (tags_plus, tags_minus) = self.treatment.get_locations_by_chr(chroms[i]) + # every paired peak has plus line and minus line + self.__model_add_line(paired_peakpos_chrom, + tags_plus, + plus_start, + plus_end) + self.__model_add_line(paired_peakpos_chrom, + tags_minus, + minus_start, + minus_end) + + self.__count(plus_start, plus_end, self.plus_line) + self.__count(minus_start, minus_end, self.minus_line) + + self.debug("start X-correlation...") + # Now I use cross-correlation to find the best d + plus_line = self.plus_line + minus_line = self.minus_line + + # normalize first + minus_data = (minus_line - minus_line.mean())/(minus_line.std()*len(minus_line)) + plus_data = (plus_line - plus_line.mean())/(plus_line.std()*len(plus_line)) + + # cross-correlation + ycorr = np.correlate(minus_data, plus_data, mode="full")[window_size-self.peaksize:window_size+self.peaksize] + xcorr = np.linspace(len(ycorr)//2*-1, len(ycorr)//2, num=len(ycorr)) + + # smooth correlation values to get rid of local maximums from small fluctuations. + # window size is by default 11. + ycorr = smooth(ycorr, window="flat") + + # all local maximums could be alternative ds. + i_l_max = np.r_[False, ycorr[1:] > ycorr[:-1]] & np.r_[ycorr[:-1] > ycorr[1:], False] + i_l_max = np.where(i_l_max)[0] + i_l_max = i_l_max[xcorr[i_l_max] > self.d_min] + i_l_max = i_l_max[np.argsort(ycorr[i_l_max])[::-1]] + + self.alternative_d = sorted([int(x) for x in xcorr[i_l_max]]) + assert len(self.alternative_d) > 0, "No proper d can be found! Tweak --mfold?" + + self.d = xcorr[i_l_max[0]] + + self.ycorr = ycorr + self.xcorr = xcorr + + self.scan_window = max(self.d, self.tag_expansion_size)*2 + + self.info("#2 Model building with cross-correlation: Done") + + return True + + @cython.cfunc + def __model_add_line(self, + pos1: list, + pos2: cnp.ndarray(cython.int, ndim=1), + start: cnp.ndarray(cython.int, ndim=1), + end: cnp.ndarray(cython.int, ndim=1)): + """Project each pos in pos2 which is included in + [pos1-self.peaksize,pos1+self.peaksize] to the line. + + pos1: paired centers -- list of coordinates + pos2: tags of certain strand -- a numpy.array object + line: numpy array object where we pileup tags + + """ + i1: cython.int + i2: cython.int + i2_prev: cython.int + i1_max: cython.int + i2_max: cython.int + last_p2: cython.int + psize_adjusted1: cython.int + p1: cython.int + p2: cython.int + max_index: cython.int + s: cython.int + e: cython.int + + i1 = 0 # index for pos1 + i2 = 0 # index for pos2 index for pos2 in + # previous pos1 [pos1-self.peaksize,pos1+self.peaksize] region + i2_prev = 0 + i1_max = len(pos1) + i2_max = pos2.shape[0] + flag_find_overlap = False + + max_index = start.shape[0] - 1 + + # half window + psize_adjusted1 = self.peaksize + self.tag_expansion_size // 2 + + while i1 < i1_max and i2 < i2_max: + p1 = pos1[i1] + p2 = pos2[i2] + + if p1-psize_adjusted1 > p2: + # move pos2 + i2 += 1 + elif p1+psize_adjusted1 < p2: + # move pos1 + i1 += 1 + i2 = i2_prev # search minus peaks from previous index + flag_find_overlap = False + else: # overlap! + if not flag_find_overlap: + flag_find_overlap = True + # only the first index is recorded + i2_prev = i2 + # project + s = max(int(p2-self.tag_expansion_size/2-p1+psize_adjusted1), 0) + start[s] += 1 + e = min(int(p2+self.tag_expansion_size/2-p1+psize_adjusted1), max_index) + end[e] -= 1 + i2 += 1 + return + + @cython.cfunc + def __count(self, + start: cnp.ndarray(cython.int, ndim=1), + end: cnp.ndarray(cython.int, ndim=1), + line: cnp.ndarray(cython.int, ndim=1)): + """ + """ + i: cython.int + pileup: cython.long + + pileup = 0 + for i in range(line.shape[0]): + pileup += start[i] + end[i] + line[i] = pileup + return + + @cython.cfunc + def __find_pair_center(self, + pluspeaks: list, + minuspeaks: list): + # index for plus peaks + ip: cython.long = 0 + # index for minus peaks + im: cython.long = 0 + # index for minus peaks in previous plus peak + im_prev: cython.long = 0 + pair_centers: list + ip_max: cython.long + im_max: cython.long + flag_find_overlap: bool + pp: cython.int + mp: cython.int + pn: cython.float + mn: cython.float + + pair_centers = [] + ip_max = len(pluspeaks) + im_max = len(minuspeaks) + self.debug(f"ip_max: {ip_max}; im_max: {im_max}") + flag_find_overlap = False + while ip < ip_max and im < im_max: + # for (peakposition, tagnumber in peak) + (pp, pn) = pluspeaks[ip] + (mp, mn) = minuspeaks[im] + if pp-self.peaksize > mp: + # move minus + im += 1 + elif pp+self.peaksize < mp: + # move plus + ip += 1 + im = im_prev # search minus peaks from previous index + flag_find_overlap = False + else: # overlap! + if not flag_find_overlap: + flag_find_overlap = True + # only the first index is recorded + im_prev = im + # number tags in plus and minus peak region are comparable... + if pn/mn < 2 and pn/mn > 0.5: + if pp < mp: + pair_centers.append((pp+mp)//2) + im += 1 + if pair_centers: + self.debug(f"Paired centers: first - {pair_centers[0]} ... second - {pair_centers[-1]} ") + return pair_centers + + +# smooth function from SciPy cookbook: +# http://www.scipy.org/Cookbook/SignalSmooth +@cython.ccall +def smooth(x, + window_len: cython.int = 11, + window: str = 'hanning'): + """smooth the data using a window with requested size. + + This method is based on the convolution of a scaled window with the signal. + The signal is prepared by introducing reflected copies of the signal + (with the window size) in both ends so that transient parts are minimized + in the beginning and end part of the output signal. + + input: + x: the input signal + window_len: the dimension of the smoothing window; should be + an odd integer + window: the type of window from 'flat', 'hanning', 'hamming', + 'bartlett', 'blackman' flat window will produce a + moving average smoothing. + + output: + the smoothed signal + + example: + + t=linspace(-2,2,0.1) + x=sin(t)+randn(len(t))*0.1 + y=smooth(x) + + see also: + + numpy.hanning, numpy.hamming, numpy.bartlett, numpy.blackman, + numpy.convolve scipy.signal.lfilter + + TODO: the window parameter could be the window itself if an array + instead of a string + + NOTE: length(output) != length(input), to correct this: return + y[(window_len/2-1):-(window_len/2)] instead of just y. + """ + + if x.ndim != 1: + raise ValueError("smooth only accepts 1 dimension arrays.") + + if x.size < window_len: + raise ValueError("Input vector needs to be bigger than window size.") + + if window_len < 3: + return x + + if window not in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']: + raise ValueError("Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'") + + s = np.r_[x[window_len-1:0:-1], x, x[-1:-window_len:-1]] + + if window == 'flat': # moving average + w = np.ones(window_len, 'd') + else: + w = eval('np.'+window+'(window_len)') + + y = np.convolve(w/w.sum(), s, mode='valid') + return y[(window_len//2):-(window_len//2)] diff --git a/MACS3/Signal/PeakModel.pyx b/MACS3/Signal/PeakModel.pyx deleted file mode 100644 index 575ce114..00000000 --- a/MACS3/Signal/PeakModel.pyx +++ /dev/null @@ -1,418 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2024-10-04 18:10:08 Tao Liu> -"""Module Description: Build shifting model - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file LICENSE included with -the distribution). -""" - -# ------------------------------------ -# Python modules -# ------------------------------------ -import sys, time, random -import array - -# ------------------------------------ -# MACS3 modules -# ------------------------------------ -from MACS3.Utilities.Constants import * -from MACS3.Signal.Pileup import naive_quick_pileup, naive_call_peaks - -# ------------------------------------ -# Other modules -# ------------------------------------ -from cpython cimport bool -from cpython cimport array -import numpy as np -cimport numpy as np - -# ------------------------------------ -# C lib -# ------------------------------------ -from libc.stdint cimport uint32_t, uint64_t, int32_t, int64_t - -ctypedef np.float32_t float32_t - -class NotEnoughPairsException(Exception): - def __init__ (self,value): - self.value = value - def __str__ (self): - return repr(self.value) - -cdef class PeakModel: - """Peak Model class. - """ - cdef: - object treatment - double gz - int max_pairnum - int umfold - int lmfold - int bw - int d_min - int tag_expansion_size - object info, debug, warn, error - str summary - int max_tags - int peaksize - public np.ndarray plus_line, minus_line, shifted_line - public int d - public int scan_window - public int min_tags - public list alternative_d - public np.ndarray xcorr, ycorr - - def __init__ ( self, opt , treatment, int max_pairnum=500 ): #, double gz = 0, int umfold=30, int lmfold=10, int bw=200, int ts = 25, int bg=0, bool quiet=False): - self.treatment = treatment - self.gz = opt.gsize - self.umfold = opt.umfold - self.lmfold = opt.lmfold - self.tag_expansion_size = 10 #opt.tsize| test 10bps. The reason is that we want the best 'lag' between left & right cutting sides. A tag will be expanded to 10bps centered at cutting point. - self.d_min = opt.d_min #discard any predicted fragment sizes < d_min - self.bw = opt.bw - self.info = opt.info - self.debug = opt.debug - self.warn = opt.warn - self.error = opt.warn - self.max_pairnum = max_pairnum - - cpdef build (self): - """Build the model. Main function of PeakModel class. - - 1. prepare self.d, self.scan_window, self.plus_line, - self.minus_line and self.shifted_line. - - 2. find paired + and - strand peaks - - 3. find the best d using x-correlation - """ - cdef: - dict paired_peakpos - long num_paired_peakpos - bytes c #chromosome - - self.peaksize = 2*self.bw - self.min_tags = int(round(float(self.treatment.total) * self.lmfold * self.peaksize / self.gz / 2)) # mininum unique hits on single strand, decided by lmfold - self.max_tags = int(round(float(self.treatment.total) * self.umfold * self.peaksize / self.gz /2)) # maximum unique hits on single strand, decided by umfold - self.debug( f"#2 min_tags: {self.min_tags}; max_tags:{self.max_tags}; " ) - - self.info( "#2 looking for paired plus/minus strand peaks..." ) - # find paired + and - strand peaks - paired_peakpos = self.__find_paired_peaks () - - num_paired_peakpos = 0 - for c in list( paired_peakpos.keys() ): - num_paired_peakpos += len (paired_peakpos[c] ) - - self.info("#2 Total number of paired peaks: %d" % (num_paired_peakpos)) - - if num_paired_peakpos < 100: - self.error(f"#2 MACS3 needs at least 100 paired peaks at + and - strand to build the model, but can only find {num_paired_peakpos}! Please make your MFOLD range broader and try again. If MACS3 still can't build the model, we suggest to use --nomodel and --extsize 147 or other fixed number instead.") - self.error("#2 Process for pairing-model is terminated!") - raise NotEnoughPairsException("No enough pairs to build model") - - # build model, find the best d using cross-correlation - self.__paired_peak_model(paired_peakpos) - - def __str__ (self): - """For debug... - - """ - return """ -Summary of Peak Model: - Baseline: %d - Upperline: %d - Fragment size: %d - Scan window size: %d -""" % (self.min_tags,self.max_tags,self.d,self.scan_window) - - cdef dict __find_paired_peaks (self): - """Call paired peaks from fwtrackI object. - - Return paired peaks center positions. - """ - cdef: - int i - list chrs - bytes chrom - np.ndarray[np.int32_t, ndim=1] plus_tags, minus_tags - list plus_peaksinfo - list minus_peaksinfo - dict paired_peaks_pos # return - - chrs = list(self.treatment.get_chr_names()) - chrs.sort() - paired_peaks_pos = {} - for i in range( len(chrs) ): - chrom = chrs[ i ] - self.debug( f"Chromosome: {chrom}" ) - # extract tag positions - [ plus_tags, minus_tags ] = self.treatment.get_locations_by_chr( chrom ) - # look for + strand peaks - plus_peaksinfo = self.__naive_find_peaks ( plus_tags ) - self.debug("Number of unique tags on + strand: %d" % ( plus_tags.shape[0] ) ) - self.debug("Number of peaks in + strand: %d" % ( len(plus_peaksinfo) ) ) - if plus_peaksinfo: - self.debug(f"plus peaks: first - {plus_peaksinfo[0]} ... last - {plus_peaksinfo[-1]}") - # look for - strand peaks - minus_peaksinfo = self.__naive_find_peaks ( minus_tags ) - self.debug("Number of unique tags on - strand: %d" % ( minus_tags.shape[0] ) ) - self.debug("Number of peaks in - strand: %d" % ( len( minus_peaksinfo ) ) ) - if minus_peaksinfo: - self.debug(f"minus peaks: first - {minus_peaksinfo[0]} ... last - {minus_peaksinfo[-1]}") - if not plus_peaksinfo or not minus_peaksinfo: - self.debug("Chrom %s is discarded!" % (chrom) ) - continue - else: - paired_peaks_pos[chrom] = self.__find_pair_center (plus_peaksinfo, minus_peaksinfo) - self.debug("Number of paired peaks in this chromosome: %d" %(len(paired_peaks_pos[chrom]))) - return paired_peaks_pos - - cdef list __naive_find_peaks ( self, np.ndarray[np.int32_t, ndim=1] taglist ): - """Naively call peaks based on tags counting. - - Return peak positions and the tag number in peak region by a tuple list [(pos,num)]. - """ - cdef: - long i - int pos - list peak_info - list pileup_array - - peak_info = [] # store peak pos in every peak region and - # unique tag number in every peak region - if taglist.shape[0] < 2: # less than 2 tags, no need to call peaks, return [] - return peak_info - - pileup_array = naive_quick_pileup( taglist, int(self.peaksize/2) ) # build pileup by extending both side to half peak size - peak_info = naive_call_peaks( pileup_array, self.min_tags, self.max_tags ) - - return peak_info - - cdef __paired_peak_model (self, dict paired_peakpos,): - """Use paired peak positions and treatment tag positions to build the model. - - Modify self.(d, model_shift size and scan_window size. and extra, plus_line, minus_line and shifted_line for plotting). - """ - cdef: - int window_size, i - list chroms - object paired_peakpos_chrom - np.ndarray[np.int32_t, ndim=1] tags_plus, tags_minus, plus_start, plus_end, minus_start, minus_end, plus_line, minus_line - np.ndarray plus_data, minus_data, xcorr, ycorr, i_l_max - - window_size = 1+2*self.peaksize+self.tag_expansion_size - self.plus_line = np.zeros(window_size, dtype="int32") # for plus strand pileup - self.minus_line = np.zeros(window_size, dtype="int32")# for minus strand pileup - plus_start = np.zeros(window_size, dtype="int32") # for fast pileup - plus_end = np.zeros(window_size, dtype="int32") # for fast pileup - minus_start = np.zeros(window_size, dtype="int32") # for fast pileup - minus_end = np.zeros(window_size, dtype="int32") # for fast pileup - self.debug("start model_add_line...") - chroms = list(paired_peakpos.keys()) - - for i in range(len(chroms)): - paired_peakpos_chrom = paired_peakpos[chroms[i]] - (tags_plus, tags_minus) = self.treatment.get_locations_by_chr(chroms[i]) - # every paired peak has plus line and minus line - self.__model_add_line (paired_peakpos_chrom, tags_plus, plus_start, plus_end) #, plus_strand=1) - self.__model_add_line (paired_peakpos_chrom, tags_minus, minus_start, minus_end) #, plus_strand=0) - - self.__count ( plus_start, plus_end, self.plus_line ) - self.__count ( minus_start, minus_end, self.minus_line ) - - self.debug("start X-correlation...") - # Now I use cross-correlation to find the best d - plus_line = self.plus_line - minus_line = self.minus_line - - # normalize first - minus_data = (minus_line - minus_line.mean())/(minus_line.std()*len(minus_line)) - plus_data = (plus_line - plus_line.mean())/(plus_line.std()*len(plus_line)) - - # cross-correlation - ycorr = np.correlate(minus_data,plus_data,mode="full")[window_size-self.peaksize:window_size+self.peaksize] - xcorr = np.linspace(len(ycorr)//2*-1, len(ycorr)//2, num=len(ycorr)) - - # smooth correlation values to get rid of local maximums from small fluctuations. - ycorr = smooth(ycorr, window="flat") # window size is by default 11. - - # all local maximums could be alternative ds. - i_l_max = np.r_[False, ycorr[1:] > ycorr[:-1]] & np.r_[ycorr[:-1] > ycorr[1:], False] - i_l_max = np.where(i_l_max)[0] - i_l_max = i_l_max[ xcorr[i_l_max] > self.d_min ] - i_l_max = i_l_max[ np.argsort(ycorr[i_l_max])[::-1]] - - self.alternative_d = sorted([int(x) for x in xcorr[i_l_max]]) - assert len(self.alternative_d) > 0, "No proper d can be found! Tweak --mfold?" - - self.d = xcorr[i_l_max[0]] - - self.ycorr = ycorr - self.xcorr = xcorr - - self.scan_window = max(self.d,self.tag_expansion_size)*2 - - self.info("#2 Model building with cross-correlation: Done") - - return True - - cdef __model_add_line (self, list pos1, np.ndarray[np.int32_t, ndim=1] pos2, np.ndarray[np.int32_t, ndim=1] start, np.ndarray[np.int32_t, ndim=1] end): #, int plus_strand=1): - """Project each pos in pos2 which is included in - [pos1-self.peaksize,pos1+self.peaksize] to the line. - - pos1: paired centers -- list of coordinates - pos2: tags of certain strand -- a numpy.array object - line: numpy array object where we pileup tags - - """ - cdef: - int i1, i2, i2_prev, i1_max, i2_max, last_p2, psize_adjusted1, psize_adjusted2, p1, p2, max_index, s, e - - i1 = 0 # index for pos1 - i2 = 0 # index for pos2 - i2_prev = 0 # index for pos2 in previous pos1 - # [pos1-self.peaksize,pos1+self.peaksize] - # region - i1_max = len(pos1) - i2_max = pos2.shape[0] - last_p2 = -1 - flag_find_overlap = False - - max_index = start.shape[0] - 1 - - psize_adjusted1 = self.peaksize + self.tag_expansion_size // 2 # half window - - while i1 p2: # move pos2 - i2 += 1 - elif p1+psize_adjusted1 < p2: # move pos1 - i1 += 1 - i2 = i2_prev # search minus peaks from previous index - flag_find_overlap = False - else: # overlap! - if not flag_find_overlap: - flag_find_overlap = True - i2_prev = i2 # only the first index is recorded - # project - s = max(int(p2-self.tag_expansion_size/2-p1+psize_adjusted1), 0) - start[s] += 1 - e = min(int(p2+self.tag_expansion_size/2-p1+psize_adjusted1), max_index) - end[e] -= 1 - i2+=1 - return - - cdef __count ( self, np.ndarray[np.int32_t, ndim=1] start, np.ndarray[np.int32_t, ndim=1] end, np.ndarray[np.int32_t, ndim=1] line ): - """ - """ - cdef: - int i - long pileup - pileup = 0 - for i in range(line.shape[0]): - pileup += start[i] + end[i] - line[i] = pileup - return - - cdef __find_pair_center (self, list pluspeaks, list minuspeaks): - cdef: - long ip = 0 # index for plus peaks - long im = 0 # index for minus peaks - long im_prev = 0 # index for minus peaks in previous plus peak - list pair_centers - long ip_max - long im_max - bool flag_find_overlap - int pp, mp - float pn, mn - - pair_centers = [] - ip_max = len(pluspeaks) - im_max = len(minuspeaks) - self.debug(f"ip_max: {ip_max}; im_max: {im_max}") - flag_find_overlap = False - while ip mp: # move minus - im += 1 - elif pp+self.peaksize < mp: # move plus - ip += 1 - im = im_prev # search minus peaks from previous index - flag_find_overlap = False - else: # overlap! - if not flag_find_overlap: - flag_find_overlap = True - im_prev = im # only the first index is recorded - if pn/mn < 2 and pn/mn > 0.5: # number tags in plus and minus peak region are comparable... - if pp < mp: - pair_centers.append((pp+mp)//2) - #self.debug ( "distance: %d, minus: %d, plus: %d" % (mp-pp,mp,pp)) - im += 1 - if pair_centers: - self.debug(f"Paired centers: first - {pair_centers[0]} ... second - {pair_centers[-1]} ") - return pair_centers - -# smooth function from SciPy cookbook: http://www.scipy.org/Cookbook/SignalSmooth -cpdef smooth(x, int window_len=11, str window='hanning'): - """smooth the data using a window with requested size. - - This method is based on the convolution of a scaled window with the signal. - The signal is prepared by introducing reflected copies of the signal - (with the window size) in both ends so that transient parts are minimized - in the beginning and end part of the output signal. - - input: - x: the input signal - window_len: the dimension of the smoothing window; should be an odd integer - window: the type of window from 'flat', 'hanning', 'hamming', 'bartlett', 'blackman' - flat window will produce a moving average smoothing. - - output: - the smoothed signal - - example: - - t=linspace(-2,2,0.1) - x=sin(t)+randn(len(t))*0.1 - y=smooth(x) - - see also: - - numpy.hanning, numpy.hamming, numpy.bartlett, numpy.blackman, numpy.convolve - scipy.signal.lfilter - - TODO: the window parameter could be the window itself if an array instead of a string - NOTE: length(output) != length(input), to correct this: return y[(window_len/2-1):-(window_len/2)] instead of just y. - """ - - if x.ndim != 1: - raise ValueError, "smooth only accepts 1 dimension arrays." - - if x.size < window_len: - raise ValueError, "Input vector needs to be bigger than window size." - - - if window_len<3: - return x - - - if not window in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']: - raise ValueError, "Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'" - - - s=np.r_[x[window_len-1:0:-1],x,x[-1:-window_len:-1]] - #print(len(s)) - if window == 'flat': #moving average - w=np.ones(window_len,'d') - else: - w=eval('np.'+window+'(window_len)') - - y=np.convolve(w/w.sum(),s,mode='valid') - return y[(window_len//2):-(window_len//2)] - diff --git a/MACS3/Signal/SignalProcessing.pyx b/MACS3/Signal/SignalProcessing.py similarity index 61% rename from MACS3/Signal/SignalProcessing.pyx rename to MACS3/Signal/SignalProcessing.py index 3a9a7220..5632e821 100644 --- a/MACS3/Signal/SignalProcessing.pyx +++ b/MACS3/Signal/SignalProcessing.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-05-14 11:43:45 Tao Liu> +# Time-stamp: <2024-10-15 11:25:35 Tao Liu> """Module Description: functions to find maxima minima or smooth the signal tracks. @@ -20,39 +20,42 @@ # ------------------------------------ # smoothing function import numpy as np -cimport numpy as np -from numpy cimport uint8_t, uint16_t, uint32_t, uint64_t, int8_t, int16_t, int32_t, int64_t, float32_t, float64_t -from cpython cimport bool +import cython +import cython.cimports.numpy as cnp +from cython.cimports.cpython import bool -cpdef np.ndarray[int32_t, ndim=1] maxima(np.ndarray[float32_t, ndim=1] signal, - int window_size=51): +@cython.ccall +def maxima(signal: cnp.ndarray(cython.float, ndim=1), + window_size: cython.int = 51) -> cnp.ndarray: """return the local maxima in a signal after applying a 2nd order Savitsky-Golay (polynomial) filter using window_size specified """ - cdef: - np.ndarray[int32_t, ndim=1] m - np.ndarray[float64_t, ndim=1] smoothed - np.ndarray[float64_t, ndim=1] sign, diff + m: cnp.ndarray(cython.int, ndim=1) + smoothed: cnp.ndarray(cython.double, ndim=1) + sign: cnp.ndarray(cython.double, ndim=1) + diff: cnp.ndarray(cython.double, ndim=1) - window_size = window_size//2*2+1 # to make an odd number + window_size = window_size//2*2+1 # to make an odd number smoothed = savitzky_golay_order2_deriv1(signal, window_size).round(16) - sign = np.sign( smoothed ) - diff = np.diff( sign ) - m = np.where( diff <= -1)[0].astype("int32") + sign = np.sign(smoothed) + diff = np.diff(sign) + m = np.where(diff <= -1)[0].astype("i4") return m -cdef np.ndarray[int32_t, ndim=1] internal_minima( np.ndarray[float32_t, ndim=1] signal, - np.ndarray[int32_t, ndim=1] maxima ): - cdef: - np.ndarray[int32_t, ndim=1] ret - int32_t n = maxima.shape[0] - int32_t i, v, v2 + +@cython.cfunc +def internal_minima(signal: cnp.ndarray(cython.float, ndim=1), + maxima: cnp.ndarray(cython.int, ndim=1)) -> cnp.ndarray: + ret: cnp.ndarray(cython.int, ndim=1) + n: cython.int = maxima.shape[0] + i: cython.int + if n == 0 or n == 1: - ret = np.ndarray(0, 'int32') + ret = np.ndarray(0, 'i4') return ret else: - ret = np.zeros(n - 1, 'int32') + ret = np.zeros(n - 1, 'i4') pos1 = maxima[0] for i in range(n - 1): pos2 = maxima[i + 1] @@ -60,38 +63,51 @@ pos1 = pos2 return ret -cdef inline float32_t sqrt(float32_t threshold): + +@cython.cfunc +@cython.inline +def sqrt(threshold: cython.float) -> cython.float: return mathsqrt(threshold) -cpdef enforce_peakyness(np.ndarray[float32_t, ndim=1] signal, - np.ndarray[int32_t, ndim=1] maxima): - """requires peaks described by a signal and a set of points where the signal - is at a maximum to meet a certain set of criteria + +@cython.ccall +def enforce_peakyness(signal: cnp.ndarray(cython.float, ndim=1), + maxima: cnp.ndarray(cython.int, ndim=1)): + """requires peaks described by a signal and a set of points where + the signal is at a maximum to meet a certain set of criteria maxima which do not meet the required criteria are discarded criteria: for each peak: - calculate a threshold of the maximum of its adjacent two minima - plus the sqrt of that value + + calculate a threshold of the maximum of its adjacent two + minima plus the sqrt of that value + subtract the threshold from the region bounded by those minima + clip that region if negative values occur inside it + require it be > 50 bp in width -- controlled by is_valied_peak() - require that it not be too flat (< 6 unique values) -- controlled by is_valid_peak() + + require that it not be too flat (< 6 unique values) -- + controlled by is_valid_peak() + """ - cdef: - np.ndarray[int32_t, ndim=1] minima = internal_minima(signal, maxima) - np.ndarray[float32_t, ndim=1] new_signal - int32_t n = minima.shape[0] - float32_t threshold - np.ndarray[int32_t, ndim=1] peaky_maxima = maxima.copy() - int32_t j = 0 - if n == 0: return maxima -# else: + minima: cnp.ndarray(cython.int, ndim=1) = internal_minima(signal, maxima) + new_signal: cnp.ndarray(cython.float, ndim=1) + n: cython.int = minima.shape[0] + threshold: cython.float + peaky_maxima: cnp.ndarray(cython.int, ndim=1) = maxima.copy() + j: cython.int = 0 + + if n == 0: + return maxima + threshold = signal[minima[0]] threshold += sqrt(threshold) new_signal = signal[0:minima[0]] - threshold - sqrt(threshold) -# assert maxima[0] < minima[0], '%d > %d' % ( maxima[0], minima[0] ) + if is_valid_peak(new_signal, maxima[0]): peaky_maxima[0] = maxima[0] j += 1 @@ -103,7 +119,7 @@ if is_valid_peak(new_signal, new_maximum): peaky_maxima[j] = maxima[i + 1] j += 1 - threshold = signal[minima[-1]] + threshold = signal[minima[-1]] threshold += sqrt(threshold) new_signal = signal[minima[-1]:] - threshold new_maximum = maxima[-1] - minima[-1] @@ -113,11 +129,14 @@ peaky_maxima.resize(j, refcheck=False) return peaky_maxima + # hardcoded minimum peak width = 50 -cdef bool is_valid_peak(np.ndarray[float32_t, ndim=1] signal, int maximum): - cdef: - np.ndarray s - int32_t length +@cython.cfunc +def is_valid_peak(signal: cnp.ndarray(cython.float, ndim=1), + maximum: cython.int) -> bool: + s: cnp.ndarray + length: cython.int + s = hard_clip(signal, maximum) length = s.shape[0] if length < 50: @@ -126,69 +145,84 @@ return False return True + # require at least 6 different float values -- prevents broad flat peaks -cdef bool too_flat(np.ndarray[float32_t, ndim=1] signal): +@cython.cfunc +def too_flat(signal: cnp.ndarray(cython.float, ndim=1)) -> bool: """return whether signal has at least 6 unique values """ return np.unique(signal).shape[0] < 6 + # hard clip a region with negative values -cdef np.ndarray[float32_t, ndim=1] hard_clip(np.ndarray[float32_t, ndim=1] signal, int32_t maximum): +@cython.cfunc +def hard_clip(signal: cnp.ndarray(cython.float, ndim=1), + maximum: cython.int) -> cnp.ndarray: """clip the signal in both directions at the nearest values <= 0 to position maximum """ - cdef: - int32_t i - int32_t left = 0 - int32_t right = signal.shape[0] + i: cython.int + left: cython.int = 0 + right: cython.int = signal.shape[0] + # clip left - for i in range( right - maximum, 0 ): - if signal[ -i ] < 0: + for i in range(right - maximum, 0): + if signal[-i] < 0: left = i break for i in range(maximum, right): if signal[i] < 0: right = i break - return signal[ left:right ] + return signal[left:right] + -cpdef np.ndarray[ int32_t, ndim=1 ] enforce_valleys(np.ndarray[ float32_t, ndim=1 ] signal, - np.ndarray[ int32_t, ndim=1 ] summits, - float32_t min_valley = 0.8 ): +@cython.ccall +def enforce_valleys(signal: cnp.ndarray(cython.float, ndim=1), + summits: cnp.ndarray(cython.int, ndim=1), + min_valley: cython.float = 0.8) -> cnp.ndarray: """require a value of <= min_valley * lower summit between each pair of summits """ - cdef: - float32_t req_min, v, prev_v - int32_t summit_pos, prev_summit_pos - int32_t n_summits - int32_t n_valid_summits - np.ndarray[ int32_t, ndim=1 ] valid_summits + req_min: cython.float + v: cython.float + prev_v: cython.float + + summit_pos: cython.int + prev_summit_pos: cython.int + n_summits: cython.int + n_valid_summits: cython.int + + valid_summits: cnp.ndarray(cython.int, ndim=1) + n_summits = summits.shape[0] - n_valid_summits = 1 - valid_summits = summits.copy() + n_valid_summits = 1 + valid_summits = summits.copy() # Remove peaks that do not have sufficient valleys - if n_summits == 1: return summits - for i in range( 1, n_summits ): - prev_summit_pos = valid_summits[ n_valid_summits-1 ] - summit_pos = summits[ i ] - prev_v = signal[ prev_summit_pos ] - v = signal[ summit_pos ] - req_min = min_valley * min( prev_v, v ) - if ( signal[ prev_summit_pos:summit_pos ] < req_min ).any(): - valid_summits[ n_valid_summits ] = summit_pos + if n_summits == 1: + return summits + for i in range(1, n_summits): + prev_summit_pos = valid_summits[n_valid_summits-1] + summit_pos = summits[i] + prev_v = signal[prev_summit_pos] + v = signal[summit_pos] + req_min = min_valley * min(prev_v, v) + if (signal[prev_summit_pos:summit_pos] < req_min).any(): + valid_summits[n_valid_summits] = summit_pos n_valid_summits += 1 elif v > prev_v: - valid_summits[ n_valid_summits-1 ] = summit_pos - valid_summits.resize( n_valid_summits, refcheck=False ) + valid_summits[n_valid_summits-1] = summit_pos + valid_summits.resize(n_valid_summits, refcheck=False) return valid_summits + # Modified from http://www.scipy.org/Cookbook/SavitzkyGolay # positive window_size not enforced anymore # needs sane input paramters, window size > 4 # switched to double precision for internal accuracy -cpdef np.ndarray[float64_t, ndim=1] savitzky_golay_order2_deriv1(np.ndarray[float32_t, ndim=1] signal, - int32_t window_size): +@cython.ccall +def savitzky_golay_order2_deriv1(signal: cnp.ndarray(cython.float, ndim=1), + window_size: cython.int) -> cnp.ndarray: """Smooth (and optionally differentiate) data with a Savitzky-Golay filter. The Savitzky-Golay filter removes high frequency noise from data. It has the advantage of preserving the original shape and @@ -223,31 +257,40 @@ W.H. Press, S.A. Teukolsky, W.T. Vetterling, B.P. Flannery Cambridge University Press ISBN-13: 9780521880688 """ - cdef: - int32_t half_window, k - np.ndarray[int64_t, ndim=2] b - # pad the signal at the extremes with - # values taken from the signal itself - np.ndarray[float32_t, ndim=1] firstvals, lastvals - np.ndarray[float64_t, ndim=1] m, ret - - if window_size % 2 != 1: window_size += 1 + half_window: cython.int + b: cnp.ndarray(cython.long, ndim=2) + # pad the signal at the extremes with + # values taken from the signal itself + firstvals: cnp.ndarray(cython.float, ndim=1) + lastvals: cnp.ndarray(cython.float, ndim=1) + m: cnp.ndarray(cython.double, ndim=1) + ret: cnp.ndarray(cython.double, ndim=1) + + if window_size % 2 != 1: + window_size += 1 half_window = (window_size - 1) // 2 # precompute coefficients b = np.array([[1, k, k**2] for k in range(-half_window, half_window+1)], - dtype='int64') + dtype='i8') m = np.linalg.pinv(b)[1] # pad the signal at the extremes with # values taken from the signal itself firstvals = signal[0] - np.abs(signal[1:half_window+1][::-1] - signal[0]) lastvals = signal[-1] + np.abs(signal[-half_window-1:-1][::-1] - signal[-1]) signal = np.concatenate((firstvals, signal, lastvals)) - ret = np.convolve( m[::-1], signal.astype("float64"), mode='valid') #.astype("float32").round(8) # round to 8 decimals to avoid signing issue + ret = np.convolve(m[::-1], + signal.astype("f8"), + mode='valid') return ret + # Another modified version from http://www.scipy.org/Cookbook/SavitzkyGolay -cpdef np.ndarray[float32_t, ndim=1] savitzky_golay( np.ndarray[float32_t, ndim=1] y, int32_t window_size, - int32_t order, int32_t deriv = 0, int32_t rate = 1 ): +@cython.ccall +def savitzky_golay(y: cnp.ndarray(cython.float, ndim=1), + window_size: cython.int, + order: cython.int, + deriv: cython.int = 0, + rate: cython.int = 1) -> cnp.ndarray: """Smooth (and optionally differentiate) data with a Savitzky-Golay filter. The Savitzky-Golay filter removes high frequency noise from data. It has the advantage of preserving the original shape and @@ -278,7 +321,7 @@ Examples -------- t = np.linspace(-4, 4, 500) - y = np.exp( -t**2 ) + np.random.normal(0, 0.05, t.shape) + y = np.exp(-t**2) + np.random.normal(0, 0.05, t.shape) ysg = savitzky_golay(y, window_size=31, order=4) import matplotlib.pyplot as plt plt.plot(t, y, label='Noisy signal') @@ -295,31 +338,34 @@ W.H. Press, S.A. Teukolsky, W.T. Vetterling, B.P. Flannery Cambridge University Press ISBN-13: 9780521880688 """ - cdef: - int32_t half_window, k - np.ndarray[int64_t, ndim=2] b - # pad the signal at the extremes with - # values taken from the signal itself - np.ndarray[float32_t, ndim=1] firstvals, lastvals, ret - np.ndarray[float64_t, ndim=1] m + half_window: cython.int + b: cnp.ndarray(cython.long, ndim=2) + # pad the signal at the extremes with + # values taken from the signal itself + firstvals: cnp.ndarray(cython.float, ndim=1) + lastvals: cnp.ndarray(cython.float, ndim=1) + ret: cnp.ndarray(cython.float, ndim=1) + m: cnp.ndarray(cython.double, ndim=1) try: - window_size = np.abs( np.int( window_size ) ) - order = np.abs( np.int( order ) ) - except ValueError, msg: + window_size = np.abs(np.int(window_size)) + order = np.abs(np.int(order)) + except ValueError: raise ValueError("window_size and order have to be of type int") if window_size % 2 != 1 or window_size < 1: raise TypeError("window_size size must be a positive odd number") if window_size < order + 2: raise TypeError("window_size is too small for the polynomials order") - half_window = ( window_size -1 ) // 2 + half_window = (window_size - 1) // 2 # precompute coefficients - b = np.array( [ [ k**i for i in range( order + 1 ) ] for k in range( -half_window, half_window+1 ) ] ) - m = np.linalg.pinv( b )[ deriv ] * rate**deriv * mathfactorial( deriv ) + b = np.array([[k**i + for i in range(order + 1)] + for k in range(-half_window, half_window+1)]) + m = np.linalg.pinv(b)[deriv] * rate**deriv * mathfactorial(deriv) # pad the signal at the extremes with # values taken from the signal itself - firstvals = y[ 0 ] - np.abs( y[ 1:half_window + 1 ][ ::-1 ] - y[ 0 ] ) - lastvals = y[ -1 ] + np.abs( y[ -half_window - 1:-1 ][ ::-1 ] - y[ -1 ]) - y = np.concatenate( ( firstvals, y, lastvals ) ) - ret = np.convolve( m[ ::-1 ], y, mode = 'valid' ).astype("float32") + firstvals = y[0] - np.abs(y[1:half_window + 1][::-1] - y[0]) + lastvals = y[-1] + np.abs(y[-half_window - 1:-1][::-1] - y[-1]) + y = np.concatenate((firstvals, y, lastvals)) + ret = np.convolve(m[::-1], y, mode='valid').astype("float32") return ret diff --git a/setup.py b/setup.py index cce3c579..ec4d3735 100644 --- a/setup.py +++ b/setup.py @@ -105,14 +105,14 @@ def main(): include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.PeakModel", - ["MACS3/Signal/PeakModel.pyx"], + ["MACS3/Signal/PeakModel.py"], include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.PeakDetect", - ["MACS3/Signal/PeakDetect.pyx"], + ["MACS3/Signal/PeakDetect.py"], extra_compile_args=extra_c_args), Extension("MACS3.Signal.SignalProcessing", - ["MACS3/Signal/SignalProcessing.pyx"], + ["MACS3/Signal/SignalProcessing.py"], include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.FixWidthTrack", diff --git a/test/test_PairedEndTrack.py b/test/test_PairedEndTrack.py index 867623a0..baa5b7b1 100644 --- a/test/test_PairedEndTrack.py +++ b/test/test_PairedEndTrack.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Time-stamp: <2024-10-14 21:55:05 Tao Liu> +# Time-stamp: <2024-10-15 09:23:38 Tao Liu> import unittest from MACS3.Signal.PairedEndTrack import PETrackI, PETrackII @@ -93,6 +93,7 @@ def setUp(self): ] self.pileup_p = np.array([10, 50, 70, 80, 85, 100, 110, 160, 170, 180, 190], dtype="i4") self.pileup_v = np.array([3.0, 4.0, 6.0, 9.0, 11.0, 15.0, 19.0, 18.0, 16.0, 10.0, 6.0], dtype="f4") + self.subset_barcodes = {b'0w#AAACGAACAAGTAACA', b"0w#AAACGAACAAGTAAGA"} self.subset_pileup_p = np.array([10, 50, 70, 80, 85, 100, 110, 160, 170, 180, 190], dtype="i4") self.subset_pileup_v = np.array([1.0, 2.0, 4.0, 6.0, 7.0, 8.0, 13.0, 12.0, 10.0, 5.0, 4.0], dtype="f4") self.t = sum([(x[2]-x[1]) * x[4] for x in self.input_regions]) @@ -107,7 +108,7 @@ def test_add_frag(self): self.assertEqual(pe.length, self.t) # subset - pe_subset = pe.subset({b'0w#AAACGAACAAGTAACA', b"0w#AAACGAACAAGTAAGA"}) + pe_subset = pe.subset(self.subset_barcodes) # roughly check the numbers... self.assertEqual(pe_subset.total, 14) self.assertEqual(pe_subset.length, 1305) @@ -122,7 +123,7 @@ def test_pileup(self): np.testing.assert_array_equal(d[0], self.pileup_p) np.testing.assert_array_equal(d[1], self.pileup_v) - pe_subset = pe.subset({b'0w#AAACGAACAAGTAACA', b"0w#AAACGAACAAGTAAGA"}) + pe_subset = pe.subset(self.subset_barcodes) bdg = pe_subset.pileup_bdg() d = bdg.get_data_by_chr(b'chrY') # (p, v) of ndarray np.testing.assert_array_equal(d[0], self.subset_pileup_p) From 086aad0f9a2b6fdfb557489b4e648292031aa2c6 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Fri, 18 Oct 2024 16:21:18 -0400 Subject: [PATCH 06/13] rewrite scoretrack.py --- MACS3/Signal/ScoreTrack.py | 1852 +++++++++++++++++++++++++++++++++++ MACS3/Signal/ScoreTrack.pyx | 1483 ---------------------------- 2 files changed, 1852 insertions(+), 1483 deletions(-) create mode 100644 MACS3/Signal/ScoreTrack.py delete mode 100644 MACS3/Signal/ScoreTrack.pyx diff --git a/MACS3/Signal/ScoreTrack.py b/MACS3/Signal/ScoreTrack.py new file mode 100644 index 00000000..d67349a4 --- /dev/null +++ b/MACS3/Signal/ScoreTrack.py @@ -0,0 +1,1852 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-18 15:22:06 Tao Liu> + +"""Module for Feature IO classes. + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file LICENSE included with +the distribution). +""" + +# ------------------------------------ +# python modules +# ------------------------------------ +from functools import reduce + +# ------------------------------------ +# MACS3 modules +# ------------------------------------ +from MACS3.Signal.SignalProcessing import maxima, enforce_peakyness +from MACS3.Signal.Prob import poisson_cdf +from MACS3.IO.PeakIO import PeakIO, BroadPeakIO + +# ------------------------------------ +# Other modules +# ------------------------------------ +import cython +import numpy as np +import cython.cimports.numpy as cnp +from cython.cimports.cpython import bool +from cykhash import PyObjectMap, Float32to32Map + +# ------------------------------------ +# C lib +# ------------------------------------ +from cython.cimports.libc.math import (log10, + log) + +# ------------------------------------ +# constants +# ------------------------------------ + +# ------------------------------------ +# Misc functions +# ------------------------------------ + + +@cython.inline +@cython.cfunc +def int_max(a: cython.int, b: cython.int) -> cython.int: + return a if a >= b else b + + +@cython.inline +@cython.cfunc +def int_min(a: cython.int, b: cython.int) -> cython.int: + return a if a <= b else b + + +LOG10_E: cython.float = 0.43429448190325176 + +pscore_dict = PyObjectMap() + + +@cython.cfunc +def get_pscore(observed: cython.int, + expectation: cython.float) -> cython.float: + """Get p-value score from Poisson test. First check existing + table, if failed, call poisson_cdf function, then store the result + in table. + + """ + score: cython.double + + try: + return pscore_dict[(observed, expectation)] + except KeyError: + score = -1 * poisson_cdf(observed, + expectation, + False, + True) + pscore_dict[(observed, expectation)] = score + return score + + +asym_logLR_dict = PyObjectMap() + + +@cython.cfunc +def logLR_asym(x: cython.float, + y: cython.float) -> cython.float: + """Calculate log10 Likelihood between H1 (enriched) and H0 ( + chromatin bias). Set minus sign for depletion. + + *asymmetric version* + + """ + s: cython.float + + if (x, y) in asym_logLR_dict: + return asym_logLR_dict[(x, y)] + else: + if x > y: + s = (x*(log(x)-log(y))+y-x)*LOG10_E + elif x < y: + s = (x*(-log(x)+log(y))-y+x)*LOG10_E + else: + s = 0 + asym_logLR_dict[(x, y)] = s + return s + + +sym_logLR_dict = PyObjectMap() + + +@cython.cfunc +def logLR_sym(x: cython.float, y: cython.float) -> cython.float: + """Calculate log10 Likelihood between H1 (enriched) and H0 ( + another enriched). Set minus sign for H0>H1. + + * symmetric version * + + """ + s: cython.float + + if (x, y) in sym_logLR_dict: + return sym_logLR_dict[(x, y)] + else: + if x > y: + s = (x*(log(x)-log(y))+y-x)*LOG10_E + elif y > x: + s = (y*(log(x)-log(y))+y-x)*LOG10_E + else: + s = 0 + sym_logLR_dict[(x, y)] = s + return s + + +@cython.inline +@cython.cfunc +def get_logFE(x: cython.float, y: cython.float) -> cython.float: + """ return 100* log10 fold enrichment with +1 pseudocount. + """ + return log10(x/y) + + +@cython.cfunc +def get_subtraction(x: cython.float, y: cython.float) -> cython.float: + """ return subtraction. + """ + return x - y + +# ------------------------------------ +# Classes +# ------------------------------------ + + +@cython.cclass +class ScoreTrackII: + """Class for a container to keep signals of each genomic position, + including 1. score, 2. treatment and 2. control pileup. + + It also contains scoring methods and call_peak functions. + """ + # dictionary for data of each chromosome + data: dict + # length of data array of each chromosome + datalength: dict + # whether trackline should be saved in bedGraph + trackline: bool + # seq depth in million of treatment + treat_edm: cython.float + # seq depth in million of control + ctrl_edm: cython.float + # method for calculating scores. + scoring_method: cython.char + # scale to control? scale to treatment? both scale to 1million reads? + normalization_method: cython.char + # the pseudocount used to calcuate logLR, FE or logFE + pseudocount: cython.float + # cutoff + cutoff: cython.float + # save pvalue<->length dictionary + pvalue_stat: dict + + def __init__(self, + treat_depth: cython.float, + ctrl_depth: cython.float, + pseudocount: cython.float = 1.0): + """Initialize. + + treat_depth and ctrl_depth are effective depth in million: + sequencing depth in million after + duplicates being filtered. If + treatment is scaled down to + control sample size, then this + should be control sample size in + million. And vice versa. + + pseudocount: a pseudocount used to calculate logLR, FE or + logFE. Please note this value will not be changed + with normalization method. So if you really want + to set pseudocount 1 per million reads, set it + after you normalize treat and control by million + reads by `change_normalizetion_method(ord('M'))`. + + """ + # for each chromosome, there is a l*4 matrix. First column: + # end position of a region; Second: treatment pileup; third: + # control pileup ; forth: score (can be p/q-value/likelihood + # ratio/fold-enrichment/subtraction depending on -c setting) + self.data = {} + + self.datalength = {} + self.trackline = False + self.treat_edm = treat_depth + self.ctrl_edm = ctrl_depth + + # scoring_method: p: -log10 pvalue; + # q: -log10 qvalue; + # l: log10 likelihood ratio (minus for depletion) + # f: log10 fold enrichment + # F: linear fold enrichment + # d: subtraction + # m: fragment pileup per million reads + # N: not set + self.scoring_method = ord("N") + + # normalization_method: T: scale to depth of treatment; + # C: scale to depth of control; + # M: scale to depth of 1 million; + # N: not set/ raw pileup + self.normalization_method = ord("N") + + self.pseudocount = pseudocount + self.pvalue_stat = {} + + @cython.ccall + def set_pseudocount(self, pseudocount: cython.float): + self.pseudocount = pseudocount + + @cython.ccall + def enable_trackline(self): + """Turn on trackline with bedgraph output + """ + self.trackline = True + + @cython.ccall + def add_chromosome(self, + chrom: bytes, + chrom_max_len: cython.int): + """ + chrom: chromosome name + chrom_max_len: maximum number of data points in this chromosome + + """ + if chrom not in self.data: + self.data[chrom] = [np.zeros(chrom_max_len, dtype="int32"), # pos + # pileup at each interval, in float32 format + np.zeros(chrom_max_len, dtype="float32"), + # control at each interval, in float32 format + np.zeros(chrom_max_len, dtype="float32"), + # score at each interval, in float32 format + np.zeros(chrom_max_len, dtype="float32")] + self.datalength[chrom] = 0 + + @cython.ccall + def add(self, + chromosome: bytes, + endpos: cython.int, + chip: cython.float, + control: cython.float): + """Add a chr-endpos-sample-control block into data + dictionary. + + chromosome: chromosome name in string + endpos : end position of each interval in integer + chip : ChIP pileup value of each interval in float + control : Control pileup value of each interval in float + + *Warning* Need to add regions continuously. + """ + i: cython.int + + i = self.datalength[chromosome] + c = self.data[chromosome] + c[0][i] = endpos + c[1][i] = chip + c[2][i] = control + self.datalength[chromosome] += 1 + + @cython.ccall + def finalize(self): + """ + Adjust array size of each chromosome. + + """ + chrom: bytes + ln: cython.int + + for chrom in sorted(self.data.keys()): + d = self.data[chrom] + ln = self.datalength[chrom] + d[0].resize(ln, refcheck=False) + d[1].resize(ln, refcheck=False) + d[2].resize(ln, refcheck=False) + d[3].resize(ln, refcheck=False) + return + + @cython.ccall + def get_data_by_chr(self, + chromosome: bytes): + """Return array of counts by chromosome. + + The return value is a tuple: + ([end pos],[value]) + """ + if chromosome in self.data: + return self.data[chromosome] + else: + return None + + @cython.ccall + def get_chr_names(self): + """Return all the chromosome names stored. + + """ + return set(self.data.keys()) + + @cython.ccall + def change_normalization_method(self, + normalization_method: cython.char): + """Change/set normalization method. However, I do not + recommend change this back and forward, since some precision + issue will happen -- I only keep two digits. + + normalization_method: T: scale to depth of treatment; + C: scale to depth of control; + M: scale to depth of 1 million; + N: not set/ raw pileup + """ + if normalization_method == ord('T'): + if self.normalization_method == ord('T'): # do nothing + pass + elif self.normalization_method == ord('C'): + self.normalize(self.treat_edm/self.ctrl_edm, + self.treat_edm/self.ctrl_edm) + elif self.normalization_method == ord('M'): + self.normalize(self.treat_edm, self.treat_edm) + elif self.normalization_method == ord('N'): + self.normalize(1, self.treat_edm/self.ctrl_edm) + else: + raise NotImplementedError + self.normalization_method = ord('T') + elif normalization_method == ord('C'): + if self.normalization_method == ord('T'): + self.normalize(self.ctrl_edm/self.treat_edm, + self.ctrl_edm/self.treat_edm) + elif self.normalization_method == ord('C'): # do nothing + pass + elif self.normalization_method == ord('M'): + self.normalize(self.ctrl_edm, self.ctrl_edm) + elif self.normalization_method == ord('N'): + self.normalize(self.ctrl_edm/self.treat_edm, 1) + else: + raise NotImplementedError + self.normalization_method = ord('C') + elif normalization_method == ord('M'): + if self.normalization_method == ord('T'): + self.normalize(1/self.treat_edm, + 1/self.treat_edm) + elif self.normalization_method == ord('C'): + self.normalize(1/self.ctrl_edm, + 1/self.ctrl_edm) + elif self.normalization_method == ord('M'): # do nothing + pass + elif self.normalization_method == ord('N'): + self.normalize(1/self.treat_edm, + 1/self.ctrl_edm) + else: + raise NotImplementedError + self.normalization_method = ord('M') + elif normalization_method == ord('N'): + if self.normalization_method == ord('T'): + self.normalize(self.treat_edm, + self.treat_edm) + elif self.normalization_method == ord('C'): + self.normalize(self.ctrl_edm, + self.ctrl_edm) + elif self.normalization_method == ord('M'): + self.normalize(self.treat_edm, + self.ctrl_edm) + elif self.normalization_method == ord('N'): # do nothing + pass + else: + raise NotImplementedError + self.normalization_method = ord('N') + + @cython.cfunc + def normalize(self, + treat_scale: cython.float, + control_scale: cython.float): + p: cnp.ndarray + c: cnp.ndarray + ln: cython.long + i: cython.long + + for chrom in sorted(self.data.keys()): + p = self.data[chrom][1] + c = self.data[chrom][2] + ln = self.datalength[chrom] + for i in range(ln): + p[i] *= treat_scale + c[i] *= control_scale + return + + @cython.ccall + def change_score_method(self, + scoring_method: cython.char): + """ + scoring_method: p: -log10 pvalue; + q: -log10 qvalue; + l: log10 likelihood ratio (minus for depletion) + s: symmetric log10 likelihood ratio (for comparing two + ChIPs) + f: log10 fold enrichment + F: linear fold enrichment + d: subtraction + M: maximum + m: fragment pileup per million reads + """ + if scoring_method == ord('p'): + self.compute_pvalue() + elif scoring_method == ord('q'): + # if not already calculated p, compute pvalue first + if self.scoring_method != ord('p'): + self.compute_pvalue() + self.compute_qvalue() + elif scoring_method == ord('l'): + self.compute_likelihood() + elif scoring_method == ord('s'): + self.compute_sym_likelihood() + elif scoring_method == ord('f'): + self.compute_logFE() + elif scoring_method == ord('F'): + self.compute_foldenrichment() + elif scoring_method == ord('d'): + self.compute_subtraction() + elif scoring_method == ord('m'): + self.compute_SPMR() + elif scoring_method == ord('M'): + self.compute_max() + else: + raise NotImplementedError + + @cython.cfunc + def compute_pvalue(self): + """Compute -log_{10}(pvalue) + """ + p: cnp.ndarray + c: cnp.ndarray + v: cnp.ndarray + pos: cnp.ndarray + ln: cython.long + i: cython.long + prev_pos: cython.long + chrom: bytes + + for chrom in sorted(self.data.keys()): + prev_pos = 0 + pos = self.data[chrom][0] + p = self.data[chrom][1] + c = self.data[chrom][2] + v = self.data[chrom][3] + ln = self.datalength[chrom] + for i in range(ln): + v[i] = get_pscore(cython.cast(cython.int, + (p[i] + self.pseudocount)), + c[i] + self.pseudocount) + try: + self.pvalue_stat[v[i]] += pos[i] - prev_pos + except Exception: + self.pvalue_stat[v[i]] = pos[i] - prev_pos + prev_pos = pos[i] + + self.scoring_method = ord('p') + return + + @cython.cfunc + def compute_qvalue(self): + """Compute -log_{10}(qvalue) + """ + pqtable: object + i: cython.long + ln: cython.long + chrom: bytes + v: cnp.ndarray + + # pvalue should be computed first! + assert self.scoring_method == ord('p') + # make pqtable + pqtable = self.make_pq_table() + + # convert p to q + for chrom in sorted(self.data.keys()): + v = self.data[chrom][3] + ln = self.datalength[chrom] + for i in range(ln): + v[i] = pqtable[v[i]] + + self.scoring_method = ord('q') + return + + @cython.ccall + def make_pq_table(self): + """Make pvalue-qvalue table. + + Step1: get all pvalue and length of block with this pvalue + Step2: Sort them + Step3: Apply AFDR method to adjust pvalue and get qvalue for + each pvalue + + Return a dictionary of + {-log10pvalue:(-log10qvalue,rank,basepairs)} relationships. + + """ + ln: cython.long + i: cython.long + j: cython.long + v: cython.float + q: cython.float + pre_q: cython.float # store the p and q scores + N: cython.long + k: cython.float + f: cython.float + pvalue2qvalue: object + pvalue_stat: dict + unique_values: list + + assert self.scoring_method == ord('p') + + pvalue_stat = self.pvalue_stat + + N = sum(pvalue_stat.values()) + k = 1 # rank + f = -log10(N) + pre_q = 2147483647 # save the previous q-value + + pvalue2qvalue = Float32to32Map(for_int=False) + unique_values = sorted(list(pvalue_stat.keys()), reverse=True) + for i in range(len(unique_values)): + v = unique_values[i] + ln = pvalue_stat[v] + q = v + (log10(k) + f) + if q > pre_q: + q = pre_q + if q <= 0: + q = 0 + break + pvalue2qvalue[v] = q + pre_q = q + k += ln + # bottom rank pscores all have qscores 0 + for j in range(i, len(unique_values)): + v = unique_values[j] + pvalue2qvalue[v] = 0 + return pvalue2qvalue + + @cython.cfunc + def compute_likelihood(self): + """Calculate log10 likelihood. + + """ + ln: cython.long + i: cython.long + chrom: bytes + v1: cython.float + v2: cython.float + pseudocount: cython.float + + pseudocount = self.pseudocount + + for chrom in sorted(self.data.keys()): + p = self.data[chrom][1].flat.__next__ # pileup in treatment + c = self.data[chrom][2].flat.__next__ # pileup in control + v = self.data[chrom][3] # score + ln = self.datalength[chrom] + v1 = 2 + v2 = 1 + for i in range(ln): + v1 = p() + v2 = c() + v[i] = logLR_asym(v1 + pseudocount, v2 + pseudocount) + self.scoring_method = ord('l') + return + + @cython.cfunc + def compute_sym_likelihood(self): + """Calculate symmetric log10 likelihood. + + """ + ln: cython.long + i: cython.long + chrom: bytes + v1: cython.float + v2: cython.float + pseudocount: cython.float + + pseudocount = self.pseudocount + + for chrom in sorted(self.data.keys()): + p = self.data[chrom][1].flat.__next__ + c = self.data[chrom][2].flat.__next__ + v = self.data[chrom][3] + ln = self.datalength[chrom] + v1 = 2 + v2 = 1 + for i in range(ln): + v1 = p() + v2 = c() + v[i] = logLR_sym(v1 + pseudocount, v2 + pseudocount) + self.scoring_method = ord('s') + return + + @cython.cfunc + def compute_logFE(self): + """Calculate log10 fold enrichment (with 1 pseudocount). + + """ + p: cnp.ndarray + c: cnp.ndarray + v: cnp.ndarray + ln: cython.long + i: cython.long + pseudocount: cython.float + + pseudocount = self.pseudocount + + for chrom in sorted(self.data.keys()): + p = self.data[chrom][1] + c = self.data[chrom][2] + v = self.data[chrom][3] + ln = self.datalength[chrom] + for i in range(ln): + v[i] = get_logFE(p[i] + pseudocount, c[i] + pseudocount) + self.scoring_method = ord('f') + return + + @cython.cfunc + def compute_foldenrichment(self): + """Calculate linear scale fold enrichment (with 1 pseudocount). + + """ + p: cnp.ndarray + c: cnp.ndarray + v: cnp.ndarray + ln: cython.long + i: cython.long + pseudocount: cython.float + + pseudocount = self.pseudocount + + for chrom in sorted(self.data.keys()): + p = self.data[chrom][1] + c = self.data[chrom][2] + v = self.data[chrom][3] + ln = self.datalength[chrom] + for i in range(ln): + v[i] = (p[i] + pseudocount)/(c[i] + pseudocount) + self.scoring_method = ord('F') + return + + @cython.cfunc + def compute_subtraction(self): + p: cnp.ndarray + c: cnp.ndarray + v: cnp.ndarray + ln: cython.long + i: cython.long + + for chrom in sorted(self.data.keys()): + p = self.data[chrom][1] + c = self.data[chrom][2] + v = self.data[chrom][3] + ln = self.datalength[chrom] + for i in range(ln): + v[i] = p[i] - c[i] + self.scoring_method = ord('d') + return + + @cython.cfunc + def compute_SPMR(self): + p: cnp.ndarray + v: cnp.ndarray + ln: cython.long + i: cython.long + scale: cython.float + + if self.normalization_method == ord('T') or self.normalization_method == ord('N'): + scale = self.treat_edm + elif self.normalization_method == ord('C'): + scale = self.ctrl_edm + elif self.normalization_method == ord('M'): + scale = 1 + + for chrom in sorted(self.data.keys()): + p = self.data[chrom][1] + v = self.data[chrom][3] + ln = self.datalength[chrom] + for i in range(ln): + v[i] = p[i] / scale # two digit precision may not be enough... + self.scoring_method = ord('m') + return + + @cython.cfunc + def compute_max(self): + p: cnp.ndarray + c: cnp.ndarray + v: cnp.ndarray + ln: cython.long + i: cython.long + + for chrom in sorted(self.data.keys()): + p = self.data[chrom][1] + c = self.data[chrom][2] + v = self.data[chrom][3] + ln = self.datalength[chrom] + for i in range(ln): + v[i] = max(p[i], c[i]) + self.scoring_method = ord('M') + return + + @cython.ccall + def write_bedGraph(self, + fhd, + name: str, + description: str, + column: cython.short = 3): + """Write all data to fhd in bedGraph Format. + + fhd: a filehandler to save bedGraph. + + name/description: the name and description in track line. + + colname: can be 1: chip, 2: control, 3: score + + """ + chrom: bytes + ln: cython.int + pre: cython.int + i: cython.int + p: cython.int + pre_v: cython.float + v: cython.float + chrs: set + pos: cnp.ndarray + value: cnp.ndarray + + assert column in range(1, 4), "column should be between 1, 2 or 3." + + write = fhd.write + + if self.trackline: + # this line is REQUIRED by the wiggle format for UCSC browser + write("track type=bedGraph name=\"%s\" description=\"%s\"\n" % + (name.decode(), description)) + + chrs = self.get_chr_names() + for chrom in sorted(chrs): + pos = self.data[chrom][0] + value = self.data[chrom][column] + ln = self.datalength[chrom] + pre = 0 + if pos.shape[0] == 0: + continue # skip if there's no data + pre_v = value[0] + for i in range(1, ln): + v = value[i] + p = pos[i-1] + if abs(pre_v - v) > 1e-5: # precision is 5 digits + write("%s\t%d\t%d\t%.5f\n" % + (chrom.decode(), pre, p, pre_v)) + pre_v = v + pre = p + p = pos[-1] + # last one + write("%s\t%d\t%d\t%.5f\n" % + (chrom.decode(), pre, p, pre_v)) + + return True + + @cython.ccall + def call_peaks(self, + cutoff: cython.float = 5.0, + min_length: cython.int = 200, + max_gap: cython.int = 50, + call_summits: bool = False): + """This function try to find regions within which, scores + are continuously higher than a given cutoff. + + This function is NOT using sliding-windows. Instead, any + regions in bedGraph above certain cutoff will be detected, + then merged if the gap between nearby two regions are below + max_gap. After this, peak is reported if its length is above + min_length. + + cutoff: cutoff of value, default 5. For -log10pvalue, it means 10^-5. + min_length : minimum peak length, default 200. + max_gap : maximum gap to merge nearby peaks, default 50. + call_summits: whether or not to call all summits (local maxima). + """ + i: cython.int + chrom: bytes + pos: cnp.ndarray + sample: cnp.ndarray + control: cnp.ndarray + value: cnp.ndarray + above_cutoff: cnp.ndarray + above_cutoff_v: cnp.ndarray + above_cutoff_endpos: cnp.ndarray + above_cutoff_startpos: cnp.ndarray + above_cutoff_sv: cnp.ndarray + peak_content: list + + chrs = self.get_chr_names() + peaks = PeakIO() # dictionary to save peaks + + self.cutoff = cutoff + for chrom in sorted(chrs): + peak_content = [] # to store points above cutoff + + pos = self.data[chrom][0] + sample = self.data[chrom][1] + # control = self.data[chrom][2] + value = self.data[chrom][3] + + # indices where score is above cutoff + above_cutoff = np.nonzero(value >= cutoff)[0] + # scores where score is above cutoff + above_cutoff_v = value[above_cutoff] + # end positions of regions where score is above cutoff + above_cutoff_endpos = pos[above_cutoff] + # start positions of regions where score is above cutoff + above_cutoff_startpos = pos[above_cutoff-1] + # sample pileup height where score is above cutoff + above_cutoff_sv = sample[above_cutoff] + if above_cutoff_v.size == 0: + # nothing above cutoff + continue + + if above_cutoff[0] == 0: + # first element > cutoff, fix the first point as + # 0. otherwise it would be the last item in + # data[chrom]['pos'] + above_cutoff_startpos[0] = 0 + + # first bit of region above cutoff + peak_content.append((above_cutoff_startpos[0], + above_cutoff_endpos[0], + above_cutoff_v[0], + above_cutoff_sv[0], + above_cutoff[0])) + for i in range(1, above_cutoff_startpos.size): + if above_cutoff_startpos[i] - peak_content[-1][1] <= max_gap: + # append + peak_content.append((above_cutoff_startpos[i], + above_cutoff_endpos[i], + above_cutoff_v[i], + above_cutoff_sv[i], + above_cutoff[i])) + else: + # close + if call_summits: + self.__close_peak2(peak_content, + peaks, + min_length, + chrom, + max_gap//2) + else: + self.__close_peak(peak_content, + peaks, + min_length, + chrom) + peak_content = [(above_cutoff_startpos[i], + above_cutoff_endpos[i], + above_cutoff_v[i], + above_cutoff_sv[i], + above_cutoff[i]),] + + # save the last peak + if not peak_content: + continue + else: + if call_summits: + self.__close_peak2(peak_content, + peaks, + min_length, + chrom, + max_gap//2) + else: + self.__close_peak(peak_content, + peaks, + min_length, + chrom) + + return peaks + + @cython.cfunc + def __close_peak(self, + peak_content: list, + peaks: object, + min_length: cython.int, + chrom: bytes) -> bool: + """Close the peak region, output peak boundaries, peak summit + and scores, then add the peak to peakIO object. + + In this function, we define the peak summit as the middle + point of the region with the highest score, in this peak. For + example, if the region of the highest score is from 100 to + 200, the summit is 150. If there are several regions of the + same 'highest score', we will first calculate the possible + summit for each such region, then pick a position close to the + middle index (= (len(highest_regions) + 1) / 2) of these + summits. For example, if there are three regions with the same + highest scores, [100,200], [300,400], [600,700], we will first + find the possible summits as 150, 350, and 650, and then pick + the middle index, the 2nd, of the three positions -- 350 as + the final summit. If there are four regions, we pick the 2nd + as well. + + peaks: a PeakIO object + + """ + summit_pos: cython.int + tstart: cython.int + tend: cython.int + summit_index: cython.int + i: cython.int + midindex: cython.int + summit_value: cython.float + tvalue: cython.float + tsummitvalue: cython.float + + peak_length = peak_content[-1][1] - peak_content[0][0] + if peak_length >= min_length: # if the peak is too small, reject it + tsummit = [] + summit_pos = 0 + summit_value = 0 + for i in range(len(peak_content)): + (tstart, tend, tvalue, tsummitvalue, tindex) = peak_content[i] + #for (tstart,tend,tvalue,tsummitvalue, tindex) in peak_content: + if not summit_value or summit_value < tsummitvalue: + tsummit = [(tend + tstart) / 2,] + tsummit_index = [tindex,] + summit_value = tsummitvalue + elif summit_value == tsummitvalue: + # remember continuous summit values + tsummit.append(int((tend + tstart) / 2)) + tsummit_index.append(tindex) + # the middle of all highest points in peak region is defined as summit + midindex = int((len(tsummit) + 1) / 2) - 1 + summit_pos = tsummit[midindex] + summit_index = tsummit_index[midindex] + if self.scoring_method == ord('q'): + qscore = self.data[chrom][3][summit_index] + else: + # if q value is not computed, use -1 + qscore = -1 + + peaks.add(chrom, + peak_content[0][0], + peak_content[-1][1], + summit=summit_pos, + peak_score=self.data[chrom][3][summit_index], + # should be the same as summit_value + pileup=self.data[chrom][1][summit_index], + pscore=get_pscore(self.data[chrom][1][summit_index], + self.data[chrom][2][summit_index]), + fold_change=(self.data[chrom][1][summit_index] + + self.pseudocount) / (self.data[chrom][2][summit_index] + + self.pseudocount), + qscore=qscore, + ) + # start a new peak + return True + + @cython.cfunc + def __close_peak2(self, + peak_content: list, + peaks: object, + min_length: cython.int, + chrom: bytes, + smoothlen: cython.int = 51, + min_valley: cython.float = 0.9) -> bool: + """Close the peak region, output peak boundaries, peak summit + and scores, then add the peak to peakIO object. + + In this function, we use signal processing methods to smooth + the scores in the peak region, find the maxima and enforce the + peaky shape, and to define the best maxima as the peak + summit. The functions used for signal processing is 'maxima' + (with 2nd order polynomial filter) and 'enfoce_peakyness' + functions in SignalProcessing.pyx. + + peaks: a PeakIO object + + """ + tstart: cython.int + tend: cython.int + tmpindex: cython.int + summit_index: cython.int + summit_offset: cython.int + start: cython.int + end: cython.int + i: cython.int + j: cython.int + start_boundary: cython.int + tvalue: cython.float + peakdata: cnp.ndarray(cython.float, ndim=1) + peakindices: cnp.ndarray(cython.int, ndim=1) + summit_offsets: cnp.ndarray(cython.int, ndim=1) + + # Add 10 bp padding to peak region so that we can get true minima + end = peak_content[-1][1] + 10 + start = peak_content[0][0] - 10 + if start < 0: + start_boundary = 10 + start + start = 0 + else: + start_boundary = 10 + peak_length = end - start + if end - start < min_length: + return # if the region is too small, reject it + + peakdata = np.zeros(end - start, dtype='f4') + peakindices = np.zeros(end - start, dtype='i4') + for (tstart, tend, tvalue, tsvalue, tmpindex) in peak_content: + i = tstart - start + start_boundary + j = tend - start + start_boundary + peakdata[i:j] = tsvalue + peakindices[i:j] = tmpindex + summit_offsets = maxima(peakdata, smoothlen) + if summit_offsets.shape[0] == 0: + # **failsafe** if no summits, fall back on old approach # + return self.__close_peak(peak_content, peaks, min_length, chrom) + else: + # remove maxima that occurred in padding + i = np.searchsorted(summit_offsets, + start_boundary) + j = np.searchsorted(summit_offsets, + peak_length + start_boundary, + 'right') + summit_offsets = summit_offsets[i:j] + + summit_offsets = enforce_peakyness(peakdata, summit_offsets) + if summit_offsets.shape[0] == 0: + # **failsafe** if no summits, fall back on old approach # + return self.__close_peak(peak_content, peaks, min_length, chrom) + + summit_indices = peakindices[summit_offsets] + summit_offsets -= start_boundary + + peak_scores = self.data[chrom][3][summit_indices] + if not (peak_scores > self.cutoff).all(): + return self.__close_peak(peak_content, peaks, min_length, chrom) + for summit_offset, summit_index in zip(summit_offsets, summit_indices): + if self.scoring_method == ord('q'): + qscore = self.data[chrom][3][summit_index] + else: + # if q value is not computed, use -1 + qscore = -1 + peaks.add(chrom, + start, + end, + summit=start + summit_offset, + peak_score=self.data[chrom][3][summit_index], + # should be the same as summit_value + pileup=self.data[chrom][1][summit_index], + pscore=get_pscore(self.data[chrom][1][summit_index], + self.data[chrom][2][summit_index]), + fold_change=(self.data[chrom][1][summit_index] + + self.pseudocount) / (self.data[chrom][2][summit_index] + + self.pseudocount), + qscore=qscore, + ) + # start a new peak + return True + + @cython.cfunc + def total(self) -> cython.long: + """Return the number of regions in this object. + + """ + t: cython.long + chrom: bytes + + t = 0 + for chrom in sorted(self.data.keys()): + t += self.datalength[chrom] + return t + + @cython.ccall + def call_broadpeaks(self, + lvl1_cutoff: cython.float = 5.0, + lvl2_cutoff: cython.float = 1.0, + min_length: cython.int = 200, + lvl1_max_gap: cython.int = 50, + lvl2_max_gap: cython.int = 400): + """This function try to find enriched regions within which, + scores are continuously higher than a given cutoff for level + 1, and link them using the gap above level 2 cutoff with a + maximum length of lvl2_max_gap. + + lvl1_cutoff: cutoff of value at enriched regions, default 5.0. + lvl2_cutoff: cutoff of value at linkage regions, default 1.0. + min_length : minimum peak length, default 200. + lvl1_max_gap : maximum gap to merge nearby enriched peaks, default 50. + lvl2_max_gap : maximum length of linkage regions, default 400. + + Return both general PeakIO object for highly enriched regions + and gapped broad regions in BroadPeakIO. + """ + i: cython.int + chrom: bytes + + assert lvl1_cutoff > lvl2_cutoff, "level 1 cutoff should be larger than level 2." + assert lvl1_max_gap < lvl2_max_gap, "level 2 maximum gap should be larger than level 1." + lvl1_peaks = self.call_peaks(cutoff=lvl1_cutoff, + min_length=min_length, + max_gap=lvl1_max_gap) + lvl2_peaks = self.call_peaks(cutoff=lvl2_cutoff, + min_length=min_length, + max_gap=lvl2_max_gap) + chrs = lvl1_peaks.peaks.keys() + broadpeaks = BroadPeakIO() + # use lvl2_peaks as linking regions between lvl1_peaks + for chrom in sorted(chrs): + lvl1peakschrom = lvl1_peaks.peaks[chrom] + lvl2peakschrom = lvl2_peaks.peaks[chrom] + lvl1peakschrom_next = iter(lvl1peakschrom).__next__ + tmppeakset = [] # to temporarily store lvl1 region inside a lvl2 region + # our assumption is lvl1 regions should be included in lvl2 regions + try: + lvl1 = lvl1peakschrom_next() + for i in range(len(lvl2peakschrom)): + # for each lvl2 peak, find all lvl1 peaks inside + # I assume lvl1 peaks can be ALL covered by lvl2 peaks. + lvl2 = lvl2peakschrom[i] + + while True: + if lvl2["start"] <= lvl1["start"] and lvl1["end"] <= lvl2["end"]: + tmppeakset.append(lvl1) + lvl1 = lvl1peakschrom_next() + else: + # make a hierarchical broad peak + #print lvl2["start"], lvl2["end"], lvl2["score"] + self.__add_broadpeak(broadpeaks, + chrom, + lvl2, + tmppeakset) + tmppeakset = [] + break + except StopIteration: + # no more strong (aka lvl1) peaks left + self.__add_broadpeak(broadpeaks, + chrom, + lvl2, + tmppeakset) + tmppeakset = [] + # add the rest lvl2 peaks + for j in range(i+1, len(lvl2peakschrom)): + self.__add_broadpeak(broadpeaks, + chrom, + lvl2peakschrom[j], + tmppeakset) + + return broadpeaks + + def __add_broadpeak(self, + bpeaks, + chrom: bytes, + lvl2peak: dict, + lvl1peakset: list): + """Internal function to create broad peak. + """ + + blockNum: cython.int + thickStart: cython.int + thickEnd: cython.int + start: cython.int + end: cython.int + blockSizes: bytes + blockStarts: bytes + + start = lvl2peak["start"] + end = lvl2peak["end"] + + # the following code will add those broad/lvl2 peaks with no strong/lvl1 peaks inside + if not lvl1peakset: + # will complement by adding 1bps start and end to this region + # may change in the future if gappedPeak format was improved. + bpeaks.add(chrom, + start, + end, + score=lvl2peak["score"], + thickStart=(b"%d" % start), + thickEnd=(b"%d" % end), + blockNum=2, + blockSizes=b"1,1", + blockStarts=(b"0,%d" % (end-start-1)), + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) + return bpeaks + + thickStart = b"%d" % lvl1peakset[0]["start"] + thickEnd = b"%d" % lvl1peakset[-1]["end"] + blockNum = int(len(lvl1peakset)) + blockSizes = b",".join([b"%d" % x["length"] for x in lvl1peakset]) + blockStarts = b",".join([b"%d" % (x["start"]-start) for x in lvl1peakset]) + + if lvl2peak["start"] != thickStart: + # add 1bp mark for the start of lvl2 peak + thickStart = b"%d" % start + blockNum += 1 + blockSizes = b"1,"+blockSizes + blockStarts = b"0,"+blockStarts + if lvl2peak["end"] != thickEnd: + # add 1bp mark for the end of lvl2 peak + thickEnd = b"%d" % end + blockNum += 1 + blockSizes = blockSizes+b",1" + blockStarts = blockStarts + b"," + (b"%d" % (end-start-1)) + + # add to BroadPeakIO object + bpeaks.add(chrom, + start, + end, + score=lvl2peak["score"], + thickStart=thickStart, + thickEnd=thickEnd, + blockNum=blockNum, + blockSizes=blockSizes, + blockStarts=blockStarts, + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) + return bpeaks + +@cython.cclass +class TwoConditionScores: + """Class for saving two condition comparison scores. + """ + # dictionary for data of each chromosome + data: dict + # length of data array of each chromosome + datalength: dict + # factor to apply to cond1 pileup values + cond1_factor: cython.float + # factor to apply to cond2 pileup values + cond2_factor: cython.float + # the pseudocount used to calcuate LLR + pseudocount: cython.float + cutoff: cython.float + t1bdg: object + c1bdg: object + t2bdg: object + c2bdg: object + pvalue_stat1: dict + pvalue_stat2: dict + pvalue_stat3: dict + + def __init__(self, + t1bdg, + c1bdg, + t2bdg, + c2bdg, + cond1_factor: cython.float = 1.0, + cond2_factor: cython.float = 1.0, + pseudocount: cython.float = 0.01, + proportion_background_empirical_distribution: cython.float = 0.99999): + """t1bdg: a bedGraphTrackI object for treat 1 + c1bdg: a bedGraphTrackI object for control 1 + t2bdg: a bedGraphTrackI object for treat 2 + c2bdg: a bedGraphTrackI object for control 2 + + cond1_factor: this will be multiplied to values in t1bdg and c1bdg + cond2_factor: this will be multiplied to values in t2bdg and c2bdg + + pseudocount: pseudocount, by default 0.01. + + proportion_background_empirical_distribution: proportion of + genome as the background to build empirical distribution + + """ + # for each chromosome, there is a l*4 matrix. First column: end + # position of a region; Second: treatment pileup; third: + # control pileup ; forth: score (can be p/q-value/likelihood + # ratio/fold-enrichment/subtraction depending on -c setting) + self.data = {} + self.datalength = {} + self.cond1_factor = cond1_factor + self.cond2_factor = cond2_factor + self.pseudocount = pseudocount + self.pvalue_stat1 = {} + self.pvalue_stat2 = {} + self.t1bdg = t1bdg + self.c1bdg = c1bdg + self.t2bdg = t2bdg + self.c2bdg = c2bdg + # self.empirical_distr_llr = [] # save all values in histogram + + @cython.ccall + def set_pseudocount(self, pseudocount: cython.float): + self.pseudocount = pseudocount + + @cython.ccall + def build(self): + """Compute scores from 3 types of comparisons and store them + in self.data. + + """ + common_chrs: set + chrname: bytes + chrom_max_len: cython.int + # common chromosome names + common_chrs = self.get_common_chrs() + for chrname in common_chrs: + (cond1_treat_ps, cond1_treat_vs) = self.t1bdg.get_data_by_chr(chrname) + (cond1_control_ps, cond1_control_vs) = self.c1bdg.get_data_by_chr(chrname) + (cond2_treat_ps, cond2_treat_vs) = self.t2bdg.get_data_by_chr(chrname) + (cond2_control_ps, cond2_control_vs) = self.c2bdg.get_data_by_chr(chrname) + chrom_max_len = len(cond1_treat_ps) + len(cond1_control_ps) + len(cond2_treat_ps) + len(cond2_control_ps) + self.add_chromosome(chrname, chrom_max_len) + self.build_chromosome(chrname, + cond1_treat_ps, cond1_control_ps, + cond2_treat_ps, cond2_control_ps, + cond1_treat_vs, cond1_control_vs, + cond2_treat_vs, cond2_control_vs) + + @cython.cfunc + def build_chromosome(self, chrname, + cond1_treat_ps, cond1_control_ps, + cond2_treat_ps, cond2_control_ps, + cond1_treat_vs, cond1_control_vs, + cond2_treat_vs, cond2_control_vs): + """Internal function to calculate scores for three types of comparisons. + + cond1_treat_ps, cond1_control_ps: position of treat and control of condition 1 + cond2_treat_ps, cond2_control_ps: position of treat and control of condition 2 + cond1_treat_vs, cond1_control_vs: value of treat and control of condition 1 + cond2_treat_vs, cond2_control_vs: value of treat and control of condition 2 + + """ + c1tp: cython.int + c1cp: cython.int + c2tp: cython.int + c2cp: cython.int + minp: cython.int + pre_p: cython.int + c1tv: cython.float + c1cv: cython.float + c2tv: cython.float + c2cv: cython.float + + c1tpn = iter(cond1_treat_ps).__next__ + c1cpn = iter(cond1_control_ps).__next__ + c2tpn = iter(cond2_treat_ps).__next__ + c2cpn = iter(cond2_control_ps).__next__ + c1tvn = iter(cond1_treat_vs).__next__ + c1cvn = iter(cond1_control_vs).__next__ + c2tvn = iter(cond2_treat_vs).__next__ + c2cvn = iter(cond2_control_vs).__next__ + + pre_p = 0 + + try: + c1tp = c1tpn() + c1tv = c1tvn() + + c1cp = c1cpn() + c1cv = c1cvn() + + c2tp = c2tpn() + c2tv = c2tvn() + + c2cp = c2cpn() + c2cv = c2cvn() + + while True: + minp = min(c1tp, c1cp, c2tp, c2cp) + self.add(chrname, pre_p, c1tv, c1cv, c2tv, c2cv) + pre_p = minp + if c1tp == minp: + c1tp = c1tpn() + c1tv = c1tvn() + if c1cp == minp: + c1cp = c1cpn() + c1cv = c1cvn() + if c2tp == minp: + c2tp = c2tpn() + c2tv = c2tvn() + if c2cp == minp: + c2cp = c2cpn() + c2cv = c2cvn() + except StopIteration: + # meet the end of either bedGraphTrackI, simply exit + pass + return + + @cython.cfunc + def get_common_chrs(self) -> set: + t1chrs: set + c1chrs: set + t2chrs: set + c2chrs: set + common: set + t1chrs = self.t1bdg.get_chr_names() + c1chrs = self.c1bdg.get_chr_names() + t2chrs = self.t2bdg.get_chr_names() + c2chrs = self.c2bdg.get_chr_names() + common = reduce(lambda x, y: x.intersection(y), + (t1chrs, c1chrs, t2chrs, c2chrs)) + return common + + @cython.cfunc + def add_chromosome(self, + chrom: bytes, + chrom_max_len: cython.int): + """ + chrom: chromosome name + chrom_max_len: maximum number of data points in this chromosome + + """ + if chrom not in self.data: + self.data[chrom] = [np.zeros(chrom_max_len, dtype="i4"), # pos + np.zeros(chrom_max_len, dtype="f4"), # LLR t1 vs c1 + np.zeros(chrom_max_len, dtype="f4"), # LLR t2 vs c2 + np.zeros(chrom_max_len, dtype="f4")] # LLR t1 vs t2 + self.datalength[chrom] = 0 + + @cython.cfunc + def add(self, + chromosome: bytes, + endpos: cython.int, + t1: cython.float, + c1: cython.float, + t2: cython.float, + c2: cython.float): + """Take chr-endpos-sample1-control1-sample2-control2 and + compute logLR for t1 vs c1, t2 vs c2, and t1 vs t2, then save + values. + + chromosome: chromosome name in string + endpos : end position of each interval in integer + t1 : Sample 1 ChIP pileup value of each interval in float + c1 : Sample 1 Control pileup value of each interval in float + t2 : Sample 2 ChIP pileup value of each interval in float + c2 : Sample 2 Control pileup value of each interval in float + + *Warning* Need to add regions continuously. + """ + i: cython.int + c: list + + i = self.datalength[chromosome] + c = self.data[chromosome] + c[0][i] = endpos + c[1][i] = logLR_asym((t1+self.pseudocount) * self.cond1_factor, + (c1+self.pseudocount) * self.cond1_factor) + c[2][i] = logLR_asym((t2+self.pseudocount) * self.cond2_factor, + (c2+self.pseudocount) * self.cond2_factor) + c[3][i] = logLR_sym((t1+self.pseudocount) * self.cond1_factor, + (t2+self.pseudocount) * self.cond2_factor) + self.datalength[chromosome] += 1 + return + + @cython.ccall + def finalize(self): + """ + Adjust array size of each chromosome. + + """ + chrom: bytes + ln: cython.int + d: list + + for chrom in sorted(self.data.keys()): + d = self.data[chrom] + ln = self.datalength[chrom] + d[0].resize(ln, refcheck=False) + d[1].resize(ln, refcheck=False) + d[2].resize(ln, refcheck=False) + d[3].resize(ln, refcheck=False) + return + + @cython.ccall + def get_data_by_chr(self, + chromosome: bytes): + """Return array of counts by chromosome. + + The return value is a tuple: + ([end pos],[value]) + """ + if chromosome in self.data: + return self.data[chromosome] + else: + return None + + @cython.ccall + def get_chr_names(self): + """Return all the chromosome names stored. + + """ + return set(self.data.keys()) + + @cython.ccall + def write_bedGraph(self, + fhd, + name: str, + description: str, + column: cython.int = 3): + """Write all data to fhd in bedGraph Format. + + fhd: a filehandler to save bedGraph. + + name/description: the name and description in track line. + + colname: can be 1: cond1 chip vs cond1 ctrl, 2: cond2 chip vs + cond2 ctrl, 3: cond1 chip vs cond2 chip + + """ + chrom: bytes + ln: cython.int + pre: cython.int + i: cython.int + p: cython.int + pre_v: cython.float + v: cython.float + pos: cnp.ndarray + value: cnp.ndarray + + assert column in range(1, 4), "column should be between 1, 2 or 3." + + write = fhd.write + + # if self.trackline: + # # this line is REQUIRED by the wiggle format for UCSC browser + # write("track type=bedGraph name=\"%s\" description=\"%s\"\n" % (name.decode(), description)) + + chrs = self.get_chr_names() + for chrom in sorted(chrs): + pos = self.data[chrom][0] + value = self.data[chrom][column] + ln = self.datalength[chrom] + pre = 0 + if pos.shape[0] == 0: + continue # skip if there's no data + pre_v = value[0] + for i in range(1, ln): + v = value[i] + p = pos[i-1] + if abs(pre_v - v) >= 1e-6: + write("%s\t%d\t%d\t%.5f\n" % + (chrom.decode(), pre, p, pre_v)) + pre_v = v + pre = p + p = pos[-1] + # last one + write("%s\t%d\t%d\t%.5f\n" % (chrom.decode(), pre, p, pre_v)) + + return True + + @cython.ccall + def write_matrix(self, + fhd, + name: str, + description: str): + """Write all data to fhd into five columns Format: + + col1: chr_start_end + col2: t1 vs c1 + col3: t2 vs c2 + col4: t1 vs t2 + + fhd: a filehandler to save the matrix. + + """ + chrom: bytes + ln: cython.int + pre: cython.int + i: cython.int + p: cython.int + v1: cython.float + v2: cython.float + v3: cython.float + pos: cnp.ndarray + value1: cnp.ndarray + value2: cnp.ndarray + value3: cnp.ndarray + + write = fhd.write + + chrs = self.get_chr_names() + for chrom in sorted(chrs): + [pos, value1, value2, value3] = self.data[chrom] + ln = self.datalength[chrom] + pre = 0 + if pos.shape[0] == 0: + continue # skip if there's no data + for i in range(0, ln): + v1 = value1[i] + v2 = value2[i] + v3 = value3[i] + p = pos[i] + write("%s:%d_%d\t%.5f\t%.5f\t%.5f\n" % + (chrom.decode(), pre, p, v1, v2, v3)) + pre = p + + return True + + @cython.ccall + def call_peaks(self, + cutoff: cython.float = 3, + min_length: cython.int = 200, + max_gap: cython.int = 100, + call_summits: bool = False) -> tuple: + """This function try to find regions within which, scores + are continuously higher than a given cutoff. + + For bdgdiff. + + This function is NOT using sliding-windows. Instead, any + regions in bedGraph above certain cutoff will be detected, + then merged if the gap between nearby two regions are below + max_gap. After this, peak is reported if its length is above + min_length. + + cutoff: cutoff of value, default 3. For log10 LR, it means 1000 or -1000. + min_length : minimum peak length, default 200. + max_gap : maximum gap to merge nearby peaks, default 100. + ptrack: an optional track for pileup heights. If it's not None, use it to find summits. Otherwise, use self/scoreTrack. + """ + chrom: bytes + pos: cnp.ndarray + t1_vs_c1: cnp.ndarray + t2_vs_c2: cnp.ndarray + t1_vs_t2: cnp.ndarray + cond1_over_cond2: cnp.ndarray + cond2_over_cond1: cnp.ndarray + cond1_equal_cond2: cnp.ndarray + cond1_sig: cnp.ndarray + cond2_sig: cnp.ndarray + cat1: cnp.ndarray + cat2: cnp.ndarray + cat3: cnp.ndarray + cat1_startpos: cnp.ndarray + cat1_endpos: cnp.ndarray + cat2_startpos: cnp.ndarray + cat2_endpos: cnp.ndarray + cat3_startpos: cnp.ndarray + cat3_endpos: cnp.ndarray + + chrs = self.get_chr_names() + cat1_peaks = PeakIO() # dictionary to save peaks significant at condition 1 + cat2_peaks = PeakIO() # dictionary to save peaks significant at condition 2 + cat3_peaks = PeakIO() # dictionary to save peaks significant in both conditions + + self.cutoff = cutoff + + for chrom in sorted(chrs): + pos = self.data[chrom][0] + t1_vs_c1 = self.data[chrom][1] + t2_vs_c2 = self.data[chrom][2] + t1_vs_t2 = self.data[chrom][3] + and_ = np.logical_and + # regions with stronger cond1 signals + cond1_over_cond2 = t1_vs_t2 >= cutoff + # regions with stronger cond2 signals + cond2_over_cond1 = t1_vs_t2 <= -1*cutoff + cond1_equal_cond2 = and_(t1_vs_t2 >= -1*cutoff, t1_vs_t2 <= cutoff) + # enriched regions in condition 1 + cond1_sig = t1_vs_c1 >= cutoff + # enriched regions in condition 2 + cond2_sig = t2_vs_c2 >= cutoff + # indices where score is above cutoff + # cond1 stronger than cond2, the indices + cat1 = np.where(and_(cond1_sig, cond1_over_cond2))[0] + # cond2 stronger than cond1, the indices + cat2 = np.where(and_(cond2_over_cond1, cond2_sig))[0] + # cond1 and cond2 are equal, the indices + cat3 = np.where(and_(and_(cond1_sig, cond2_sig), + cond1_equal_cond2))[0] + + # end positions of regions where score is above cutoff + cat1_endpos = pos[cat1] + # start positions of regions where score is above cutoff + cat1_startpos = pos[cat1-1] + # end positions of regions where score is above cutoff + cat2_endpos = pos[cat2] + # start positions of regions where score is above cutoff + cat2_startpos = pos[cat2-1] + # end positions of regions where score is above cutoff + cat3_endpos = pos[cat3] + # start positions of regions where score is above cutoff + cat3_startpos = pos[cat3-1] + + # for cat1: condition 1 stronger regions + self.__add_a_peak(cat1_peaks, + chrom, + cat1, + cat1_startpos, + cat1_endpos, + t1_vs_t2, + max_gap, + min_length) + # for cat2: condition 2 stronger regions + self.__add_a_peak(cat2_peaks, + chrom, + cat2, + cat2_startpos, + cat2_endpos, + -1 * t1_vs_t2, + max_gap, + min_length) + # for cat3: commonly strong regions + self.__add_a_peak(cat3_peaks, + chrom, + cat3, + cat3_startpos, + cat3_endpos, + abs(t1_vs_t2), + max_gap, + min_length) + + return (cat1_peaks, cat2_peaks, cat3_peaks) + + @cython.cfunc + def __add_a_peak(self, + peaks: object, + chrom: bytes, + indices: cnp.ndarray, + startpos: cnp.ndarray, + endpos: cnp.ndarray, + score: cnp.ndarray, + max_gap: cython.int, + min_length: cython.int): + + """For a given chromosome, merge nearby significant regions, + filter out smaller regions, then add regions to PeakIO + object. + + """ + i: cython.int + peak_content: list + mean_logLR: cython.float + + if startpos.size > 0: + # if it is not empty + peak_content = [] + if indices[0] == 0: + # first element > cutoff, fix the first point as + # 0. otherwise it would be the last item in + # data[chrom]['pos'] + startpos[0] = 0 + # first bit of region above cutoff + peak_content.append((startpos[0], + endpos[0], + score[indices[0]])) + for i in range(1, startpos.size): + if startpos[i] - peak_content[-1][1] <= max_gap: + # append + peak_content.append((startpos[i], + endpos[i], + score[indices[i]])) + else: + # close + if peak_content[-1][1] - peak_content[0][0] >= min_length: + mean_logLR = self.mean_from_peakcontent(peak_content) + # if peak_content[0][0] == 22414956: + # print(f"{peak_content} {mean_logLR}") + peaks.add(chrom, + peak_content[0][0], + peak_content[-1][1], + summit=-1, + peak_score=mean_logLR, + pileup=0, + pscore=0, + fold_change=0, + qscore=0, + ) + peak_content = [(startpos[i], + endpos[i], + score[indices[i]]),] + + # save the last peak + if peak_content: + if peak_content[-1][1] - peak_content[0][0] >= min_length: + mean_logLR = self.mean_from_peakcontent(peak_content) + peaks.add(chrom, + peak_content[0][0], + peak_content[-1][1], + summit=-1, + peak_score=mean_logLR, + pileup=0, + pscore=0, + fold_change=0, + qscore=0, + ) + + return + + @cython.cfunc + def mean_from_peakcontent(self, + peakcontent: list) -> cython.float: + """ + + """ + tmp_s: cython.int + tmp_e: cython.int + ln: cython.int + tmp_v: cython.long + sum_v: cython.long # for better precision + r: cython.float + i: cython.int + + ln = 0 + sum_v = 0 # initialize sum_v as 0 + for i in range(len(peakcontent)): + tmp_s = peakcontent[i][0] + tmp_e = peakcontent[i][1] + tmp_v = peakcontent[i][2] + sum_v += tmp_v * (tmp_e - tmp_s) + ln += tmp_e - tmp_s + + r = cython.cast(cython.float, (sum_v / ln)) + return r + + @cython.cfunc + def total(self) -> cython.long: + """Return the number of regions in this object. + + """ + t: cython.long + chrom: bytes + + t = 0 + for chrom in sorted(self.data.keys()): + t += self.datalength[chrom] + return t diff --git a/MACS3/Signal/ScoreTrack.pyx b/MACS3/Signal/ScoreTrack.pyx deleted file mode 100644 index 1ef3d31b..00000000 --- a/MACS3/Signal/ScoreTrack.pyx +++ /dev/null @@ -1,1483 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2024-10-10 16:45:13 Tao Liu> - -"""Module for Feature IO classes. - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file LICENSE included with -the distribution). -""" - -# ------------------------------------ -# python modules -# ------------------------------------ -from copy import copy -from functools import reduce - -# ------------------------------------ -# MACS3 modules -# ------------------------------------ -from MACS3.Signal.SignalProcessing import maxima, enforce_valleys, enforce_peakyness -from MACS3.Signal.Prob import poisson_cdf -from MACS3.IO.PeakIO import PeakIO, BroadPeakIO - -# ------------------------------------ -# Other modules -# ------------------------------------ -cimport cython -import numpy as np -cimport numpy as np -from numpy cimport uint8_t, uint16_t, uint32_t, uint64_t, int8_t, int16_t, int32_t, int64_t, float32_t, float64_t -from cpython cimport bool -from cykhash import PyObjectMap, Float32to32Map - -# ------------------------------------ -# C lib -# ------------------------------------ -from libc.math cimport log10,log, floor, ceil - -# ------------------------------------ -# constants -# ------------------------------------ -__version__ = "scoreTrack $Revision$" -__author__ = "Tao Liu " -__doc__ = "scoreTrack classes" - -# ------------------------------------ -# Misc functions -# ------------------------------------ -cdef inline int32_t int_max(int32_t a, int32_t b): return a if a >= b else b -cdef inline int32_t int_min(int32_t a, int32_t b): return a if a <= b else b - -LOG10_E = 0.43429448190325176 - -pscore_dict = PyObjectMap() - -cdef float32_t get_pscore ( int32_t observed, float32_t expectation ): - """Get p-value score from Poisson test. First check existing - table, if failed, call poisson_cdf function, then store the result - in table. - - """ - cdef: - float64_t score - - try: - return pscore_dict[(observed, expectation)] - except KeyError: - score = -1*poisson_cdf(observed,expectation,False,True) - pscore_dict[(observed, expectation)] = score - return score - -asym_logLR_dict = PyObjectMap() - -cdef float32_t logLR_asym ( float32_t x, float32_t y ): - """Calculate log10 Likelihood between H1 ( enriched ) and H0 ( - chromatin bias ). Set minus sign for depletion. - - *asymmetric version* - - """ - cdef: - float32_t s - - if (x,y) in asym_logLR_dict: - return asym_logLR_dict[ ( x, y ) ] - else: - if x > y: - s = (x*(log(x)-log(y))+y-x)*LOG10_E - elif x < y: - s = (x*(-log(x)+log(y))-y+x)*LOG10_E - else: - s = 0 - asym_logLR_dict[ ( x, y ) ] = s - return s - -sym_logLR_dict = PyObjectMap() - -cdef float32_t logLR_sym ( float32_t x, float32_t y ): - """Calculate log10 Likelihood between H1 ( enriched ) and H0 ( - another enriched ). Set minus sign for H0>H1. - - * symmetric version * - - """ - cdef: - float32_t s - - if (x,y) in sym_logLR_dict: - return sym_logLR_dict[ ( x, y ) ] - else: - if x > y: - s = (x*(log(x)-log(y))+y-x)*LOG10_E - elif y > x: - s = (y*(log(x)-log(y))+y-x)*LOG10_E - else: - s = 0 - sym_logLR_dict[ ( x, y ) ] = s - return s - -cdef float32_t get_logFE ( float32_t x, float32_t y ): - """ return 100* log10 fold enrichment with +1 pseudocount. - """ - return log10( x/y ) - -cdef float32_t get_subtraction ( float32_t x, float32_t y): - """ return subtraction. - """ - return x - y - -# ------------------------------------ -# Classes -# ------------------------------------ - -cdef class ScoreTrackII: - """Class for a container to keep signals of each genomic position, - including 1. score, 2. treatment and 2. control pileup. - - It also contains scoring methods and call_peak functions. - """ - cdef: - dict data # dictionary for data of each chromosome - dict datalength # length of data array of each chromosome - bool trackline # whether trackline should be saved in bedGraph - float32_t treat_edm # seq depth in million of treatment - float32_t ctrl_edm # seq depth in million of control - char scoring_method # method for calculating scores. - char normalization_method # scale to control? scale to treatment? both scale to 1million reads? - float32_t pseudocount # the pseudocount used to calcuate logLR, FE or logFE - float32_t cutoff - dict pvalue_stat # save pvalue<->length dictionary - - - def __init__ (self, float32_t treat_depth, float32_t ctrl_depth, float32_t pseudocount = 1.0 ): - """Initialize. - - treat_depth and ctrl_depth are effective depth in million: - sequencing depth in million after - duplicates being filtered. If - treatment is scaled down to - control sample size, then this - should be control sample size in - million. And vice versa. - - pseudocount: a pseudocount used to calculate logLR, FE or - logFE. Please note this value will not be changed - with normalization method. So if you really want - to set pseudocount 1 per million reads, set it - after you normalize treat and control by million - reads by `change_normalizetion_method(ord('M'))`. - - """ - self.data = {} # for each chromosome, there is a l*4 - # matrix. First column: end position - # of a region; Second: treatment - # pileup; third: control pileup ; - # forth: score ( can be - # p/q-value/likelihood - # ratio/fold-enrichment/subtraction - # depending on -c setting) - self.datalength = {} - self.trackline = False - self.treat_edm = treat_depth - self.ctrl_edm = ctrl_depth - #scoring_method: p: -log10 pvalue; - # q: -log10 qvalue; - # l: log10 likelihood ratio ( minus for depletion ) - # f: log10 fold enrichment - # F: linear fold enrichment - # d: subtraction - # m: fragment pileup per million reads - # N: not set - self.scoring_method = ord("N") - - #normalization_method: T: scale to depth of treatment; - # C: scale to depth of control; - # M: scale to depth of 1 million; - # N: not set/ raw pileup - self.normalization_method = ord("N") - - self.pseudocount = pseudocount - self.pvalue_stat = {} - - cpdef set_pseudocount( self, float32_t pseudocount ): - self.pseudocount = pseudocount - - cpdef enable_trackline( self ): - """Turn on trackline with bedgraph output - """ - self.trackline = True - - cpdef add_chromosome ( self, bytes chrom, int32_t chrom_max_len ): - """ - chrom: chromosome name - chrom_max_len: maximum number of data points in this chromosome - - """ - if chrom not in self.data: - #self.data[chrom] = np.zeros( ( chrom_max_len, 4 ), dtype="int32" ) # remember col #2-4 is actual value * 100, I use integer here. - self.data[chrom] = [ np.zeros( chrom_max_len, dtype="int32" ), # pos - np.zeros( chrom_max_len, dtype="float32" ), # pileup at each interval, in float32 format - np.zeros( chrom_max_len, dtype="float32" ), # control at each interval, in float32 format - np.zeros( chrom_max_len, dtype="float32" ) ] # score at each interval, in float32 format - self.datalength[chrom] = 0 - - cpdef add (self, bytes chromosome, int32_t endpos, float32_t chip, float32_t control): - """Add a chr-endpos-sample-control block into data - dictionary. - - chromosome: chromosome name in string - endpos : end position of each interval in integer - chip : ChIP pileup value of each interval in float - control : Control pileup value of each interval in float - - *Warning* Need to add regions continuously. - """ - cdef int32_t i - i = self.datalength[chromosome] - c = self.data[chromosome] - c[0][ i ] = endpos - c[1][ i ] = chip - c[2][ i ] = control - self.datalength[chromosome] += 1 - - cpdef finalize ( self ): - """ - Adjust array size of each chromosome. - - """ - cdef: - bytes chrom, k - int32_t l - - for chrom in sorted(self.data.keys()): - d = self.data[chrom] - l = self.datalength[chrom] - d[0].resize( l, refcheck = False ) - d[1].resize( l, refcheck = False ) - d[2].resize( l, refcheck = False ) - d[3].resize( l, refcheck = False ) - return - - cpdef get_data_by_chr (self, bytes chromosome): - """Return array of counts by chromosome. - - The return value is a tuple: - ([end pos],[value]) - """ - if chromosome in self.data: - return self.data[chromosome] - else: - return None - - cpdef get_chr_names (self): - """Return all the chromosome names stored. - - """ - l = set(self.data.keys()) - return l - - cpdef change_normalization_method ( self, char normalization_method ): - """Change/set normalization method. However, I do not - recommend change this back and forward, since some precision - issue will happen -- I only keep two digits. - - normalization_method: T: scale to depth of treatment; - C: scale to depth of control; - M: scale to depth of 1 million; - N: not set/ raw pileup - """ - if normalization_method == ord('T'): - if self.normalization_method == ord('T'): # do nothing - pass - elif self.normalization_method == ord('C'): - self.normalize( self.treat_edm/self.ctrl_edm, self.treat_edm/self.ctrl_edm ) - elif self.normalization_method == ord('M'): - self.normalize( self.treat_edm, self.treat_edm ) - elif self.normalization_method == ord('N'): - self.normalize( 1, self.treat_edm/self.ctrl_edm ) - else: - raise NotImplemented - self.normalization_method = ord('T') - elif normalization_method == ord('C'): - if self.normalization_method == ord('T'): - self.normalize( self.ctrl_edm/self.treat_edm, self.ctrl_edm/self.treat_edm ) - elif self.normalization_method == ord('C'): # do nothing - pass - elif self.normalization_method == ord('M'): - self.normalize( self.ctrl_edm, self.ctrl_edm ) - elif self.normalization_method == ord('N'): - self.normalize( self.ctrl_edm/self.treat_edm, 1 ) - else: - raise NotImplemented - self.normalization_method = ord('C') - elif normalization_method == ord('M'): - if self.normalization_method == ord('T'): - self.normalize( 1/self.treat_edm, 1/self.treat_edm ) - elif self.normalization_method == ord('C'): - self.normalize( 1/self.ctrl_edm, 1/self.ctrl_edm ) - elif self.normalization_method == ord('M'): # do nothing - pass - elif self.normalization_method == ord('N'): - self.normalize( 1/self.treat_edm, 1/self.ctrl_edm ) - else: - raise NotImplemented - self.normalization_method = ord('M') - elif normalization_method == ord('N'): - if self.normalization_method == ord('T'): - self.normalize( self.treat_edm, self.treat_edm ) - elif self.normalization_method == ord('C'): - self.normalize( self.ctrl_edm, self.ctrl_edm ) - elif self.normalization_method == ord('M'): - self.normalize( self.treat_edm, self.ctrl_edm ) - elif self.normalization_method == ord('N'): # do nothing - pass - else: - raise NotImplemented - self.normalization_method = ord('N') - - cdef normalize ( self, float32_t treat_scale, float32_t control_scale ): - cdef: - np.ndarray p, c - int64_t l, i - - for chrom in sorted(self.data.keys()): - p = self.data[chrom][1] - c = self.data[chrom][2] - l = self.datalength[chrom] - for i in range(l): - p[ i ] *= treat_scale - c[ i ] *= control_scale - return - - cpdef change_score_method (self, char scoring_method): - """ - scoring_method: p: -log10 pvalue; - q: -log10 qvalue; - l: log10 likelihood ratio ( minus for depletion ) - s: symmetric log10 likelihood ratio ( for comparing two ChIPs ) - f: log10 fold enrichment - F: linear fold enrichment - d: subtraction - M: maximum - m: fragment pileup per million reads - """ - if scoring_method == ord('p'): - self.compute_pvalue() - elif scoring_method == ord('q'): - #if not already calculated p, compute pvalue first - if self.scoring_method != ord('p'): - self.compute_pvalue() - self.compute_qvalue() - elif scoring_method == ord('l'): - self.compute_likelihood() - elif scoring_method == ord('s'): - self.compute_sym_likelihood() - elif scoring_method == ord('f'): - self.compute_logFE() - elif scoring_method == ord('F'): - self.compute_foldenrichment() - elif scoring_method == ord('d'): - self.compute_subtraction() - elif scoring_method == ord('m'): - self.compute_SPMR() - elif scoring_method == ord('M'): - self.compute_max() - else: - raise NotImplemented - - cdef compute_pvalue ( self ): - """Compute -log_{10}(pvalue) - """ - cdef: - np.ndarray[np.float32_t] p, c, v - np.ndarray[np.int32_t] pos - int64_t l, i, prev_pos - bytes chrom - - for chrom in sorted(self.data.keys()): - prev_pos = 0 - pos = self.data[chrom][0] - p = self.data[chrom][1] - c = self.data[chrom][2] - v = self.data[chrom][3] - l = self.datalength[chrom] - for i in range(l): - v[ i ] = get_pscore( (p[ i ] + self.pseudocount) , c[ i ] + self.pseudocount ) - try: - self.pvalue_stat[v[ i ]] += pos[ i ] - prev_pos - except: - self.pvalue_stat[v[ i ]] = pos[ i ] - prev_pos - prev_pos = pos[ i ] - - self.scoring_method = ord('p') - return - - cdef compute_qvalue ( self ): - """Compute -log_{10}(qvalue) - """ - cdef: - object pqtable - int64_t i,l,j - bytes chrom - np.ndarray p, c, v - - # pvalue should be computed first! - assert self.scoring_method == ord('p') - # make pqtable - pqtable = self.make_pq_table() - - # convert p to q - for chrom in sorted(self.data.keys()): - v = self.data[chrom][3] - l = self.datalength[chrom] - for i in range(l): - v[ i ] = pqtable[ v[ i ] ] - #v [ i ] = g( v[ i ]) - - self.scoring_method = ord('q') - return - - cpdef object make_pq_table ( self ): - """Make pvalue-qvalue table. - - Step1: get all pvalue and length of block with this pvalue - Step2: Sort them - Step3: Apply AFDR method to adjust pvalue and get qvalue for each pvalue - - Return a dictionary of {-log10pvalue:(-log10qvalue,rank,basepairs)} relationships. - """ - cdef: - int64_t n, pre_p, this_p, length, pre_l, l, i, j - float32_t this_v, pre_v, v, q, pre_q # store the p and q scores - int64_t N, k - float32_t f - bytes chrom - np.ndarray v_chrom, pos_chrom - object pvalue2qvalue - dict pvalue_stat - list unique_values - - assert self.scoring_method == ord('p') - - pvalue_stat = self.pvalue_stat - - N = sum(pvalue_stat.values()) - k = 1 # rank - f = -log10(N) - pre_v = -2147483647 - pre_l = 0 - pre_q = 2147483647 # save the previous q-value - - pvalue2qvalue = Float32to32Map( for_int = False ) - unique_values = sorted(list(pvalue_stat.keys()), reverse=True) - for i in range(len(unique_values)): - v = unique_values[i] - l = pvalue_stat[v] - q = v + (log10(k) + f) - if q > pre_q: - q = pre_q - if q <= 0: - q = 0 - break - pvalue2qvalue[ v ] = q - pre_q = q - k+=l - # bottom rank pscores all have qscores 0 - for j in range(i, len(unique_values) ): - v = unique_values[ j ] - pvalue2qvalue[ v ] = 0 - return pvalue2qvalue - - cdef compute_likelihood ( self ): - """Calculate log10 likelihood. - - """ - cdef: - #np.ndarray v, p, c - int64_t l, i - bytes chrom - float32_t v1, v2 - float32_t pseudocount - - pseudocount = self.pseudocount - - for chrom in sorted(self.data.keys()): - p = self.data[chrom][ 1 ].flat.__next__ # pileup in treatment - c = self.data[chrom][ 2 ].flat.__next__ # pileup in control - v = self.data[chrom][ 3 ] # score - l = self.datalength[chrom] - v1 = 2 - v2 = 1 - for i in range(l): - v1 = p() - v2 = c() - v[ i ] = logLR_asym( v1 + pseudocount, v2 + pseudocount ) #logLR( d[ i, 1]/100.0, d[ i, 2]/100.0 ) - #print v1, v2, v[i] - self.scoring_method = ord('l') - return - - cdef compute_sym_likelihood ( self ): - """Calculate symmetric log10 likelihood. - - """ - cdef: - #np.ndarray v, p, c - int64_t l, i - bytes chrom - float32_t v1, v2 - float32_t pseudocount - - pseudocount = self.pseudocount - - for chrom in sorted(self.data.keys()): - p = self.data[chrom][ 1 ].flat.__next__ - c = self.data[chrom][ 2 ].flat.__next__ - v = self.data[chrom][ 3 ] - l = self.datalength[chrom] - v1 = 2 - v2 = 1 - for i in range(l): - v1 = p() - v2 = c() - v[ i ] = logLR_sym( v1 + pseudocount, v2 + pseudocount ) #logLR( d[ i, 1]/100.0, d[ i, 2]/100.0 ) - self.scoring_method = ord('s') - return - - cdef compute_logFE ( self ): - """Calculate log10 fold enrichment ( with 1 pseudocount ). - - """ - cdef: - np.ndarray p, c, v - int64_t l, i - float32_t pseudocount - - pseudocount = self.pseudocount - - for chrom in sorted(self.data.keys()): - p = self.data[chrom][1] - c = self.data[chrom][2] - v = self.data[chrom][3] - l = self.datalength[chrom] - for i in range(l): - v[ i ] = get_logFE ( p[ i ] + pseudocount, c[ i ] + pseudocount) - self.scoring_method = ord('f') - return - - cdef compute_foldenrichment ( self ): - """Calculate linear scale fold enrichment ( with 1 pseudocount ). - - """ - cdef: - np.ndarray p, c, v - int64_t l, i - float32_t pseudocount - - pseudocount = self.pseudocount - - for chrom in sorted(self.data.keys()): - p = self.data[chrom][1] - c = self.data[chrom][2] - v = self.data[chrom][3] - l = self.datalength[chrom] - for i in range(l): - v[ i ] = ( p[ i ] + pseudocount )/( c[ i ] + pseudocount ) - self.scoring_method = ord('F') - return - - cdef compute_subtraction ( self ): - cdef: - np.ndarray p, c, v - int64_t l, i - - for chrom in sorted(self.data.keys()): - p = self.data[chrom][1] - c = self.data[chrom][2] - v = self.data[chrom][3] - l = self.datalength[chrom] - for i in range(l): - v[ i ] = p[ i ] - c[ i ] - self.scoring_method = ord('d') - return - - cdef compute_SPMR ( self ): - cdef: - np.ndarray p, v - int64_t l, i - float32_t scale - if self.normalization_method == ord('T') or self.normalization_method == ord('N'): - scale = self.treat_edm - elif self.normalization_method == ord('C'): - scale = self.ctrl_edm - elif self.normalization_method == ord('M'): - scale = 1 - - for chrom in sorted(self.data.keys()): - p = self.data[chrom][1] - v = self.data[chrom][3] - l = self.datalength[chrom] - for i in range(l): - v[ i ] = p[ i ] / scale # two digit precision may not be enough... - self.scoring_method = ord('m') - return - - cdef compute_max ( self ): - cdef: - np.ndarray p, c, v - int64_t l, i - - for chrom in sorted(self.data.keys()): - p = self.data[chrom][1] - c = self.data[chrom][2] - v = self.data[chrom][3] - l = self.datalength[chrom] - for i in range(l): - v[ i ] = max(p[ i ],c[ i ]) - self.scoring_method = ord('M') - return - - cpdef write_bedGraph ( self, fhd, str name, str description, short column = 3): - """Write all data to fhd in bedGraph Format. - - fhd: a filehandler to save bedGraph. - - name/description: the name and description in track line. - - colname: can be 1: chip, 2: control, 3: score - - """ - cdef: - bytes chrom - int32_t l, pre, i, p - float32_t pre_v, v - set chrs - np.ndarray pos, value - - assert column in range( 1, 4 ), "column should be between 1, 2 or 3." - - write = fhd.write - - if self.trackline: - # this line is REQUIRED by the wiggle format for UCSC browser - write( "track type=bedGraph name=\"%s\" description=\"%s\"\n" % ( name.decode(), description ) ) - - chrs = self.get_chr_names() - for chrom in sorted(chrs): - pos = self.data[ chrom ][ 0 ] - value = self.data[ chrom ][ column ] - l = self.datalength[ chrom ] - pre = 0 - if pos.shape[ 0 ] == 0: continue # skip if there's no data - pre_v = value[ 0 ] - for i in range( 1, l ): - v = value[ i ] - p = pos[ i-1 ] - #if ('%.5f' % pre_v) != ('%.5f' % v): - if abs(pre_v - v) > 1e-5: # precision is 5 digits - write( "%s\t%d\t%d\t%.5f\n" % ( chrom.decode(), pre, p, pre_v ) ) - pre_v = v - pre = p - p = pos[ -1 ] - # last one - write( "%s\t%d\t%d\t%.5f\n" % ( chrom.decode(), pre, p, pre_v ) ) - - return True - - cpdef call_peaks (self, float32_t cutoff=5.0, int32_t min_length=200, int32_t max_gap=50, bool call_summits=False): - """This function try to find regions within which, scores - are continuously higher than a given cutoff. - - This function is NOT using sliding-windows. Instead, any - regions in bedGraph above certain cutoff will be detected, - then merged if the gap between nearby two regions are below - max_gap. After this, peak is reported if its length is above - min_length. - - cutoff: cutoff of value, default 5. For -log10pvalue, it means 10^-5. - min_length : minimum peak length, default 200. - max_gap : maximum gap to merge nearby peaks, default 50. - acll_summits: - """ - cdef: - int32_t i - bytes chrom - np.ndarray pos, sample, control, value, above_cutoff, above_cutoff_v, above_cutoff_endpos, above_cutoff_startpos, above_cutoff_sv - list peak_content - - chrs = self.get_chr_names() - peaks = PeakIO() # dictionary to save peaks - - self.cutoff = cutoff - for chrom in sorted(chrs): - peak_content = [] # to store points above cutoff - - pos = self.data[chrom][ 0 ] - sample = self.data[chrom][ 1 ] - control = self.data[chrom][ 2 ] - value = self.data[chrom][ 3 ] - - above_cutoff = np.nonzero( value >= cutoff )[0] # indices where score is above cutoff - above_cutoff_v = value[above_cutoff] # scores where score is above cutoff - - above_cutoff_endpos = pos[above_cutoff] # end positions of regions where score is above cutoff - above_cutoff_startpos = pos[above_cutoff-1] # start positions of regions where score is above cutoff - above_cutoff_sv= sample[above_cutoff] # sample pileup height where score is above cutoff - - if above_cutoff_v.size == 0: - # nothing above cutoff - continue - - if above_cutoff[0] == 0: - # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] - above_cutoff_startpos[0] = 0 - - # first bit of region above cutoff - peak_content.append( (above_cutoff_startpos[0], above_cutoff_endpos[0], above_cutoff_v[0], above_cutoff_sv[0], above_cutoff[0]) ) - for i in range( 1,above_cutoff_startpos.size ): - if above_cutoff_startpos[i] - peak_content[-1][1] <= max_gap: - # append - peak_content.append( (above_cutoff_startpos[i], above_cutoff_endpos[i], above_cutoff_v[i], above_cutoff_sv[i], above_cutoff[i]) ) - else: - # close - if call_summits: - self.__close_peak2(peak_content, peaks, min_length, chrom, max_gap//2 ) - else: - self.__close_peak(peak_content, peaks, min_length, chrom ) - peak_content = [(above_cutoff_startpos[i], above_cutoff_endpos[i], above_cutoff_v[i], above_cutoff_sv[i], above_cutoff[i]),] - - # save the last peak - if not peak_content: - continue - else: - if call_summits: - self.__close_peak2(peak_content, peaks, min_length, chrom, max_gap//2 ) - else: - self.__close_peak(peak_content, peaks, min_length, chrom ) - - return peaks - - cdef bool __close_peak (self, list peak_content, object peaks, int32_t min_length, - bytes chrom): - """Close the peak region, output peak boundaries, peak summit - and scores, then add the peak to peakIO object. - - In this function, we define the peak summit as the middle - point of the region with the highest score, in this peak. For - example, if the region of the highest score is from 100 to - 200, the summit is 150. If there are several regions of the - same 'highest score', we will first calculate the possible - summit for each such region, then pick a position close to the - middle index ( = (len(highest_regions) + 1) / 2 ) of these - summits. For example, if there are three regions with the same - highest scores, [100,200], [300,400], [600,700], we will first - find the possible summits as 150, 350, and 650, and then pick - the middle index, the 2nd, of the three positions -- 350 as - the final summit. If there are four regions, we pick the 2nd - as well. - - peaks: a PeakIO object - - """ - cdef: - int32_t summit_pos, tstart, tend, tmpindex, summit_index, i, midindex - float32_t summit_value, tvalue, tsummitvalue - - peak_length = peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] - if peak_length >= min_length: # if the peak is too small, reject it - tsummit = [] - summit_pos = 0 - summit_value = 0 - for i in range(len(peak_content)): - (tstart,tend,tvalue,tsummitvalue, tindex) = peak_content[i] - #for (tstart,tend,tvalue,tsummitvalue, tindex) in peak_content: - if not summit_value or summit_value < tsummitvalue: - tsummit = [(tend + tstart) / 2, ] - tsummit_index = [ tindex, ] - summit_value = tsummitvalue - elif summit_value == tsummitvalue: - # remember continuous summit values - tsummit.append(int((tend + tstart) / 2)) - tsummit_index.append( tindex ) - # the middle of all highest points in peak region is defined as summit - midindex = int((len(tsummit) + 1) / 2) - 1 - summit_pos = tsummit[ midindex ] - summit_index = tsummit_index[ midindex ] - if self.scoring_method == ord('q'): - qscore = self.data[chrom][3][ summit_index ] - else: - # if q value is not computed, use -1 - qscore = -1 - - peaks.add( chrom, - peak_content[0][0], - peak_content[-1][1], - summit = summit_pos, - peak_score = self.data[chrom][ 3 ][ summit_index ], - pileup = self.data[chrom][ 1 ][ summit_index ], # should be the same as summit_value - pscore = get_pscore(self.data[chrom][ 1 ][ summit_index ], self.data[chrom][ 2 ][ summit_index ]), - fold_change = ( self.data[chrom][ 1 ][ summit_index ] + self.pseudocount ) / ( self.data[chrom][ 2 ][ summit_index ] + self.pseudocount ), - qscore = qscore, - ) - # start a new peak - return True - - cdef bool __close_peak2 (self, list peak_content, object peaks, int32_t min_length, - bytes chrom, int32_t smoothlen=51, - float32_t min_valley = 0.9): - """Close the peak region, output peak boundaries, peak summit - and scores, then add the peak to peakIO object. - - In this function, we use signal processing methods to smooth - the scores in the peak region, find the maxima and enforce the - peaky shape, and to define the best maxima as the peak - summit. The functions used for signal processing is 'maxima' - (with 2nd order polynomial filter) and 'enfoce_peakyness' - functions in SignalProcessing.pyx. - - peaks: a PeakIO object - - """ - cdef: - int32_t summit_pos, tstart, tend, tmpindex, summit_index, summit_offset - int32_t start, end, i, j, start_boundary - float32_t summit_value, tvalue, tsummitvalue -# np.ndarray[np.float32_t, ndim=1] w - np.ndarray[np.float32_t, ndim=1] peakdata - np.ndarray[np.int32_t, ndim=1] peakindices, summit_offsets - - # Add 10 bp padding to peak region so that we can get true minima - end = peak_content[ -1 ][ 1 ] + 10 - start = peak_content[ 0 ][ 0 ] - 10 - if start < 0: - start_boundary = 10 + start - start = 0 - else: - start_boundary = 10 - peak_length = end - start - if end - start < min_length: return # if the region is too small, reject it - - peakdata = np.zeros(end - start, dtype='float32') - peakindices = np.zeros(end - start, dtype='int32') - for (tstart,tend,tvalue,tsvalue, tmpindex) in peak_content: - i = tstart - start + start_boundary - j = tend - start + start_boundary - peakdata[i:j] = tsvalue - peakindices[i:j] = tmpindex - summit_offsets = maxima(peakdata, smoothlen) - if summit_offsets.shape[0] == 0: - # **failsafe** if no summits, fall back on old approach # - return self.__close_peak(peak_content, peaks, min_length, chrom) - else: - # remove maxima that occurred in padding - i = np.searchsorted(summit_offsets, start_boundary) - j = np.searchsorted(summit_offsets, peak_length + start_boundary, 'right') - summit_offsets = summit_offsets[i:j] - - summit_offsets = enforce_peakyness(peakdata, summit_offsets) - if summit_offsets.shape[0] == 0: - # **failsafe** if no summits, fall back on old approach # - return self.__close_peak(peak_content, peaks, min_length, chrom) - - summit_indices = peakindices[summit_offsets] - summit_offsets -= start_boundary - - peak_scores = self.data[chrom][3][ summit_indices ] - if not (peak_scores > self.cutoff).all(): - return self.__close_peak(peak_content, peaks, min_length, chrom) - for summit_offset, summit_index in zip(summit_offsets, summit_indices): - if self.scoring_method == ord('q'): - qscore = self.data[chrom][3][ summit_index ] - else: - # if q value is not computed, use -1 - qscore = -1 - peaks.add( chrom, - start, - end, - summit = start + summit_offset, - peak_score = self.data[chrom][3][ summit_index ], - pileup = self.data[chrom][1][ summit_index ], # should be the same as summit_value - pscore = get_pscore(self.data[chrom][ 1 ][ summit_index ], self.data[chrom][ 2 ][ summit_index ]), - fold_change = ( self.data[chrom][ 1 ][ summit_index ] + self.pseudocount ) / ( self.data[chrom][ 2 ][ summit_index ] + self.pseudocount ), - qscore = qscore, - ) - # start a new peak - return True - - cdef int64_t total ( self ): - """Return the number of regions in this object. - - """ - cdef: - int64_t t - bytes chrom - - t = 0 - for chrom in sorted(self.data.keys()): - t += self.datalength[chrom] - return t - - cpdef call_broadpeaks (self, float32_t lvl1_cutoff=5.0, float32_t lvl2_cutoff=1.0, int32_t min_length=200, int32_t lvl1_max_gap=50, int32_t lvl2_max_gap=400): - """This function try to find enriched regions within which, - scores are continuously higher than a given cutoff for level - 1, and link them using the gap above level 2 cutoff with a - maximum length of lvl2_max_gap. - - lvl1_cutoff: cutoff of value at enriched regions, default 5.0. - lvl2_cutoff: cutoff of value at linkage regions, default 1.0. - min_length : minimum peak length, default 200. - lvl1_max_gap : maximum gap to merge nearby enriched peaks, default 50. - lvl2_max_gap : maximum length of linkage regions, default 400. - - Return both general PeakIO object for highly enriched regions - and gapped broad regions in BroadPeakIO. - """ - cdef: - int32_t i - bytes chrom - - assert lvl1_cutoff > lvl2_cutoff, "level 1 cutoff should be larger than level 2." - assert lvl1_max_gap < lvl2_max_gap, "level 2 maximum gap should be larger than level 1." - lvl1_peaks = self.call_peaks(cutoff=lvl1_cutoff, min_length=min_length, max_gap=lvl1_max_gap) - lvl2_peaks = self.call_peaks(cutoff=lvl2_cutoff, min_length=min_length, max_gap=lvl2_max_gap) - chrs = lvl1_peaks.peaks.keys() - broadpeaks = BroadPeakIO() - # use lvl2_peaks as linking regions between lvl1_peaks - for chrom in sorted(chrs): - lvl1peakschrom = lvl1_peaks.peaks[chrom] - lvl2peakschrom = lvl2_peaks.peaks[chrom] - lvl1peakschrom_next = iter(lvl1peakschrom).__next__ - tmppeakset = [] # to temporarily store lvl1 region inside a lvl2 region - # our assumption is lvl1 regions should be included in lvl2 regions - try: - lvl1 = lvl1peakschrom_next() - for i in range( len(lvl2peakschrom) ): - # for each lvl2 peak, find all lvl1 peaks inside - # I assume lvl1 peaks can be ALL covered by lvl2 peaks. - lvl2 = lvl2peakschrom[i] - - while True: - if lvl2["start"] <= lvl1["start"] and lvl1["end"] <= lvl2["end"]: - tmppeakset.append(lvl1) - lvl1 = lvl1peakschrom_next() - else: - # make a hierarchical broad peak - #print lvl2["start"], lvl2["end"], lvl2["score"] - self.__add_broadpeak ( broadpeaks, chrom, lvl2, tmppeakset) - tmppeakset = [] - break - except StopIteration: - # no more strong (aka lvl1) peaks left - self.__add_broadpeak ( broadpeaks, chrom, lvl2, tmppeakset) - tmppeakset = [] - # add the rest lvl2 peaks - for j in range( i+1, len(lvl2peakschrom) ): - self.__add_broadpeak( broadpeaks, chrom, lvl2peakschrom[j], tmppeakset ) - - return broadpeaks - - def __add_broadpeak (self, bpeaks, bytes chrom, dict lvl2peak, list lvl1peakset): - """Internal function to create broad peak. - """ - - cdef: - int32_t blockNum, thickStart, thickEnd, start, end - bytes blockSizes, blockStarts - - start = lvl2peak["start"] - end = lvl2peak["end"] - - # the following code will add those broad/lvl2 peaks with no strong/lvl1 peaks inside - if not lvl1peakset: - # will complement by adding 1bps start and end to this region - # may change in the future if gappedPeak format was improved. - bpeaks.add(chrom, start, end, score=lvl2peak["score"], thickStart=(b"%d" % start), thickEnd=(b"%d" % end), - blockNum = 2, blockSizes = b"1,1", blockStarts = (b"0,%d" % (end-start-1)), pileup = lvl2peak["pileup"], - pscore = lvl2peak["pscore"], fold_change = lvl2peak["fc"], - qscore = lvl2peak["qscore"] ) - return bpeaks - - thickStart = b"%d" % lvl1peakset[0]["start"] - thickEnd = b"%d" % lvl1peakset[-1]["end"] - blockNum = int(len(lvl1peakset)) - blockSizes = b",".join( [b"%d" % x["length"] for x in lvl1peakset] ) - blockStarts = b",".join( [b"%d" % (x["start"]-start) for x in lvl1peakset] ) - - if lvl2peak["start"] != thickStart: - # add 1bp mark for the start of lvl2 peak - thickStart = b"%d" % start - blockNum += 1 - blockSizes = b"1,"+blockSizes - blockStarts = b"0,"+blockStarts - if lvl2peak["end"] != thickEnd: - # add 1bp mark for the end of lvl2 peak - thickEnd = b"%d" % end - blockNum += 1 - blockSizes = blockSizes+b",1" - blockStarts = blockStarts + b"," + (b"%d" % (end-start-1)) - - # add to BroadPeakIO object - bpeaks.add(chrom, start, end, score=lvl2peak["score"], thickStart=thickStart, thickEnd=thickEnd, - blockNum = blockNum, blockSizes = blockSizes, blockStarts = blockStarts, pileup = lvl2peak["pileup"], - pscore = lvl2peak["pscore"], fold_change = lvl2peak["fc"], - qscore = lvl2peak["qscore"] ) - return bpeaks - -cdef class TwoConditionScores: - """Class for saving two condition comparison scores. - """ - cdef: - dict data # dictionary for data of each chromosome - dict datalength # length of data array of each chromosome - float32_t cond1_factor # factor to apply to cond1 pileup values - float32_t cond2_factor # factor to apply to cond2 pileup values - float32_t pseudocount # the pseudocount used to calcuate LLR - float32_t cutoff - object t1bdg, c1bdg, t2bdg, c2bdg - dict pvalue_stat1, pvalue_stat2, pvalue_stat3 - - def __init__ (self, t1bdg, c1bdg, t2bdg, c2bdg, float32_t cond1_factor = 1.0, float32_t cond2_factor = 1.0, float32_t pseudocount = 0.01, float32_t proportion_background_empirical_distribution = 0.99999 ): - """ - t1bdg: a bedGraphTrackI object for treat 1 - c1bdg: a bedGraphTrackI object for control 1 - t2bdg: a bedGraphTrackI object for treat 2 - c2bdg: a bedGraphTrackI object for control 2 - - cond1_factor: this will be multiplied to values in t1bdg and c1bdg - cond2_factor: this will be multiplied to values in t2bdg and c2bdg - - pseudocount: pseudocount, by default 0.01. - - proportion_background_empirical_distribution: proportion of genome as the background to build empirical distribution - - """ - - self.data = {} # for each chromosome, there is a l*4 - # matrix. First column: end position - # of a region; Second: treatment - # pileup; third: control pileup ; - # forth: score ( can be - # p/q-value/likelihood - # ratio/fold-enrichment/subtraction - # depending on -c setting) - self.datalength = {} - self.cond1_factor = cond1_factor - self.cond2_factor = cond2_factor - self.pseudocount = pseudocount - self.pvalue_stat1 = {} - self.pvalue_stat2 = {} - self.t1bdg = t1bdg - self.c1bdg = c1bdg - self.t2bdg = t2bdg - self.c2bdg = c2bdg - - #self.empirical_distr_llr = [] # save all values in histogram - - cpdef set_pseudocount( self, float32_t pseudocount ): - self.pseudocount = pseudocount - - cpdef build ( self ): - """Compute scores from 3 types of comparisons and store them in self.data. - - """ - cdef: - set common_chrs - bytes chrname - int32_t chrom_max_len - # common chromosome names - common_chrs = self.get_common_chrs() - for chrname in common_chrs: - (cond1_treat_ps, cond1_treat_vs) = self.t1bdg.get_data_by_chr(chrname) - (cond1_control_ps, cond1_control_vs) = self.c1bdg.get_data_by_chr(chrname) - (cond2_treat_ps, cond2_treat_vs) = self.t2bdg.get_data_by_chr(chrname) - (cond2_control_ps, cond2_control_vs) = self.c2bdg.get_data_by_chr(chrname) - chrom_max_len = len(cond1_treat_ps) + len(cond1_control_ps) +\ - len(cond2_treat_ps) + len(cond2_control_ps) - self.add_chromosome( chrname, chrom_max_len ) - self.build_chromosome( chrname, - cond1_treat_ps, cond1_control_ps, - cond2_treat_ps, cond2_control_ps, - cond1_treat_vs, cond1_control_vs, - cond2_treat_vs, cond2_control_vs ) - - - cdef build_chromosome( self, chrname, - cond1_treat_ps, cond1_control_ps, - cond2_treat_ps, cond2_control_ps, - cond1_treat_vs, cond1_control_vs, - cond2_treat_vs, cond2_control_vs ): - """Internal function to calculate scores for three types of comparisons. - - cond1_treat_ps, cond1_control_ps: position of treat and control of condition 1 - cond2_treat_ps, cond2_control_ps: position of treat and control of condition 2 - cond1_treat_vs, cond1_control_vs: value of treat and control of condition 1 - cond2_treat_vs, cond2_control_vs: value of treat and control of condition 2 - - """ - cdef: - int32_t c1tp, c1cp, c2tp, c2cp, minp, pre_p - float32_t c1tv, c1cv, c2tv, c2cv - c1tpn = iter(cond1_treat_ps).__next__ - c1cpn = iter(cond1_control_ps).__next__ - c2tpn = iter(cond2_treat_ps).__next__ - c2cpn = iter(cond2_control_ps).__next__ - c1tvn = iter(cond1_treat_vs).__next__ - c1cvn = iter(cond1_control_vs).__next__ - c2tvn = iter(cond2_treat_vs).__next__ - c2cvn = iter(cond2_control_vs).__next__ - - pre_p = 0 - - try: - c1tp = c1tpn() - c1tv = c1tvn() - - c1cp = c1cpn() - c1cv = c1cvn() - - c2tp = c2tpn() - c2tv = c2tvn() - - c2cp = c2cpn() - c2cv = c2cvn() - - while True: - minp = min(c1tp, c1cp, c2tp, c2cp) - self.add( chrname, pre_p, c1tv, c1cv, c2tv, c2cv ) - pre_p = minp - if c1tp == minp: - c1tp = c1tpn() - c1tv = c1tvn() - if c1cp == minp: - c1cp = c1cpn() - c1cv = c1cvn() - if c2tp == minp: - c2tp = c2tpn() - c2tv = c2tvn() - if c2cp == minp: - c2cp = c2cpn() - c2cv = c2cvn() - except StopIteration: - # meet the end of either bedGraphTrackI, simply exit - pass - return - - cdef set get_common_chrs ( self ): - cdef: - set t1chrs, c1chrs, t2chrs, c2chrs, common - t1chrs = self.t1bdg.get_chr_names() - c1chrs = self.c1bdg.get_chr_names() - t2chrs = self.t2bdg.get_chr_names() - c2chrs = self.c2bdg.get_chr_names() - common = reduce(lambda x,y:x.intersection(y), (t1chrs,c1chrs,t2chrs,c2chrs)) - return common - - cdef add_chromosome ( self, bytes chrom, int32_t chrom_max_len ): - """ - chrom: chromosome name - chrom_max_len: maximum number of data points in this chromosome - - """ - if chrom not in self.data: - self.data[chrom] = [ np.zeros( chrom_max_len, dtype="int32" ), # pos - np.zeros( chrom_max_len, dtype="float32" ), # LLR t1 vs c1 - np.zeros( chrom_max_len, dtype="float32" ), # LLR t2 vs c2 - np.zeros( chrom_max_len, dtype="float32" )] # LLR t1 vs t2 - self.datalength[chrom] = 0 - - cdef add (self, bytes chromosome, int32_t endpos, float32_t t1, float32_t c1, float32_t t2, float32_t c2): - """Take chr-endpos-sample1-control1-sample2-control2 and - compute logLR for t1 vs c1, t2 vs c2, and t1 vs t2, then save - values. - - chromosome: chromosome name in string - endpos : end position of each interval in integer - t1 : Sample 1 ChIP pileup value of each interval in float - c1 : Sample 1 Control pileup value of each interval in float - t2 : Sample 2 ChIP pileup value of each interval in float - c2 : Sample 2 Control pileup value of each interval in float - - *Warning* Need to add regions continuously. - """ - cdef: - int32_t i - list c - i = self.datalength[chromosome] - c = self.data[chromosome] - c[0][ i ] = endpos - c[1][ i ] = logLR_asym( (t1+self.pseudocount)*self.cond1_factor, (c1+self.pseudocount)*self.cond1_factor ) - c[2][ i ] = logLR_asym( (t2+self.pseudocount)*self.cond2_factor, (c2+self.pseudocount)*self.cond2_factor ) - c[3][ i ] = logLR_sym( (t1+self.pseudocount)*self.cond1_factor, (t2+self.pseudocount)*self.cond2_factor ) - self.datalength[chromosome] += 1 - return - - cpdef finalize ( self ): - """ - Adjust array size of each chromosome. - - """ - cdef: - bytes chrom - int32_t l - list d - - for chrom in sorted(self.data.keys()): - d = self.data[chrom] - l = self.datalength[chrom] - d[0].resize( l, refcheck = False ) - d[1].resize( l, refcheck = False ) - d[2].resize( l, refcheck = False ) - d[3].resize( l, refcheck = False ) - return - - cpdef get_data_by_chr (self, bytes chromosome): - """Return array of counts by chromosome. - - The return value is a tuple: - ([end pos],[value]) - """ - if chromosome in self.data: - return self.data[chromosome] - else: - return None - - cpdef get_chr_names (self): - """Return all the chromosome names stored. - - """ - l = set(self.data.keys()) - return l - - cpdef write_bedGraph ( self, fhd, str name, str description, int32_t column = 3): - """Write all data to fhd in bedGraph Format. - - fhd: a filehandler to save bedGraph. - - name/description: the name and description in track line. - - colname: can be 1: cond1 chip vs cond1 ctrl, 2: cond2 chip vs cond2 ctrl, 3: cond1 chip vs cond2 chip - - """ - cdef: - bytes chrom - int32_t l, pre, i, p - float32_t pre_v, v - np.ndarray pos, value - - assert column in range( 1, 4 ), "column should be between 1, 2 or 3." - - write = fhd.write - - #if self.trackline: - # # this line is REQUIRED by the wiggle format for UCSC browser - # write( "track type=bedGraph name=\"%s\" description=\"%s\"\n" % ( name.decode(), description ) ) - - chrs = self.get_chr_names() - for chrom in sorted(chrs): - pos = self.data[ chrom ][ 0 ] - value = self.data[ chrom ][ column ] - l = self.datalength[ chrom ] - pre = 0 - if pos.shape[ 0 ] == 0: continue # skip if there's no data - pre_v = value[ 0 ] - for i in range( 1, l ): - v = value[ i ] - p = pos[ i-1 ] - if abs(pre_v - v)>=1e-6: - write( "%s\t%d\t%d\t%.5f\n" % ( chrom.decode(), pre, p, pre_v ) ) - pre_v = v - pre = p - p = pos[ -1 ] - # last one - write( "%s\t%d\t%d\t%.5f\n" % ( chrom.decode(), pre, p, pre_v ) ) - - return True - - cpdef write_matrix ( self, fhd, str name, str description ): - """Write all data to fhd into five columns Format: - - col1: chr_start_end - col2: t1 vs c1 - col3: t2 vs c2 - col4: t1 vs t2 - - fhd: a filehandler to save the matrix. - - """ - cdef: - bytes chrom - int32_t l, pre, i, p - float32_t v1, v2, v3 - np.ndarray pos, value1, value2, value3 - - write = fhd.write - - chrs = self.get_chr_names() - for chrom in sorted(chrs): - [ pos, value1, value2, value3 ] = self.data[ chrom ] - l = self.datalength[ chrom ] - pre = 0 - if pos.shape[ 0 ] == 0: continue # skip if there's no data - for i in range( 0, l ): - v1 = value1[ i ] - v2 = value2[ i ] - v3 = value3[ i ] - p = pos[ i ] - write( "%s:%d_%d\t%.5f\t%.5f\t%.5f\n" % ( chrom.decode(), pre, p, v1, v2, v3 ) ) - pre = p - - return True - - cpdef tuple call_peaks (self, float32_t cutoff=3, int32_t min_length=200, int32_t max_gap = 100, - bool call_summits=False): - """This function try to find regions within which, scores - are continuously higher than a given cutoff. - - For bdgdiff. - - This function is NOT using sliding-windows. Instead, any - regions in bedGraph above certain cutoff will be detected, - then merged if the gap between nearby two regions are below - max_gap. After this, peak is reported if its length is above - min_length. - - cutoff: cutoff of value, default 3. For log10 LR, it means 1000 or -1000. - min_length : minimum peak length, default 200. - max_gap : maximum gap to merge nearby peaks, default 100. - ptrack: an optional track for pileup heights. If it's not None, use it to find summits. Otherwise, use self/scoreTrack. - """ - cdef: - int32_t i - bytes chrom - np.ndarray pos, t1_vs_c1, t2_vs_c2, t1_vs_t2, \ - cond1_over_cond2, cond2_over_cond1, cond1_equal_cond2, \ - cond1_sig, cond2_sig,\ - cat1, cat2, cat3, \ - cat1_startpos, cat1_endpos, cat2_startpos, cat2_endpos, \ - cat3_startpos, cat3_endpos - chrs = self.get_chr_names() - cat1_peaks = PeakIO() # dictionary to save peaks significant at condition 1 - cat2_peaks = PeakIO() # dictionary to save peaks significant at condition 2 - cat3_peaks = PeakIO() # dictionary to save peaks significant in both conditions - - self.cutoff = cutoff - - for chrom in sorted(chrs): - pos = self.data[chrom][ 0 ] - t1_vs_c1 = self.data[chrom][ 1 ] - t2_vs_c2 = self.data[chrom][ 2 ] - t1_vs_t2 = self.data[chrom][ 3 ] - and_ = np.logical_and - cond1_over_cond2 = t1_vs_t2 >= cutoff # regions with stronger cond1 signals - cond2_over_cond1 = t1_vs_t2 <= -1*cutoff # regions with stronger cond2 signals - cond1_equal_cond2= and_( t1_vs_t2 >= -1*cutoff, t1_vs_t2 <= cutoff ) - cond1_sig = t1_vs_c1 >= cutoff # enriched regions in condition 1 - cond2_sig = t2_vs_c2 >= cutoff # enriched regions in condition 2 - # indices where score is above cutoff - cat1 = np.where( and_( cond1_sig, cond1_over_cond2 ) )[ 0 ] # cond1 stronger than cond2, the indices - cat2 = np.where( and_( cond2_over_cond1, cond2_sig ) )[ 0 ] # cond2 stronger than cond1, the indices - cat3 = np.where( and_( and_( cond1_sig, cond2_sig ), # cond1 and cond2 are equal, the indices - cond1_equal_cond2 ) ) [ 0 ] - - cat1_endpos = pos[cat1] # end positions of regions where score is above cutoff - cat1_startpos = pos[cat1-1] # start positions of regions where score is above cutoff - cat2_endpos = pos[cat2] # end positions of regions where score is above cutoff - cat2_startpos = pos[cat2-1] # start positions of regions where score is above cutoff - cat3_endpos = pos[cat3] # end positions of regions where score is above cutoff - cat3_startpos = pos[cat3-1] # start positions of regions where score is above cutoff - - # for cat1: condition 1 stronger regions - self.__add_a_peak ( cat1_peaks, chrom, cat1, cat1_startpos, cat1_endpos, t1_vs_t2, max_gap, min_length ) - # for cat2: condition 2 stronger regions - self.__add_a_peak ( cat2_peaks, chrom, cat2, cat2_startpos, cat2_endpos, -1 * t1_vs_t2, max_gap, min_length ) - # for cat3: commonly strong regions - self.__add_a_peak ( cat3_peaks, chrom, cat3, cat3_startpos, cat3_endpos, abs(t1_vs_t2), max_gap, min_length ) - - return (cat1_peaks, cat2_peaks, cat3_peaks) - - cdef object __add_a_peak ( self, object peaks, bytes chrom, np.ndarray indices, np.ndarray startpos, np.ndarray endpos, - np.ndarray score, int32_t max_gap, int32_t min_length ): - """For a given chromosome, merge nearby significant regions, - filter out smaller regions, then add regions to PeakIO - object. - - """ - cdef: - int32_t i - list peak_content - float32_t mean_logLR - - if startpos.size > 0: - # if it is not empty - peak_content = [] - if indices[0] == 0: - # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] - startpos[0] = 0 - # first bit of region above cutoff - peak_content.append( (startpos[0], endpos[0], score[indices[ 0 ]]) ) - for i in range( 1, startpos.size ): - if startpos[i] - peak_content[-1][1] <= max_gap: - # append - peak_content.append( ( startpos[i], endpos[i], score[indices[ i ]] ) ) - else: - # close - if peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] >= min_length: - mean_logLR = self.mean_from_peakcontent( peak_content ) - #if peak_content[0][0] == 22414956: - # print(f"{peak_content} {mean_logLR}") - peaks.add( chrom, peak_content[0][0], peak_content[-1][1], - summit = -1, peak_score = mean_logLR, pileup = 0, pscore = 0, - fold_change = 0, qscore = 0, - ) - peak_content = [(startpos[i], endpos[i], score[ indices[ i ] ]),] - - # save the last peak - if peak_content: - if peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] >= min_length: - mean_logLR = self.mean_from_peakcontent( peak_content ) - peaks.add( chrom, peak_content[0][0], peak_content[-1][1], - summit = -1, peak_score = mean_logLR, pileup = 0, pscore = 0, - fold_change = 0, qscore = 0, - ) - - return - - cdef float32_t mean_from_peakcontent ( self, list peakcontent ): - """ - - """ - cdef: - int32_t tmp_s, tmp_e - int32_t l - float64_t tmp_v, sum_v #for better precision - float32_t r - int32_t i - - l = 0 - sum_v = 0 #initialize sum_v as 0 - for i in range( len(peakcontent) ): - tmp_s = peakcontent[i][0] - tmp_e = peakcontent[i][1] - tmp_v = peakcontent[i][2] - sum_v += tmp_v * ( tmp_e - tmp_s ) - l += tmp_e - tmp_s - - r = ( sum_v / l ) - return r - - - cdef int64_t total ( self ): - """Return the number of regions in this object. - - """ - cdef: - int64_t t - bytes chrom - - t = 0 - for chrom in sorted(self.data.keys()): - t += self.datalength[chrom] - return t - - From f7a8cb75ede980ef203d81c30b52d9dd4fa803c4 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Fri, 18 Oct 2024 16:21:37 -0400 Subject: [PATCH 07/13] rewrite scoretrack.py --- MACS3/IO/Parser.py | 157 +++- MACS3/IO/PeakIO.py | 20 +- MACS3/Signal/BedGraph.py | 1335 +++++++++++++++++++++++++++++++- MACS3/Signal/PairedEndTrack.py | 19 +- setup.py | 2 +- test/test_PairedEndTrack.py | 49 +- test/test_Parser.py | 56 +- test/test_ScoreTrack.py | 168 ++-- 8 files changed, 1697 insertions(+), 109 deletions(-) diff --git a/MACS3/IO/Parser.py b/MACS3/IO/Parser.py index 09fd6f81..b8e3057a 100644 --- a/MACS3/IO/Parser.py +++ b/MACS3/IO/Parser.py @@ -1,7 +1,7 @@ # cython: language_level=3 # cython: profile=True # cython: linetrace=True -# Time-stamp: <2024-10-07 16:08:43 Tao Liu> +# Time-stamp: <2024-10-16 00:09:32 Tao Liu> """Module for all MACS Parser classes for input. Please note that the parsers are for reading the alignment files ONLY. @@ -28,7 +28,7 @@ from MACS3.Utilities.Constants import READ_BUFFER_SIZE from MACS3.Signal.FixWidthTrack import FWTrack -from MACS3.Signal.PairedEndTrack import PETrackI +from MACS3.Signal.PairedEndTrack import PETrackI, PETrackII from MACS3.Utilities.Logger import logging logger = logging.getLogger(__name__) @@ -1472,3 +1472,156 @@ def fw_parse_line(self, thisline: bytes) -> tuple: 1) else: raise StrandFormatError(thisline, thisfields[1]) + + +@cython.cclass +class FragParser(GenericParser): + """Parser for Fragment file containing scATAC-seq information. + + Format: + + chromosome frag_leftend frag_rightend barcode count + + Note: Only the first five columns are used! + + """ + n = cython.declare(cython.int, visibility='public') + d = cython.declare(cython.float, visibility='public') + + @cython.cfunc + def skip_first_commentlines(self): + """BEDPEParser needs to skip the first several comment lines. + """ + l_line: cython.int + thisline: bytes + + for thisline in self.fhd: + l_line = len(thisline) + if thisline and (thisline[:5] != b"track") \ + and (thisline[:7] != b"browser") \ + and (thisline[0] != 35): # 35 is b"#" + break + + # rewind from SEEK_CUR + self.fhd.seek(-l_line, 1) + return + + @cython.cfunc + def pe_parse_line(self, thisline: bytes): + """Parse each line, and return chromosome, left and right + positions, barcode and count. + + """ + thisfields: list + + thisline = thisline.rstrip() + + # still only support tabular as delimiter. + thisfields = thisline.split(b'\t') + try: + return (thisfields[0], + atoi(thisfields[1]), + atoi(thisfields[2]), + thisfields[3], + atoi(thisfields[4])) + except IndexError: + raise Exception("Less than 5 columns found at this line: %s\n" % thisline) + + @cython.ccall + def build_petrack2(self): + """Build PETrackII from all lines. + + """ + chromosome: bytes + left_pos: cython.int + right_pos: cython.int + barcode: bytes + count: cython.uchar + i: cython.long = 0 # number of fragments + m: cython.long = 0 # sum of fragment lengths + tmp: bytes = b"" + + petrack = PETrackII(buffer_size=self.buffer_size) + add_loc = petrack.add_loc + + while True: + # for each block of input + tmp += self.fhd.read(READ_BUFFER_SIZE) + if not tmp: + break + lines = tmp.split(b"\n") + tmp = lines[-1] + for thisline in lines[:-1]: + (chromosome, left_pos, right_pos, barcode, count) = self.pe_parse_line(thisline) + if left_pos < 0 or not chromosome: + continue + assert right_pos > left_pos, "Right position must be larger than left position, check your BED file at line: %s" % thisline + m += right_pos - left_pos + i += 1 + if i % 1000000 == 0: + info(" %d fragments parsed" % i) + add_loc(chromosome, left_pos, right_pos, barcode, count) + # last one + if tmp: + (chromosome, left_pos, right_pos, barcode, count) = self.pe_parse_line(thisline) + if left_pos >= 0 and chromosome: + assert right_pos > left_pos, "Right position must be larger than left position, check your BED file at line: %s" % thisline + i += 1 + m += right_pos - left_pos + add_loc(chromosome, left_pos, right_pos, barcode, count) + + self.d = cython.cast(cython.float, m) / i + self.n = i + assert self.d >= 0, "Something went wrong (mean fragment size was negative)" + + self.close() + petrack.set_rlengths({"DUMMYCHROM": 0}) + return petrack + + @cython.ccall + def append_petrack(self, petrack): + """Build PETrackI from all lines, return a PETrackI object. + """ + chromosome: bytes + left_pos: cython.int + right_pos: cython.int + barcode: bytes + count: cython.uchar + i: cython.long = 0 # number of fragments + m: cython.long = 0 # sum of fragment lengths + tmp: bytes = b"" + + add_loc = petrack.add_loc + while True: + # for each block of input + tmp += self.fhd.read(READ_BUFFER_SIZE) + if not tmp: + break + lines = tmp.split(b"\n") + tmp = lines[-1] + for thisline in lines[:-1]: + (chromosome, left_pos, right_pos, barcode, count) = self.pe_parse_line(thisline) + if left_pos < 0 or not chromosome: + continue + assert right_pos > left_pos, "Right position must be larger than left position, check your BED file at line: %s" % thisline + m += right_pos - left_pos + i += 1 + if i % 1000000 == 0: + info(" %d fragments parsed" % i) + add_loc(chromosome, left_pos, right_pos, barcode, count) + # last one + if tmp: + (chromosome, left_pos, right_pos, barcode, count) = self.pe_parse_line(thisline) + if left_pos >= 0 and chromosome: + assert right_pos > left_pos, "Right position must be larger than left position, check your BED file at line: %s" % thisline + i += 1 + m += right_pos - left_pos + add_loc(chromosome, left_pos, right_pos, barcode, count) + + self.d = (self.d * self.n + m) / (self.n + i) + self.n += i + + assert self.d >= 0, "Something went wrong (mean fragment size was negative)" + self.close() + petrack.set_rlengths({"DUMMYCHROM": 0}) + return petrack diff --git a/MACS3/IO/PeakIO.py b/MACS3/IO/PeakIO.py index 9ba1496f..0ad8f36c 100644 --- a/MACS3/IO/PeakIO.py +++ b/MACS3/IO/PeakIO.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-11 11:13:11 Tao Liu> +# Time-stamp: <2024-10-15 11:48:33 Tao Liu> """Module for PeakIO IO classes. @@ -141,6 +141,24 @@ def __str__(self): self.end, self.score) + def __getstate__(self): + return (self.chrom, + self.start, + self.end, + self.length, + self.summit, + self.score, + self.pileup, + self.pscore, + self.fc, + self.qscore, + self.name) + + def __setstate__(self, state): + (self.chrom, self.start, self.end, self.length, self.summit, + self.score, self.pileup, self.pscore, self.fc, + self.qscore, self.name) = state + @cython.cclass class PeakIO: diff --git a/MACS3/Signal/BedGraph.py b/MACS3/Signal/BedGraph.py index b16881f9..baa46af4 100644 --- a/MACS3/Signal/BedGraph.py +++ b/MACS3/Signal/BedGraph.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-14 23:47:21 Tao Liu> +# Time-stamp: <2024-10-15 16:18:23 Tao Liu> """Module for BedGraph data class. @@ -259,7 +259,7 @@ def add_chrom_data_PV(self, self.__data[chromosome] = [pyarray('i', pv['p']), pyarray('f', pv['v'])] minv = pv['v'].min() - maxv = pv['p'].max() + maxv = pv['v'].max() if maxv > self.maxvalue: self.maxvalue = maxv if minv < self.minvalue: @@ -1488,6 +1488,1337 @@ def cutoff_analysis(self, return ret +@cython.cclass +class bedGraphTrackII: + """Class for bedGraph type data. + + In bedGraph, data are represented as continuous non-overlapping + regions in the whole genome. I keep this assumption in all the + functions. If data has overlaps, some functions will definitely + give incorrect results. + + 1. Continuous: the next region should be after the previous one + unless they are on different chromosomes; + + 2. Non-overlapping: the next region should never have overlaps + with preceding region. + + The way to memorize bedGraph data is to remember the transition + points together with values of their preceding regions. The last + data point may exceed chromosome end, unless a chromosome + dictionary is given. Remember the coordinations in bedGraph and + this class is 0-indexed and right-open. + + Different with bedGraphTrackI, we use numpy array to store the + (end) positions and values. + + """ + __data: dict + maxvalue = cython.declare(cython.float, visibility="public") + minvalue = cython.declare(cython.float, visibility="public") + baseline_value = cython.declare(cython.float, visibility="public") + buffer_size: int + __size: dict + + def __init__(self, + baseline_value: cython.float = 0, + buffer_size: cython.int = 100000): + """ + baseline_value is the value to fill in the regions not defined + in bedGraph. For example, if the bedGraph is like: + + chr1 100 200 1 + chr1 250 350 2 + + Then the region chr1:200..250 should be filled with baseline_value. + + """ + self.__data = {} + self.__size = {} + self.maxvalue = -10000000 # initial maximum value is tiny since I want safe_add_loc to update it + self.minvalue = 10000000 # initial minimum value is large since I want safe_add_loc to update it + self.baseline_value = baseline_value + self.buffer_size = 100000 + + @cython.ccall + def add_loc(self, chromosome: bytes, + startpos: cython.int, + endpos: cython.int, + value: cython.float): + """Add a chr-start-end-value block into __data dictionary. + + Note, we don't check if the add_loc is called continuously on + sorted regions without any gap. So we only suggest calling + this function within MACS. + + """ + pre_v: cython.float + c: cnp.ndarray + i: cython.int + + # basic assumption, end pos should > start pos + + if endpos <= 0: + return + if startpos < 0: + startpos = 0 + + if chromosome not in self.__data: + i = 0 + # first element in the chromosome + self.__data[chromosome] = np.zeros(shape=self.buffer_size, + dtype=[('p', 'u4'), ('v', 'f4')]) + c = self.__data[chromosome] + if startpos > 0: + # start pos is not 0, then add two blocks, the first + # with "baseline_value"; the second with "value" + c[0] = (startpos, self.baseline_value) + i += 1 + + c[i] = (endpos, value) + else: + c = self.__data[chromosome] + i = self.__size[chromosome] + # get the preceding region + pre_v = c[i-1][1] # which is quicker? c[i-1][1] or c["v"][i-1]? + + # if this region is next to the previous one. + if pre_v == value: + # if value is the same, simply extend it. + c[i-1][0] = endpos + else: + if i % self.buffer_size == 0: + self.__data[chromosome].resize(i+self.buffer_size, + refcheck=False) + # otherwise, add a new region + c[i] = (endpos, value) + i += 1 + + self.__size[chromosome] = i + + @cython.ccall + def add_loc_wo_merge(self, chromosome: bytes, + startpos: cython.int, + endpos: cython.int, + value: cython.float): + """Add a chr-start-end-value block into __data dictionary. + + Note, we don't check if the add_loc is called continuously on + sorted regions without any gap. So we only suggest calling + this function within MACS. + + This one won't merge nearby ranges with the same value + """ + c: cnp.ndarray + i: cython.int + + # basic assumption, end pos should > start pos + + if endpos <= 0: + return + if startpos < 0: + startpos = 0 + + if chromosome not in self.__data: + i = 0 + # first element in the chromosome + self.__data[chromosome] = np.zeros(shape=self.buffer_size, + dtype=[('p', 'u4'), ('v', 'f4')]) + c = self.__data[chromosome] + if startpos > 0: + # start pos is not 0, then add two blocks, the first + # with "baseline_value"; the second with "value" + c[0] = (startpos, self.baseline_value) + i += 1 + + c[i] = (endpos, value) + else: + c = self.__data[chromosome] + i = self.__size[chromosome] + + if i % self.buffer_size == 0: + self.__data[chromosome].resize(i+self.buffer_size, + refcheck=False) + # otherwise, add a new region + c[i] = (endpos, value) + i += 1 + + self.__size[chromosome] = i + + @cython.ccall + def add_chrom_data(self, + chromosome: bytes, + pv: cnp.ndarray): + """Add a pv data to a chromosome. Replace the previous data. + + This is a kinda silly function to waste time and convert a PV + array (2-d named numpy array) into two python arrays for this + BedGraph class. May have better function later. + + Note: no checks for error, use with caution + """ + self.__data[chromosome] = pv + self.__size[chromosome] = len(pv) + + return + + @cython.ccall + def destroy(self) -> bool: + """ destroy content, free memory. + """ + chrs: set + chrom: bytes + + chrs = self.get_chr_names() + for chrom in sorted(chrs): + if chrom in self.__data: + self.__data[chrom].resize(self.buffer_size, + refcheck=False) + self.__data[chrom].resize(0, + refcheck=False) + self.__data[chrom] = None + self.__data.pop(chrom) + self.__size[chrom] = 0 + return True + + @cython.ccall + def finalize(self): + """Resize np arrays. + + Note: If this function is called, please do not add any more + data. remember to call it after all the files are read! + + """ + c: bytes + chrnames: set + maxv: cython.float + minv: cython.float + + chrnames = self.get_chr_names() + + for c in chrnames: + self.__data[c].resize((self.__size[c]), refcheck=False) + self.__data[c].sort(order=['p']) + + minv = self.__data[c]['v'].min() + maxv = self.__data[c]['v'].max() + if maxv > self.maxvalue: + self.maxvalue = maxv + if minv < self.minvalue: + self.minvalue = minv + return + + @cython.ccall + def get_data_by_chr(self, chromosome: bytes) -> cnp.ndarray: + """Return array of counts by chromosome. + + The return value is a tuple: + ([end pos],[value]) + """ + if chromosome in self.__data: + return self.__data[chromosome] + else: + return None + + @cython.ccall + def get_chr_names(self) -> set: + """Return all the chromosome names stored. + + """ + return set(sorted(self.__data.keys())) + + @cython.ccall + def filter_score(self, cutoff: cython.float = 0) -> bool: + """Filter using a score cutoff. Any region lower than score + cutoff will be set to self.baseline_value. + + Self will be modified. + """ + # new_pre_pos: cython.int + chrom: bytes + chrs: set + d: cnp.ndarray + maxv: cython.float + minv: cython.float + + chrs = self.get_chr_names() + for chrom in sorted(chrs): + d = self.__data[chrom] + d = d[d['v'] > cutoff] + self.__data[chrom] = d + self.__size[chrom] = len(d) + minv = d['v'].min() + maxv = d['v'].max() + if maxv > self.maxvalue: + self.maxvalue = maxv + if minv < self.minvalue: + self.minvalue = minv + return True + + @cython.ccall + def summary(self) -> tuple: + """Calculate the sum, total_length, max, min, mean, and std. + + Return a tuple for (sum, total_length, max, min, mean, std). + + """ + d: cnp.ndarray + n_v: cython.long + sum_v: cython.float + max_v: cython.float + min_v: cython.float + mean_v: cython.float + variance: cython.float + tmp: cython.float + std_v: cython.float + pre_p: cython.int + ln: cython.int + i: cython.int + + pre_p = 0 + n_v = 0 + sum_v = 0 + max_v = -100000 + min_v = 100000 + for d in self.__data.values(): + # for each chromosome + pre_p = 0 + for i in range(len(d)): + # for each region + ln = d[i][0]-pre_p + sum_v += d[i][1]*ln + n_v += ln + pre_p = d[i][0] + max_v = max(max(d["v"]), max_v) + min_v = min(min(d["v"]), min_v) + mean_v = sum_v/n_v + variance = 0.0 + for d in self.__data.values(): + for i in range(len(d)): + # for each region + tmp = d[i][1]-mean_v + ln = d[i][0]-pre_p + variance += tmp*tmp*ln + pre_p = d[i][0] + + variance /= float(n_v-1) + std_v = sqrt(variance) + return (sum_v, n_v, max_v, min_v, mean_v, std_v) + + @cython.ccall + def call_peaks(self, + cutoff: cython.float = 1.0, + min_length: cython.int = 200, + max_gap: cython.int = 50, + call_summits: bool = False): + """This function try to find regions within which, scores + are continuously higher than a given cutoff. + + """ + i: cython.int + chrom: bytes + pos: cnp.ndarray + value: cnp.ndarray + above_cutoff: cnp.ndarray(dtype="bool", ndim=1) + above_cutoff_v: cnp.ndarray + above_cutoff_endpos: cnp.ndarray + above_cutoff_startpos: cnp.ndarray + peak_content: list + + chrs = self.get_chr_names() + peaks = PeakIO() # dictionary to save peaks + + for chrom in sorted(chrs): + peak_content = [] # to store points above cutoff + pos = self.__data[chrom]['p'] + value = self.__data[chrom]['v'] + + above_cutoff = value >= cutoff + # scores where score is above cutoff + above_cutoff_v = value[above_cutoff] + # end positions of regions where score is above cutoff + above_cutoff_endpos = pos[above_cutoff] + # start positions of regions where score is above cutoff + above_cutoff_startpos = pos[np.roll(above_cutoff, -1)] + + if above_cutoff_v.size == 0: + # nothing above cutoff + continue + + if above_cutoff[0] is True: + # first element > cutoff, fix the first point as 0. otherwise it would be the last item in __data[chrom]['p'] + above_cutoff_startpos[0] = 0 + + # first bit of region above cutoff + peak_content.append((above_cutoff_startpos[0], above_cutoff_endpos[0], above_cutoff_v[0])) + for i in range(1, above_cutoff_startpos.size): + if above_cutoff_startpos[i] - peak_content[-1][1] <= max_gap: + # append + peak_content.append((above_cutoff_startpos[i], above_cutoff_endpos[i], above_cutoff_v[i])) + else: + # close + self.__close_peak(peak_content, + peaks, + min_length, + chrom) + peak_content = [(above_cutoff_startpos[i], above_cutoff_endpos[i], above_cutoff_v[i]),] + + # save the last peak + if not peak_content: + continue + else: + self.__close_peak(peak_content, + peaks, + min_length, + chrom) + + return peaks + + @cython.cfunc + def __close_peak(self, + peak_content: list, + peaks, + min_length: cython.int, + chrom: bytes) -> bool: + tsummit: list # list for temporary summits + peak_length: cython.int + summit: cython.int + tstart: cython.int + tend: cython.int + summit_value: cython.float + tvalue: cython.float + peak_length = peak_content[-1][1]-peak_content[0][0] + if peak_length >= min_length: # if the peak is too small, reject it + tsummit = [] + summit = 0 + summit_value = 0 + for (tstart, tend, tvalue) in peak_content: + if not summit_value or summit_value < tvalue: + tsummit = [cython.cast(cython.int, (tend+tstart)/2),] + summit_value = tvalue + elif summit_value == tvalue: + tsummit.append(cython.cast(cython.int, (tend+tstart)/2)) + summit = tsummit[cython.cast(cython.int, (len(tsummit)+1)/2)-1] + peaks.add(chrom, + peak_content[0][0], + peak_content[-1][1], + summit=summit, + peak_score=summit_value, + pileup=0, + pscore=0, + fold_change=0, + qscore=0 + ) + return True + + @cython.ccall + def call_broadpeaks(self, + lvl1_cutoff: cython.float = 500, + lvl2_cutoff: cython.float = 100, + min_length: cython.int = 200, + lvl1_max_gap: cython.int = 50, + lvl2_max_gap: cython.int = 400): + """This function try to find enriched regions within which, + scores are continuously higher than a given cutoff for level + 1, and link them using the gap above level 2 cutoff with a + maximum length of lvl2_max_gap. + + lvl1_cutoff: cutoff of value at enriched regions, default 500. + lvl2_cutoff: cutoff of value at linkage regions, default 100. + min_length : minimum peak length, default 200. + lvl1_max_gap : maximum gap to merge nearby enriched peaks, default 50. + lvl2_max_gap : maximum length of linkage regions, default 400. + colname: can be 'sample','control','-100logp','-100logq'. Cutoff will be applied to the specified column. + + Return both general PeakIO object for highly enriched regions + and gapped broad regions in BroadPeakIO. + """ + chrom: bytes + i: cython.int + j: cython.int + chrs: set + lvl1: PeakContent + lvl2: PeakContent # PeakContent class object + lvl1peakschrom: list + lvl2peakschrom: list + + assert lvl1_cutoff > lvl2_cutoff, "level 1 cutoff should be larger than level 2." + assert lvl1_max_gap < lvl2_max_gap, "level 2 maximum gap should be larger than level 1." + lvl1_peaks = self.call_peaks(cutoff=lvl1_cutoff, + min_length=min_length, + max_gap=lvl1_max_gap, + call_summits=False) + lvl2_peaks = self.call_peaks(cutoff=lvl2_cutoff, + min_length=min_length, + max_gap=lvl2_max_gap, + call_summits=False) + chrs = lvl1_peaks.get_chr_names() + broadpeaks = BroadPeakIO() + # use lvl2_peaks as linking regions between lvl1_peaks + for chrom in sorted(chrs): + lvl1peakschrom = lvl1_peaks.get_data_from_chrom(chrom) + lvl2peakschrom = lvl2_peaks.get_data_from_chrom(chrom) + lvl1peakschrom_next = iter(lvl1peakschrom).__next__ + tmppeakset = [] # to temporarily store lvl1 region inside a lvl2 region + # our assumption is lvl1 regions should be included in lvl2 regions + try: + lvl1 = lvl1peakschrom_next() + for i in range(len(lvl2peakschrom)): + # for each lvl2 peak, find all lvl1 peaks inside + lvl2 = lvl2peakschrom[i] + while True: + if lvl2["start"] <= lvl1["start"] and lvl1["end"] <= lvl2["end"]: + tmppeakset.append(lvl1) + lvl1 = lvl1peakschrom_next() + else: + self.__add_broadpeak(broadpeaks, + chrom, + lvl2, + tmppeakset) + tmppeakset = [] + break + except StopIteration: + self.__add_broadpeak(broadpeaks, chrom, lvl2, tmppeakset) + tmppeakset = [] + for j in range(i+1, len(lvl2peakschrom)): + self.__add_broadpeak(broadpeaks, + chrom, + lvl2peakschrom[j], + tmppeakset) + return broadpeaks + + @cython.cfunc + def __add_broadpeak(self, + bpeaks, + chrom: bytes, + lvl2peak: PeakContent, + lvl1peakset: list): + """Internal function to create broad peak. + """ + start: cython.int + end: cython.int + blockNum: cython.int + blockSizes: bytes + blockStarts: bytes + thickStart: bytes + thickEnd: bytes + + start = lvl2peak["start"] + end = lvl2peak["end"] + + # the following code will add those broad/lvl2 peaks with no + # strong/lvl1 peaks inside + if not lvl1peakset: + # try: + # will complement by adding 1bps start and end to this region + # may change in the future if gappedPeak format was improved. + bpeaks.add(chrom, start, end, + score=lvl2peak["score"], + thickStart=(b"%d" % start), + thickEnd=(b"%d" % end), + blockNum=2, + blockSizes=b"1,1", + blockStarts=(b"0,%d" % (end-start-1)), + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) + return bpeaks + + thickStart = b"%d" % lvl1peakset[0]["start"] + thickEnd = b"%d" % lvl1peakset[-1]["end"] + blockNum = len(lvl1peakset) + blockSizes = b",".join([b"%d" % x["length"] for x in lvl1peakset]) + blockStarts = b",".join([b"%d" % (x["start"]-start) for x in lvl1peakset]) + + if int(thickStart) != start: + # add 1bp left block + thickStart = b"%d" % start + blockNum += 1 + blockSizes = b"1,"+blockSizes + blockStarts = b"0,"+blockStarts + if int(thickEnd) != end: + # add 1bp right block + thickEnd = b"%d" % end + blockNum += 1 + blockSizes = blockSizes+b",1" + blockStarts = blockStarts + b"," + (b"%d" % (end-start-1)) + + bpeaks.add(chrom, start, end, + score=lvl2peak["score"], + thickStart=thickStart, + thickEnd=thickEnd, + blockNum=blockNum, + blockSizes=blockSizes, + blockStarts=blockStarts, + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) + return bpeaks + + @cython.ccall + def refine_peaks(self, peaks): + """This function try to based on given peaks, re-evaluate the + peak region, call the summit. + + peaks: PeakIO object + return: a new PeakIO object + + """ + pre_p: cython.int + p: cython.int + peak_s: cython.int + peak_e: cython.int + v: cython.float + chrom: bytes + chrs: set + + peaks.sort() + new_peaks = PeakIO() + chrs = self.get_chr_names() + assert isinstance(peaks, PeakIO) + chrs = chrs.intersection(set(peaks.get_chr_names())) + + for chrom in sorted(chrs): + peaks_chr = peaks.get_data_from_chrom(chrom) + peak_content = [] + # arrays for position and values + (ps, vs) = self.get_data_by_chr(chrom) + # assign the next function to a viable to speed up + psn = iter(ps).__next__ + vsn = iter(vs).__next__ + peakn = iter(peaks_chr).__next__ + + # remember previous position in bedgraph/self + pre_p = 0 + p = psn() + v = vsn() + peak = peakn() + peak_s = peak["start"] + peak_e = peak["end"] + while True: + # look for overlap + if p > peak_s and peak_e > pre_p: + # now put four coordinates together and pick the middle two + s, e = sorted([p, peak_s, peak_e, pre_p])[1:3] + # add this content + peak_content.append((s, e, v)) + # move self/bedGraph + try: + pre_p = p + p = psn() + v = vsn() + except Exception: + # no more value chunk in bedGraph + break + elif pre_p >= peak_e: + # close peak + self.__close_peak(peak_content, new_peaks, 0, chrom) + peak_content = [] + # move peak + try: + peak = peakn() + peak_s = peak["start"] + peak_e = peak["end"] + except Exception: + # no more peak + break + elif peak_s >= p: + # move self/bedgraph + try: + pre_p = p + p = psn() + v = vsn() + except Exception: + # no more value chunk in bedGraph + break + else: + raise Exception(f"no way here! prev position:{pre_p}; position:{p}; value:{v}; peak start:{peak_s}; peak end:{peak_e}") + + # save the last peak + if peak_content: + self.__close_peak(peak_content, new_peaks, 0, chrom) + return new_peaks + + @cython.ccall + def total(self) -> cython.int: + """Return the number of regions in this object. + + """ + t: cython.int + d: cnp.ndarray + + t = 0 + for d in self.__data.values(): + t += len(d) + return t + + # @cython.ccall + # def set_single_value(self, new_value: cython.float): + # """Change all the values in bedGraph to the same new_value, + # return a new bedGraphTrackI. + + # """ + # chrom: bytes + # max_p: cython.int + + # ret = bedGraphTrackI() + # chroms = set(self.get_chr_names()) + # for chrom in sorted(chroms): + # # arrays for position and values + # (p1, v1) = self.get_data_by_chr(chrom) + # # maximum p + # max_p = max(p1) + # # add a region from 0 to max_p + # ret.add_loc(chrom, 0, max_p, new_value) + # return ret + + # @cython.ccall + # def overlie(self, bdgTracks, func: str = "max"): + # """Calculate two or more bedGraphTrackI objects by letting self + # overlying bdgTrack2, with user-defined functions. + + # Transition positions from both bedGraphTrackI objects will be + # considered and combined. For example: + + # #1 bedGraph (self) | #2 bedGraph + # ----------------------------------------------- + # chr1 0 100 0 | chr1 0 150 1 + # chr1 100 200 3 | chr1 150 250 2 + # chr1 200 300 4 | chr1 250 300 4 + + # these two bedGraphs will be combined to have five transition + # points: 100, 150, 200, 250, and 300. So in order to calculate + # two bedGraphs, I pair values within the following regions + # like: + + # chr s e (#1,#2) applied_func_max + # ----------------------------------------------- + # chr1 0 100 (0,1) 1 + # chr1 100 150 (3,1) 3 + # chr1 150 200 (3,2) 3 + # chr1 200 250 (4,2) 4 + # chr1 250 300 (4,4) 4 + + # Then the given 'func' will be applied on each 2-tuple as func(#1,#2) + + # Supported 'func' are "sum", "subtract" (only for two bdg + # objects), "product", "divide" (only for two bdg objects), + # "max", "mean" and "fisher". + + # Return value is a new bedGraphTrackI object. + + # Option: bdgTracks can be a list of bedGraphTrackI objects + # """ + # pre_p: cython.int + # chrom: bytes + + # nr_tracks = len(bdgTracks) + 1 # +1 for self + # assert nr_tracks >= 2, "Specify at least one more bdg objects." + # for i, bdgTrack in enumerate(bdgTracks): + # assert isinstance(bdgTrack, bedGraphTrackI), "bdgTrack{} is not a bedGraphTrackI object".format(i + 1) + + # if func == "max": + # f = max + # elif func == "mean": + # f = mean_func + # elif func == "fisher": + # f = fisher_func + # elif func == "sum": + # f = sum + # elif func == "product": + # f = product_func + # elif func == "subtract": + # if nr_tracks == 2: + # f = subtract_func + # else: + # raise Exception(f"Only one more bdg object is allowed, but provided {nr_tracks-1}") + # elif func == "divide": + # if nr_tracks == 2: + # f = divide_func + # else: + # raise Exception(f"Only one more bdg object is allowed, but provided {nr_tracks-1}") + # else: + # raise Exception("Invalid function {func}! Choose from 'sum', 'subtract' (only for two bdg objects), 'product', 'divide' (only for two bdg objects), 'max', 'mean' and 'fisher'. ") + + # ret = bedGraphTrackI() + + # common_chr = set(self.get_chr_names()) + # for track in bdgTracks: + # common_chr = common_chr.intersection(set(track.get_chr_names())) + + # for chrom in sorted(common_chr): + # datas = [self.get_data_by_chr(chrom)] + # datas.extend([bdgTracks[i].get_data_by_chr(chrom) for i in range(len(bdgTracks))]) + + # ps, vs, pn, vn = [], [], [], [] + # for data in datas: + # ps.append(data[0]) + # pn.append(iter(ps[-1]).__next__) + # vs.append(data[1]) + # vn.append(iter(vs[-1]).__next__) + + # pre_p = 0 # remember the previous position in the new bedGraphTrackI object ret + # try: + # ps_cur = [pn[i]() for i in range(len(pn))] + # vs_cur = [vn[i]() for i in range(len(pn))] + + # while True: + # # get the lowest position + # lowest_p = min(ps_cur) + + # # at least one lowest position, could be multiple + # locations = [i for i in range(len(ps_cur)) if ps_cur[i] == lowest_p] + + # # add the data until the interval + # ret.add_loc(chrom, pre_p, ps_cur[locations[0]], f(vs_cur)) + + # pre_p = ps_cur[locations[0]] + # for index in locations: + # ps_cur[index] = pn[index]() + # vs_cur[index] = vn[index]() + # except StopIteration: + # # meet the end of either bedGraphTrackI, simply exit + # pass + # return ret + + # @cython.ccall + # def apply_func(self, func) -> bool: + # """Apply function 'func' to every value in this bedGraphTrackI object. + + # *Two adjacent regions with same value after applying func will + # not be merged. + # """ + # i: cython.int + + # for (p, s) in self.__data.values(): + # for i in range(len(s)): + # s[i] = func(s[i]) + # self.maxvalue = func(self.maxvalue) + # self.minvalue = func(self.minvalue) + # return True + + # @cython.ccall + # def p2q(self): + # """Convert pvalue scores to qvalue scores. + + # *Assume scores in this bedGraph are pvalue scores! Not work + # for other type of scores. + # """ + # chrom: bytes + # pos_array: pyarray + # pscore_array: pyarray + # pvalue_stat: dict = {} + # pqtable: dict = {} + # pre_p: cython.long + # this_p: cython.long + # # pre_l: cython.long + # # l: cython.long + # i: cython.long + # nhcal: cython.long = 0 + # N: cython.long + # k: cython.long + # this_l: cython.long + # this_v: cython.float + # # pre_v: cython.float + # v: cython.float + # q: cython.float + # pre_q: cython.float + # f: cython.float + # unique_values: list + + # # calculate frequencies of each p-score + # for chrom in sorted(self.get_chr_names()): + # pre_p = 0 + + # [pos_array, pscore_array] = self.__data[chrom] + + # pn = iter(pos_array).__next__ + # vn = iter(pscore_array).__next__ + + # for i in range(len(pos_array)): + # this_p = pn() + # this_v = vn() + # this_l = this_p - pre_p + # if this_v in pvalue_stat: + # pvalue_stat[this_v] += this_l + # else: + # pvalue_stat[this_v] = this_l + # pre_p = this_p + + # # nhcal += len(pos_array) + + # # nhval = 0 + + # N = sum(pvalue_stat.values()) # total length + # k = 1 # rank + # f = -log10(N) + # # pre_v = -2147483647 + # # pre_l = 0 + # pre_q = 2147483647 # save the previous q-value + + # # calculate qscore for each pscore + # pqtable = {} + # unique_values = sorted(pvalue_stat.keys(), reverse=True) + # for i in range(len(unique_values)): + # v = unique_values[i] + # # l = pvalue_stat[v] + # q = v + (log10(k) + f) + # q = max(0, min(pre_q, q)) # make q-score monotonic + # pqtable[v] = q + # # pre_v = v + # pre_q = q + # # k += l + # nhcal += 1 + + # # convert pscore to qscore + # for chrom in sorted(self.get_chr_names()): + # [pos_array, pscore_array] = self.__data[chrom] + + # for i in range(len(pos_array)): + # pscore_array[i] = pqtable[pscore_array[i]] + + # self.merge_regions() + # return + + # @cython.ccall + # def extract_value(self, bdgTrack2): + # """Extract values from regions defined in bedGraphTrackI class object + # `bdgTrack2`. + + # """ + # pre_p: cython.int + # p1: cython.int + # p2: cython.int + # i: cython.int + # v1: cython.float + # v2: cython.float + # chrom: bytes + + # assert isinstance(bdgTrack2, bedGraphTrackI), "not a bedGraphTrackI object" + + # # 1: region in bdgTrack2; 2: value; 3: length with the value + # ret = [[], pyarray('f', []), pyarray('L', [])] + # radd = ret[0].append + # vadd = ret[1].append + # ladd = ret[2].append + + # chr1 = set(self.get_chr_names()) + # chr2 = set(bdgTrack2.get_chr_names()) + # common_chr = chr1.intersection(chr2) + # for i in range(len(common_chr)): + # chrom = common_chr.pop() + # (p1s, v1s) = self.get_data_by_chr(chrom) # arrays for position and values + # # assign the next function to a viable to speed up + # p1n = iter(p1s).__next__ + # v1n = iter(v1s).__next__ + + # # arrays for position and values + # (p2s, v2s) = bdgTrack2.get_data_by_chr(chrom) + # # assign the next function to a viable to speed up + # p2n = iter(p2s).__next__ + # v2n = iter(v2s).__next__ + # # remember the previous position in the new bedGraphTrackI + # # object ret + # pre_p = 0 + # try: + # p1 = p1n() + # v1 = v1n() + + # p2 = p2n() + # v2 = v2n() + + # while True: + # if p1 < p2: + # # clip a region from pre_p to p1, then set pre_p as p1. + # if v2 > 0: + # radd(str(chrom)+"."+str(pre_p)+"."+str(p1)) + # vadd(v1) + # ladd(p1-pre_p) + # pre_p = p1 + # # call for the next p1 and v1 + # p1 = p1n() + # v1 = v1n() + # elif p2 < p1: + # # clip a region from pre_p to p2, then set + # # pre_p as p2. + # if v2 > 0: + # radd(str(chrom)+"."+str(pre_p)+"."+str(p2)) + # vadd(v1) + # ladd(p2-pre_p) + # pre_p = p2 + # # call for the next p2 and v2 + # p2 = p2n() + # v2 = v2n() + # elif p1 == p2: + # # from pre_p to p1 or p2, then set pre_p as p1 or p2. + # if v2 > 0: + # radd(str(chrom)+"."+str(pre_p)+"."+str(p1)) + # vadd(v1) + # ladd(p1-pre_p) + # pre_p = p1 + # # call for the next p1, v1, p2, v2. + # p1 = p1n() + # v1 = v1n() + # p2 = p2n() + # v2 = v2n() + # except StopIteration: + # # meet the end of either bedGraphTrackI, simply exit + # pass + + # return ret + + # @cython.ccall + # def extract_value_hmmr(self, bdgTrack2): + # """Extract values from regions defined in bedGraphTrackI class object + # `bdgTrack2`. + + # I will try to tweak this function to output only the values of + # bdgTrack1 (self) in the regions in bdgTrack2 + + # This is specifically for HMMRATAC. bdgTrack2 should be a + # bedgraph object containing the bins with value set to + # 'mark_bin' -- the bins in the same region will have the same + # value. + # """ + # # pre_p: cython.int + # p1: cython.int + # p2: cython.int + # i: cython.int + # v1: cython.float + # v2: cython.float + # chrom: bytes + # ret: list + + # assert isinstance(bdgTrack2, bedGraphTrackI), "not a bedGraphTrackI object" + + # # 0: bin location (chrom, position); 1: value; 2: number of bins in this region + # ret = [[], pyarray('f', []), pyarray('i', [])] + # padd = ret[0].append + # vadd = ret[1].append + # ladd = ret[2].append + + # chr1 = set(self.get_chr_names()) + # chr2 = set(bdgTrack2.get_chr_names()) + # common_chr = sorted(list(chr1.intersection(chr2))) + # for i in range(len(common_chr)): + # chrom = common_chr.pop() + # # arrays for position and values + # (p1s, v1s) = self.get_data_by_chr(chrom) + # # assign the next function to a viable to speed up + # p1n = iter(p1s).__next__ + # v1n = iter(v1s).__next__ + + # # arrays for position and values + # (p2s, v2s) = bdgTrack2.get_data_by_chr(chrom) + # # assign the next function to a viable to speed up + # p2n = iter(p2s).__next__ + # v2n = iter(v2s).__next__ + # # remember the previous position in the new bedGraphTrackI + # # object ret + # # pre_p = 0 + # try: + # p1 = p1n() + # v1 = v1n() + + # p2 = p2n() + # v2 = v2n() + + # while True: + # if p1 < p2: + # # clip a region from pre_p to p1, then set pre_p as p1. + # # in this case, we don't output any + # # if v2>0: + # # radd(str(chrom)+"."+str(pre_p)+"."+str(p1)) + # # vadd(v1) + # # ladd(p1-pre_p) + # # pre_p = p1 + # # call for the next p1 and v1 + # p1 = p1n() + # v1 = v1n() + # elif p2 < p1: + # # clip a region from pre_p to p2, then set pre_p as p2. + # if v2 != 0: # 0 means it's a gap region, we should have value > 1 + # padd((chrom, p2)) + # vadd(v1) + # ladd(int(v2)) + # # pre_p = p2 + # # call for the next p2 and v2 + # p2 = p2n() + # v2 = v2n() + # elif p1 == p2: + # # from pre_p to p1 or p2, then set pre_p as p1 or p2. + # if v2 != 0: # 0 means it's a gap region, we should have 1 or -1 + # padd((chrom, p2)) + # vadd(v1) + # ladd(int(v2)) + # # pre_p = p1 + # # call for the next p1, v1, p2, v2. + # p1 = p1n() + # v1 = v1n() + # p2 = p2n() + # v2 = v2n() + # except StopIteration: + # # meet the end of either bedGraphTrackI, simply exit + # pass + + # return ret + + # @cython.ccall + # def make_ScoreTrackII_for_macs(self, bdgTrack2, + # depth1: float = 1.0, + # depth2: float = 1.0): + # """A modified overlie function for MACS v2. + + # effective_depth_in_million: sequencing depth in million after + # duplicates being filtered. If + # treatment is scaled down to + # control sample size, then this + # should be control sample size in + # million. And vice versa. + + # Return value is a ScoreTrackII object. + # """ + # # pre_p: cython.int + # p1: cython.int + # p2: cython.int + # v1: cython.float + # v2: cython.float + # chrom: bytes + + # assert isinstance(bdgTrack2, bedGraphTrackI), "bdgTrack2 is not a bedGraphTrackI object" + + # ret = ScoreTrackII(treat_depth=depth1, + # ctrl_depth=depth2) + # retadd = ret.add + + # chr1 = set(self.get_chr_names()) + # chr2 = set(bdgTrack2.get_chr_names()) + # common_chr = chr1.intersection(chr2) + # for chrom in sorted(common_chr): + # # arrays for position and values + # (p1s, v1s) = self.get_data_by_chr(chrom) + # # assign the next function to a viable to speed up + # p1n = iter(p1s).__next__ + # v1n = iter(v1s).__next__ + # # arrays for position and values + # (p2s, v2s) = bdgTrack2.get_data_by_chr(chrom) + # # assign the next function to a viable to speed up + # p2n = iter(p2s).__next__ + # v2n = iter(v2s).__next__ + + # # this is the maximum number of locations needed to be + # # recorded in scoreTrackI for this chromosome. + # chrom_max_len = len(p1s)+len(p2s) + + # ret.add_chromosome(chrom, chrom_max_len) + + # # remember the previous position in the new bedGraphTrackI + # # object ret + # # pre_p = 0 + + # try: + # p1 = p1n() + # v1 = v1n() + + # p2 = p2n() + # v2 = v2n() + + # while True: + # if p1 < p2: + # # clip a region from pre_p to p1, then set pre_p as p1. + # retadd(chrom, p1, v1, v2) + # # pre_p = p1 + # # call for the next p1 and v1 + # p1 = p1n() + # v1 = v1n() + # elif p2 < p1: + # # clip a region from pre_p to p2, then set pre_p as p2. + # retadd(chrom, p2, v1, v2) + # # pre_p = p2 + # # call for the next p2 and v2 + # p2 = p2n() + # v2 = v2n() + # elif p1 == p2: + # # from pre_p to p1 or p2, then set pre_p as p1 or p2. + # retadd(chrom, p1, v1, v2) + # # pre_p = p1 + # # call for the next p1, v1, p2, v2. + # p1 = p1n() + # v1 = v1n() + # p2 = p2n() + # v2 = v2n() + # except StopIteration: + # # meet the end of either bedGraphTrackI, simply exit + # pass + + # ret.finalize() + # # ret.merge_regions() + # return ret + + # @cython.ccall + # def cutoff_analysis(self, + # max_gap: cython.int, + # min_length: cython.int, + # steps: cython.int = 100, + # min_score: cython.float = 0, + # max_score: cython.float = 1000) -> str: + # """ + # Cutoff analysis function for bedGraphTrackI object. + + # This function will try all possible cutoff values on the score + # column to call peaks. Then will give a report of a number of + # metrics (number of peaks, total length of peaks, average + # length of peak) at varying score cutoffs. For each score + # cutoff, the function finds the positions where the score + # exceeds the cutoff, then groups those positions into "peaks" + # based on the maximum allowed gap (max_gap) between consecutive + # positions. If a peak's length exceeds the minimum length + # (min_length), the peak is counted. + + # Parameters + # ---------- + + # max_gap : int32_t + # Maximum allowed gap between consecutive positions above cutoff + + # min_length : int32_t Minimum length of peak + # steps: int32_t + # It will be used to calculate 'step' to increase from min_v to + # max_v (see below). + + # min_score: float32_t + # Minimum score for cutoff analysis. Note1: we will take the + # larger value between the actual minimum value in the BedGraph + # and min_score as min_v. Note2: the min_v won't be included in + # the final result. We will try to output the smallest cutoff as + # min_v+step. + + # max_score: float32_t + # Maximum score for cutoff analysis. Note1: we will take the + # smaller value between the actual maximum value in the BedGraph + # and max_score as max_v. Note2: the max_v may not be included + # in the final result. We will only output the cutoff that can + # generate at least 1 peak. + + # Returns + # ------- + + # Cutoff analysis report in str object. + + # Todos + # ----- + + # May need to separate this function out as a class so that we + # can add more ways to analyze the result. Also, we can let this + # function return a list of dictionary or data.frame in that + # way, instead of str object. + # """ + # chrs: set + # peak_content: list + # ret_list: list + # cutoff_list: list + # cutoff_npeaks: list + # cutoff_lpeaks: list + # chrom: bytes + # ret: str + # cutoff: cython.float + # total_l: cython.long + # total_p: cython.long + # i: cython.long + # n: cython.long + # ts: cython.long + # te: cython.long + # lastp: cython.long + # tl: cython.long + # peak_length: cython.long + # # dict cutoff_npeaks, cutoff_lpeaks + # s: cython.float + + # chrs = self.get_chr_names() + + # # midvalue = self.minvalue/2 + self.maxvalue/2 + # # s = float(self.minvalue - midvalue)/steps + # minv = max(min_score, self.minvalue) + # maxv = min(self.maxvalue, max_score) + + # s = float(maxv - minv)/steps + + # # a list of possible cutoff values from minv to maxv with step of s + # cutoff_list = [round(value, 3) for value in np.arange(minv, maxv, s)] + + # cutoff_npeaks = [0] * len(cutoff_list) + # cutoff_lpeaks = [0] * len(cutoff_list) + + # for chrom in sorted(chrs): + # (pos_array, score_array) = self.__data[chrom] + # pos_array = np.array(self.__data[chrom][0]) + # score_array = np.array(self.__data[chrom][1]) + + # for n in range(len(cutoff_list)): + # cutoff = cutoff_list[n] + # total_l = 0 # total length of peaks + # total_p = 0 # total number of peaks + + # # get the regions with scores above cutoffs. This is + # # not an optimized method. It would be better to store + # # score array in a 2-D ndarray? + # above_cutoff = np.nonzero(score_array > cutoff)[0] + # # end positions of regions where score is above cutoff + # above_cutoff_endpos = pos_array[above_cutoff] + # # start positions of regions where score is above cutoff + # above_cutoff_startpos = pos_array[above_cutoff-1] + + # if above_cutoff_endpos.size == 0: + # continue + + # # first bit of region above cutoff + # acs_next = iter(above_cutoff_startpos).__next__ + # ace_next = iter(above_cutoff_endpos).__next__ + + # ts = acs_next() + # te = ace_next() + # peak_content = [(ts, te),] + # lastp = te + + # for i in range(1, above_cutoff_startpos.size): + # ts = acs_next() + # te = ace_next() + # tl = ts - lastp + # if tl <= max_gap: + # peak_content.append((ts, te)) + # else: + # peak_length = peak_content[-1][1] - peak_content[0][0] + # # if the peak is too small, reject it + # if peak_length >= min_length: + # total_l += peak_length + # total_p += 1 + # peak_content = [(ts, te),] + # lastp = te + + # if peak_content: + # peak_length = peak_content[-1][1] - peak_content[0][0] + # # if the peak is too small, reject it + # if peak_length >= min_length: + # total_l += peak_length + # total_p += 1 + # cutoff_lpeaks[n] += total_l + # cutoff_npeaks[n] += total_p + + # # prepare the returnning text + # ret_list = ["score\tnpeaks\tlpeaks\tavelpeak\n"] + # for n in range(len(cutoff_list)-1, -1, -1): + # cutoff = cutoff_list[n] + # if cutoff_npeaks[n] > 0: + # ret_list.append("%.2f\t%d\t%d\t%.2f\n" % (cutoff, + # cutoff_npeaks[n], + # cutoff_lpeaks[n], + # cutoff_lpeaks[n]/cutoff_npeaks[n])) + # ret = ''.join(ret_list) + # return ret + + @cython.cfunc def calculate_elbows(values: cnp.ndarray, threshold: cython.float = 0.01) -> cnp.ndarray: diff --git a/MACS3/Signal/PairedEndTrack.py b/MACS3/Signal/PairedEndTrack.py index d8a7a85f..72c39d91 100644 --- a/MACS3/Signal/PairedEndTrack.py +++ b/MACS3/Signal/PairedEndTrack.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-14 21:13:35 Tao Liu> +# Time-stamp: <2024-10-15 15:56:00 Tao Liu> """Module for filter duplicate tags from paired-end data @@ -23,7 +23,8 @@ from MACS3.Signal.Pileup import (quick_pileup, over_two_pv_array, se_all_in_one_pileup) -from MACS3.Signal.BedGraph import bedGraphTrackI +from MACS3.Signal.BedGraph import (bedGraphTrackI, + bedGraphTrackII) from MACS3.Signal.PileupV2 import (pileup_from_LR_hmmratac, pileup_from_LRC) # ------------------------------------ @@ -997,5 +998,19 @@ def pileup_bdg(self): for chrom in self.get_chr_names(): pv = pileup_from_LRC(self.locations[chrom]) bdg.add_chrom_data_PV(chrom, pv) + return bdg + + @cython.ccall + def pileup_bdg2(self): + """Pileup all chromosome and return a bdg object. + """ + bdg: bedGraphTrackII + pv: cnp.ndarray + bdg = bedGraphTrackII() + for chrom in self.get_chr_names(): + pv = pileup_from_LRC(self.locations[chrom]) + bdg.add_chrom_data(chrom, pv) + # bedGraphTrackII needs to be 'finalized'. + bdg.finalize() return bdg diff --git a/setup.py b/setup.py index ec4d3735..1ee36921 100644 --- a/setup.py +++ b/setup.py @@ -129,7 +129,7 @@ def main(): include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.ScoreTrack", - ["MACS3/Signal/ScoreTrack.pyx"], + ["MACS3/Signal/ScoreTrack.py"], libraries=["m"], include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), diff --git a/test/test_PairedEndTrack.py b/test/test_PairedEndTrack.py index baa5b7b1..3723c944 100644 --- a/test/test_PairedEndTrack.py +++ b/test/test_PairedEndTrack.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Time-stamp: <2024-10-15 09:23:38 Tao Liu> +# Time-stamp: <2024-10-15 16:07:27 Tao Liu> import unittest from MACS3.Signal.PairedEndTrack import PETrackI, PETrackII @@ -93,9 +93,12 @@ def setUp(self): ] self.pileup_p = np.array([10, 50, 70, 80, 85, 100, 110, 160, 170, 180, 190], dtype="i4") self.pileup_v = np.array([3.0, 4.0, 6.0, 9.0, 11.0, 15.0, 19.0, 18.0, 16.0, 10.0, 6.0], dtype="f4") + self.peak_str = "chrom:chrY start:80 end:180 name:peak_1 score:19 summit:105\n" self.subset_barcodes = {b'0w#AAACGAACAAGTAACA', b"0w#AAACGAACAAGTAAGA"} self.subset_pileup_p = np.array([10, 50, 70, 80, 85, 100, 110, 160, 170, 180, 190], dtype="i4") self.subset_pileup_v = np.array([1.0, 2.0, 4.0, 6.0, 7.0, 8.0, 13.0, 12.0, 10.0, 5.0, 4.0], dtype="f4") + self.subset_peak_str = "chrom:chrY start:100 end:170 name:peak_1 score:13 summit:105\n" + self.t = sum([(x[2]-x[1]) * x[4] for x in self.input_regions]) def test_add_frag(self): @@ -128,3 +131,47 @@ def test_pileup(self): d = bdg.get_data_by_chr(b'chrY') # (p, v) of ndarray np.testing.assert_array_equal(d[0], self.subset_pileup_p) np.testing.assert_array_equal(d[1], self.subset_pileup_v) + + def test_pileup2(self): + pe = PETrackII() + for (c, l, r, b, C) in self.input_regions: + pe.add_loc(c, l, r, b, C) + pe.finalize() + bdg = pe.pileup_bdg2() + d = bdg.get_data_by_chr(b'chrY') # (p, v) of ndarray + np.testing.assert_array_equal(d['p'], self.pileup_p) + np.testing.assert_array_equal(d['v'], self.pileup_v) + + pe_subset = pe.subset(self.subset_barcodes) + bdg = pe_subset.pileup_bdg2() + d = bdg.get_data_by_chr(b'chrY') # (p, v) of ndarray + np.testing.assert_array_equal(d['p'], self.subset_pileup_p) + np.testing.assert_array_equal(d['v'], self.subset_pileup_v) + + def test_callpeak(self): + pe = PETrackII() + for (c, l, r, b, C) in self.input_regions: + pe.add_loc(c, l, r, b, C) + pe.finalize() + bdg = pe.pileup_bdg() # bedGraphTrackI object + peaks = bdg.call_peaks(cutoff=10, min_length=20, max_gap=10) + self.assertEqual(str(peaks), self.peak_str) + + pe_subset = pe.subset(self.subset_barcodes) + bdg = pe_subset.pileup_bdg() + peaks = bdg.call_peaks(cutoff=10, min_length=20, max_gap=10) + self.assertEqual(str(peaks), self.subset_peak_str) + + def test_callpeak2(self): + pe = PETrackII() + for (c, l, r, b, C) in self.input_regions: + pe.add_loc(c, l, r, b, C) + pe.finalize() + bdg = pe.pileup_bdg2() # bedGraphTrackII object + peaks = bdg.call_peaks(cutoff=10, min_length=20, max_gap=10) + self.assertEqual(str(peaks), self.peak_str) + + pe_subset = pe.subset(self.subset_barcodes) + bdg = pe_subset.pileup_bdg2() + peaks = bdg.call_peaks(cutoff=10, min_length=20, max_gap=10) + self.assertEqual(str(peaks), self.subset_peak_str) diff --git a/test/test_Parser.py b/test/test_Parser.py index 9c42b442..09c82c6d 100644 --- a/test/test_Parser.py +++ b/test/test_Parser.py @@ -1,32 +1,54 @@ #!/usr/bin/env python -# Time-stamp: <2019-12-12 14:42:28 taoliu> +# Time-stamp: <2024-10-16 00:13:01 Tao Liu> import unittest -from MACS3.IO.Parser import * +from MACS3.IO.Parser import (guess_parser, + BEDParser, + SAMParser, + BAMParser, + FragParser) -class Test_auto_guess ( unittest.TestCase ): - def setUp ( self ): +class Test_auto_guess(unittest.TestCase): + + def setUp(self): self.bedfile = "test/tiny.bed.gz" self.bedpefile = "test/tiny.bedpe.gz" self.samfile = "test/tiny.sam.gz" self.bamfile = "test/tiny.bam" - def test_guess_parser_bed ( self ): - p = guess_parser( self.bedfile ) - self.assertTrue( p.is_gzipped() ) - self.assertTrue( isinstance(p, BEDParser) ) - - def test_guess_parser_sam ( self ): - p = guess_parser( self.samfile ) - self.assertTrue( p.is_gzipped() ) - self.assertTrue( isinstance(p, SAMParser) ) + def test_guess_parser_bed(self): + p = guess_parser(self.bedfile) + self.assertTrue(p.is_gzipped()) + self.assertTrue(isinstance(p, BEDParser)) - def test_guess_parser_bam ( self ): - p = guess_parser( self.bamfile ) - self.assertTrue( p.is_gzipped() ) - self.assertTrue( isinstance(p, BAMParser) ) + def test_guess_parser_sam(self): + p = guess_parser(self.samfile) + self.assertTrue(p.is_gzipped()) + self.assertTrue(isinstance(p, SAMParser)) + def test_guess_parser_bam(self): + p = guess_parser(self.bamfile) + self.assertTrue(p.is_gzipped()) + self.assertTrue(isinstance(p, BAMParser)) +class Test_parsing(unittest.TestCase): + def setUp(self): + self.bedfile = "test/tiny.bed.gz" + self.bedpefile = "test/tiny.bedpe.gz" + self.samfile = "test/tiny.sam.gz" + self.bamfile = "test/tiny.bam" + self.fragfile = "test/test.fragments.tsv.gz" + + def test_fragment_file(self): + p = FragParser(self.fragfile) + petrack = p.build_petrack2() + petrack.finalize() + bdg = petrack.pileup_bdg() + bdg2 = petrack.pileup_bdg2() + peaks = bdg.call_peaks(cutoff=10, min_length=200, max_gap=100) + peaks2 = bdg2.call_peaks(cutoff=10, min_length=200, max_gap=100) + print(peaks) + print(peaks2) diff --git a/test/test_ScoreTrack.py b/test/test_ScoreTrack.py index 41eaeb98..7b7c340e 100644 --- a/test/test_ScoreTrack.py +++ b/test/test_ScoreTrack.py @@ -1,59 +1,62 @@ #!/usr/bin/env python -# Time-stamp: <2024-10-11 10:17:53 Tao Liu> +# Time-stamp: <2024-10-18 15:30:21 Tao Liu> import io import unittest -from numpy.testing import assert_equal, assert_almost_equal, assert_array_equal +from numpy.testing import assert_array_equal # assert_equal, assert_almost_equal -from MACS3.Signal.ScoreTrack import * +import numpy as np +from MACS3.Signal.ScoreTrack import ScoreTrackII, TwoConditionScores from MACS3.Signal.BedGraph import bedGraphTrackI + class Test_TwoConditionScores(unittest.TestCase): def setUp(self): self.t1bdg = bedGraphTrackI() self.t2bdg = bedGraphTrackI() self.c1bdg = bedGraphTrackI() self.c2bdg = bedGraphTrackI() - self.test_regions1 = [(b"chrY",0,70,0.00,0.01), - (b"chrY",70,80,7.00,0.5), - (b"chrY",80,150,0.00,0.02)] - self.test_regions2 = [(b"chrY",0,75,20.0,4.00), - (b"chrY",75,90,35.0,6.00), - (b"chrY",90,150,10.0,15.00)] + self.test_regions1 = [(b"chrY", 0, 70, 0.00, 0.01), + (b"chrY", 70, 80, 7.00, 0.5), + (b"chrY", 80, 150, 0.00, 0.02)] + self.test_regions2 = [(b"chrY", 0, 75, 20.0, 4.00), + (b"chrY", 75, 90, 35.0, 6.00), + (b"chrY", 90, 150, 10.0, 15.00)] for a in self.test_regions1: - self.t1bdg.safe_add_loc(a[0],a[1],a[2],a[3]) - self.c1bdg.safe_add_loc(a[0],a[1],a[2],a[4]) + self.t1bdg.safe_add_loc(a[0], a[1], a[2], a[3]) + self.c1bdg.safe_add_loc(a[0], a[1], a[2], a[4]) for a in self.test_regions2: - self.t2bdg.safe_add_loc(a[0],a[1],a[2],a[3]) - self.c2bdg.safe_add_loc(a[0],a[1],a[2],a[4]) - - self.twoconditionscore = TwoConditionScores( self.t1bdg, - self.c1bdg, - self.t2bdg, - self.c2bdg, - 1.0, - 1.0 ) + self.t2bdg.safe_add_loc(a[0], a[1], a[2], a[3]) + self.c2bdg.safe_add_loc(a[0], a[1], a[2], a[4]) + + self.twoconditionscore = TwoConditionScores(self.t1bdg, + self.c1bdg, + self.t2bdg, + self.c2bdg, + 1.0, + 1.0) self.twoconditionscore.build() self.twoconditionscore.finalize() - (self.cat1,self.cat2,self.cat3) = self.twoconditionscore.call_peaks(min_length=10, max_gap=10, cutoff=3) + (self.cat1, self.cat2, self.cat3) = self.twoconditionscore.call_peaks(min_length=10, max_gap=10, cutoff=3) + class Test_ScoreTrackII(unittest.TestCase): def setUp(self): # for initiate scoretrack - self.test_regions1 = [(b"chrY",10,100,10), - (b"chrY",60,10,10), - (b"chrY",110,15,20), - (b"chrY",160,5,20), - (b"chrY",210,20,5)] + self.test_regions1 = [(b"chrY", 10, 100, 10), + (b"chrY", 60, 10, 10), + (b"chrY", 110, 15, 20), + (b"chrY", 160, 5, 20), + (b"chrY", 210, 20, 5)] self.treat_edm = 10 self.ctrl_edm = 5 # for different scoring method - self.p_result = [60.49, 0.38, 0.08, 0.0, 6.41] # -log10(p-value), pseudo count 1 added - self.q_result = [58.17, 0.0, 0.0, 0.0, 5.13] # -log10(q-value) from BH, pseudo count 1 added - self.l_result = [58.17, 0.0, -0.28, -3.25, 4.91] # log10 likelihood ratio, pseudo count 1 added - self.f_result = [0.96, 0.00, -0.12, -0.54, 0.54] # note, pseudo count 1 would be introduced. + self.p_result = [60.49, 0.38, 0.08, 0.0, 6.41] # -log10(p-value), pseudo count 1 added + self.q_result = [58.17, 0.0, 0.0, 0.0, 5.13] # -log10(q-value) from BH, pseudo count 1 added + self.l_result = [58.17, 0.0, -0.28, -3.25, 4.91] # log10 likelihood ratio, pseudo count 1 added + self.f_result = [0.96, 0.00, -0.12, -0.54, 0.54] # note, pseudo count 1 would be introduced. self.d_result = [90.00, 0, -5.00, -15.00, 15.00] self.m_result = [10.00, 1.00, 1.50, 0.50, 2.00] # for norm @@ -107,98 +110,97 @@ def setUp(self): chrY 161 210 50 186 20 7.09102 3.5 -1 MACS_peak_2 """ - def assertListAlmostEqual ( self, a, b, places =2 ): - return all( [self.assertAlmostEqual(x, y, places=places) for (x, y) in zip( a, b)] ) + def assertListAlmostEqual(self, a, b, places=2): + return all([self.assertAlmostEqual(x, y, places=places) for (x, y) in zip(a, b)]) def test_compute_scores(self): - s1 = ScoreTrackII( self.treat_edm, self.ctrl_edm ) - s1.add_chromosome( b"chrY", 5 ) + s1 = ScoreTrackII(self.treat_edm, self.ctrl_edm) + s1.add_chromosome(b"chrY", 5) for a in self.test_regions1: - s1.add( a[0],a[1],a[2],a[3] ) + s1.add(a[0], a[1], a[2], a[3]) - s1.set_pseudocount ( 1.0 ) + s1.set_pseudocount(1.0) - s1.change_score_method( ord('p') ) + s1.change_score_method(ord('p')) r = s1.get_data_by_chr(b"chrY") - self.assertListAlmostEqual( [round(x,2) for x in r[3]], self.p_result ) + self.assertListAlmostEqual([round(x, 2) for x in r[3]], self.p_result) - s1.change_score_method( ord('q') ) + s1.change_score_method(ord('q')) r = s1.get_data_by_chr(b"chrY") - self.assertListAlmostEqual( [round(x,2) for x in list(r[3])], self.q_result ) + self.assertListAlmostEqual([round(x, 2) for x in list(r[3])], self.q_result) - s1.change_score_method( ord('l') ) + s1.change_score_method(ord('l')) r = s1.get_data_by_chr(b"chrY") - self.assertListAlmostEqual( [round(x,2) for x in list(r[3])], self.l_result ) + self.assertListAlmostEqual([round(x, 2) for x in list(r[3])], self.l_result) - s1.change_score_method( ord('f') ) + s1.change_score_method(ord('f')) r = s1.get_data_by_chr(b"chrY") - self.assertListAlmostEqual( [round(x,2) for x in list(r[3])], self.f_result ) + self.assertListAlmostEqual([round(x, 2) for x in list(r[3])], self.f_result) - s1.change_score_method( ord('d') ) + s1.change_score_method(ord('d')) r = s1.get_data_by_chr(b"chrY") - self.assertListAlmostEqual( [round(x,2) for x in list(r[3])], self.d_result ) + self.assertListAlmostEqual([round(x, 2) for x in list(r[3])], self.d_result) - s1.change_score_method( ord('m') ) + s1.change_score_method(ord('m')) r = s1.get_data_by_chr(b"chrY") - self.assertListAlmostEqual( [round(x,2) for x in list(r[3])], self.m_result ) + self.assertListAlmostEqual([round(x, 2) for x in list(r[3])], self.m_result) def test_normalize(self): - s1 = ScoreTrackII( self.treat_edm, self.ctrl_edm ) - s1.add_chromosome( b"chrY", 5 ) + s1 = ScoreTrackII(self.treat_edm, self.ctrl_edm) + s1.add_chromosome(b"chrY", 5) for a in self.test_regions1: - s1.add( a[0],a[1],a[2],a[3] ) + s1.add(a[0], a[1], a[2], a[3]) - s1.change_normalization_method( ord('T') ) + s1.change_normalization_method(ord('T')) r = s1.get_data_by_chr(b"chrY") - assert_array_equal( r, self.norm_T ) + assert_array_equal(r, self.norm_T) - s1.change_normalization_method( ord('C') ) + s1.change_normalization_method(ord('C')) r = s1.get_data_by_chr(b"chrY") - assert_array_equal( r, self.norm_C ) + assert_array_equal(r, self.norm_C) - s1.change_normalization_method( ord('M') ) + s1.change_normalization_method(ord('M')) r = s1.get_data_by_chr(b"chrY") - assert_array_equal( r, self.norm_M ) + assert_array_equal(r, self.norm_M) - s1.change_normalization_method( ord('N') ) + s1.change_normalization_method(ord('N')) r = s1.get_data_by_chr(b"chrY") - assert_array_equal( r, self.norm_N ) + assert_array_equal(r, self.norm_N) - def test_writebedgraph ( self ): - s1 = ScoreTrackII( self.treat_edm, self.ctrl_edm ) - s1.add_chromosome( b"chrY", 5 ) + def test_writebedgraph(self): + s1 = ScoreTrackII(self.treat_edm, self.ctrl_edm) + s1.add_chromosome(b"chrY", 5) for a in self.test_regions1: - s1.add( a[0],a[1],a[2],a[3] ) + s1.add(a[0], a[1], a[2], a[3]) - s1.change_score_method( ord('p') ) + s1.change_score_method(ord('p')) strio = io.StringIO() - s1.write_bedGraph( strio, "NAME", "DESC", 1 ) - self.assertEqual( strio.getvalue(), self.bdg1 ) + s1.write_bedGraph(strio, "NAME", "DESC", 1) + self.assertEqual(strio.getvalue(), self.bdg1) strio = io.StringIO() - s1.write_bedGraph( strio, "NAME", "DESC", 2 ) - self.assertEqual( strio.getvalue(), self.bdg2 ) + s1.write_bedGraph(strio, "NAME", "DESC", 2) + self.assertEqual(strio.getvalue(), self.bdg2) strio = io.StringIO() - s1.write_bedGraph( strio, "NAME", "DESC", 3 ) - self.assertEqual( strio.getvalue(), self.bdg3 ) + s1.write_bedGraph(strio, "NAME", "DESC", 3) + self.assertEqual(strio.getvalue(), self.bdg3) - def test_callpeak ( self ): - s1 = ScoreTrackII( self.treat_edm, self.ctrl_edm ) - s1.add_chromosome( b"chrY", 5 ) + def test_callpeak(self): + s1 = ScoreTrackII(self.treat_edm, self.ctrl_edm) + s1.add_chromosome(b"chrY", 5) for a in self.test_regions1: - s1.add( a[0],a[1],a[2],a[3] ) + s1.add(a[0], a[1], a[2], a[3]) - s1.change_score_method( ord('p') ) - p = s1.call_peaks( cutoff = 0.10, min_length=10, max_gap=10 ) + s1.change_score_method(ord('p')) + p = s1.call_peaks(cutoff=0.10, min_length=10, max_gap=10) strio = io.StringIO() - p.write_to_bed( strio, trackline = False ) - self.assertEqual( strio.getvalue(), self.peak1 ) + p.write_to_bed(strio, trackline=False) + self.assertEqual(strio.getvalue(), self.peak1) strio = io.StringIO() - p.write_to_summit_bed( strio, trackline = False ) - self.assertEqual( strio.getvalue(), self.summit1 ) + p.write_to_summit_bed(strio, trackline=False) + self.assertEqual(strio.getvalue(), self.summit1) strio = io.StringIO() - p.write_to_xls( strio ) - self.assertEqual( strio.getvalue(), self.xls1 ) - + p.write_to_xls(strio) + self.assertEqual(strio.getvalue(), self.xls1) From bff87ab80a8883ba882ffd06c393f3e2956122be Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Fri, 18 Oct 2024 21:20:53 -0400 Subject: [PATCH 08/13] upload a fragment file for testing --- test/test.fragments.tsv.gz | Bin 0 -> 511871 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/test.fragments.tsv.gz diff --git a/test/test.fragments.tsv.gz b/test/test.fragments.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..2b155e16b8f4171b84de95c9c330998a00064a2c GIT binary patch literal 511871 zcmV*wKtI19iwFpaAQE2y19W9`bS`FcVP|b+Zgg`lbaQq9l>JGzEW46yizPJ1GWwzz;-uL3nF+#$REi)tvg(m;6|M&l=*8i(qYwq2S z;9Rr)U)TTl|M`Fa-~adj{BQh^|NX!J=l}fA|Nig){eRW}$NwwUrL}gg7u0@k)#a_8 zZJ#3OSGkP3?^fpv`YJ{}RC}X1of)j{mcN{&pSfge>7S z^G@o#Y9A+IU-$3(L)}Wey%(H$t8RbPW9@lX6Ra}s-}hhjEW54V6VB@Yev&zM8Dl1_ zZr=Bta~Iwq(CYpBeydk2r!_&X&AgMjW}Uqy?906Gx#D5_^@8L6%H2x6UeJyiv2)Ma z*Gib%y_2&?OtFNRYX4TRtL@4-0a|nF+T!oq38z~18u}}N;bz)uZM8iT<}&Yl#nc_^ z1$Laomzie_!K&_k$5Wpc%ZOl9GcQ-X?OJz2cjJ4_GArW*nB9!n6${%aD`9psFJonG zYaKz4A!~knU32NTf#d$lHF_`S1@;ZDRbsIc)Tb5VQ(ap<3Ej<|7=JScU&3&+c}hZHDKVHz!Ypn!oV`a|f^#)9zGH!pw>%T#EjslG zO-tCrjKSEWVu>9IEB@Snt79LzMo*{@G8TXva(ab51YV=}H- zwt-l_-8(s>&vhm2ZGU@=I(9gkk^6(qq3Ff_`1@uSCSx0qpobC9IM?6V_#B+QB|y zoB%QDjJjTPFb09;Y|T?oxx)Ao6t_#A9v0Pn!Kv;^daGdzCWMh}z7C@EQupS*e28Xw zsh69Vr;l0NmC)S26VvjFy-ZLpx9>2C5YkqsgvfWogSk+Fw#tC~6n8mCRRYt*H3Gq3d`mEUC z6YBo=8XHSxubcq|$Bgg4A|MWG?ysCP=GT?b?5K(Zc#g>r|@lMn`qQHNa@E1)1wJnpv}KC_m9WA@+bc?Pzr;;hg}p z-Osf5lf-Hg`)Y!O?*Gex@3F_to6vbFAnqx(|F566e>MQ zzIQ%Jx+;5za7w=?bA=380$=E#v@YWgmH0qBxnz#>aC8IYH}_H zwmUJhkHvdVsK(bDiy*IgCOB_7ce7zMt{2Sv_x<NBspPA3Sxl}dkSl5Prn;Z9s(`bFG`TI)3a=rp?3J10i;5_k&MwkN$-JOo& zkg?)~o3PBiRGk>c+{sFk@aY+!$QtisQma7|yO*afUDF1=aYG z@m0k%mmz_IbE=xyhQKkj?aCKk>I~23-M|R`@!P)`0aS^>m>Mh zvahgpOM*SYgza5AXA)qVJM>Cy&87Dp(45~#f7>+@o~*DRYVB#jv2*A2hL>MpJy0>@ z`?W_xbF0t3V(sZ8;XI7*zxD|iz^RTC?{&=FiNpMsH3PKco;!x1&O5+%=Z0O+;;s&e zhjG8%!q&wj0=Qe+x;uMFPuR}Kv3tBaR%Bouv+snc&HVyv^)jU9I0@T%7Iv(DWt4M~=Rc@MIO#4|U^$Nl+i2uJMBM;4}Sr?zRdm8_?aI*NMky*gk-Z6iCtaN9{aNCq9kt1de=BUM-QJxm&E5 zE|)yozkA2T`-F~>2fq;f(f2(n%?hZF~eGdi^B^ASGrqJ3A8fm%B6n-i>*<| z48!XT*mK%pK4Mn^>mkyseW(|a(Lh+@?hT$%7AHpF*zp|(a_q(l{lSO#M@`I0B}PQB zjKvY6IOfFAOEB?lA;S515}NU|#Zp;f4jjQO=7i2ZJZF4M!PB|;{o4$|b`s{p>eD{d zu}Y1EZdUscmBK6*3}Zte!kv_`j4upPRiywood&V(Lr1o5Qme0-*aOPD%C@9>{_*mm0Ahge#>?0&~FF0JFnOyIk%jI zX2)P`sze?MsyPET0zsJsg7cv6f7OSp_MNcYERLx-G>ZksOBABr$+QqGbK}n1!xJPt zdFAR`y<#0af?mu%-*iXY)(edF7&Eu9xF#(3zKa58dcrjMJ0T(_za_Z*h8X{0+wx_kO`PF(M8B?uC5T zU|lAnBt)yrwg-;!$A(GOR{Vhp-NcAAl3fW}26{2^$OLZ1x4#mmn`xEjSS4Y(*$_ru zoD+8fp(X!T$8atCB)Di5L^v0K9*14`+;M!-iVWBl89!{y-h`*yx4&vm=7l*AG&f=~ zd}?_?v%gKSFz;K3JGU1z_F5dC1uj1=M6q+97r3~v);h7ySAw;x8X4tM2R!8c0Winer6mOe#5aKP%v^Q~hg86WEt)J@2nHx9{ zU;a~lI$k1RoBW-aU*jSQSWr#ge~Vuv67L1(=DNjX#4bsg51z`uPcp9ycT0lH^9oU% zTS6pok)w^Qr?9(U@DMBaev+~aIoE>aPSNogRlXVEJVbYXs;PpIu$}EgRBS9(3Dz>5 zEX1*4H&P9+ha2Fxnyn8T0$ApTjC<=?Pey{v>TTE=xCI5hAH!QEvv!WYT05e(;TQTQVJJkU*S z3!+2RwZO%hHgdn?h?}s@&0dJ6!ZZclSz+66d~a z>74|e|DaZkw&2O9^nMab39upq!&yqy0wS7Lu$&dPhN6}e>JS_%QVCpceuzeFlO>@!FX)3_LmT8DcK~JdeEENX*!lf=)CaNh#qa|z; z>4n>Dkaj6J?sOEQvY`UL!k-zKfD2)gxxO5NrkwOdaR zo!4&{Q#m0-BRvZN{d&kue2>9HQb9|2S_eOVo5FG}VY?Ih9}%!FH;ajOltm5Lv(&!I z&|P%EY$mG!(b97R$DJ~E?0e*23Z{GCJGQ@x%?3Q>!SuHpSLI6R?ha;?7@fb(f6cty zlrmwQ0LxiQA&T$)g5&J(5XE8S2;#V7Y)!(e2!#hYG&*${-K^iha3`LhiD$sa|KV)v>$4YX$kc*IeRbBN^tp??OX)8C;0oFSaPb~H(=M;uryo^agh=x|#&QXR9wsz;l%C#;9Ny82enPN8GLa-N(Jh0Qh- zo}xQH)YL9|ft_imT?d!Cpg2!u7!YF)$jj@k(A6S!D@L{r{!pqaXIElb&) z1iN#efp7gv*mj(Z)3hQ7#QN#RckCd#eG8ysm-+Iiw65n4nC32nTXw1-B)D;ci2A|_ z(|FEOy;{OBvE`kl9r6Ja%t<%4_NEe&f^OEq&Th@z5x6sVh?r{Q1t#X~uL!^;`yYP$ zjQ_4y{ck}tSsW7eI6?`gvBKhY#LA{8VbL;vWO5tiXA{(iI;`&}AwITW3G3ljs!5Ne3pb^?gT<~ z7LHY5yNrb%gCjQ8lHf8y5p5FW1l!!~brKCpp%n~sLYGK0tx#&pL^X9vc2HHP=k55v z_p}hje2fK6aLkz-(JJu(g67u2QsbkXE5X%aMYQ9kBrN+^;)P|61(sXC3un6X0_Umh zl*q+e7YuW&f@qVgB5+lsB;p1^@KnX1RCDseaJabN5x9F2iI)CK`Jdg_+4u)nLV2(_+J~B;7J&slXM(F@glJOCQm{<_lC2n;FsVcbvqBs9lBr}&l#dhjVXc4Awz^eGr?6c>S}-yTd-g|+b71znOKj& zcDq!iZZNYL|9y4qcdhaNI7P z+1U^VCT>B2t6>~RhN2|o1QUVT@rN9ZZ>3+lzw%X}Sx}9C@UKYF&CMR7erl?RyZJ!0 zN#ho5w^k9aMFgh6d7u!@t`QsHqOLpc*rVAn*(1z(hlQ2xT`=7~PwryA!GLbOa$Fjy zewARDy9|jai3KP`!>CDIyT|G|+;eNUN%eNOv>{rXU?zbd-{)MiH34gxX5jxy zI3`CD(V^&RC)D|JXIE-YklQJkZWdEdoMNq=`pz!Tt5c`v$|sr2d9*{c;&`zH^D;L! zh=`9&Xl@5=G>^pA44_SfQ^!kQSHg4_2Yn&sSs>Nf^;PMV|Dx!rV3=6v6!GN&*F7dh zV<#Lp({)76=3#$OIW3baGg_#B%=WXePys>@* z_LLE#LCS=nyHUky4%ddj^_}RQ$RqtWxoqwZhU-fTBX#Azxsw+`kKED-Ce~T7okv>^);Pusw%d0G#iH0Rf$DbGLH_yL3ESBaTup4&!9jSH ziL#*vbtA`J;CfM02c1n=Kph^Z-t1}#%~a_MQS51}Bv{Yci1oPQQv#a(Iy<(T{emYi z=!d#u?+RLZSRvlyGPSghr@8g{ulJXPkLq5r0b^L{thDfl!bwrfg0#pdxs8t9fp_wY@h#Eyt0n!ew zdQh}laNLSaCl0y^0^@r4@*nR6cgsk4s&@6ErpWnDaQSJ7_QF#MxQfRT5tRxYdm|b4 z1K%*=DU1439S61k0%viAXr25z+xymMxfC!eaNS8$6bmuXi}P_z4qVvwfUD0Y5vBvs zTn++}&V|4of$Ql^I5b&%3DZR9OGH0i!EqJ`Ed=S-Bd|WDsjRg10_WqP!1Ou#?W2Fm zztwe8d?MkP{4_+Hi%7wfee#|=ah4-qQ?Q(+G*u)u)ysAN>RBkR3*sbW_W3^|K{auv zG|nnok%6b`)gNk_-joE_(UR2ZC9kuRnB8fVPVXd!2&`uzM4L*@g8pC?yuS|lfcWqL z|K%se^O|6~S|6PYV9Kl~*cE~ROwLk*hqKiCxmPI{RxeQ9d50S}`3eH)p=Q-##Po#f z{K&NXqcTW>iIOB5a5g>3;X2>`Hh<P8E9 zK`rjNX`dViorGcX7&}g9I+>PLgngu?tp23J8TEt7X$XTGekRo zo9)%@u6V*IK_+;rH`e-zBgwg zZV&*MG2E%!K)#f~M(u3^qS?6tmkAn!q>(9*u$=w9w}KNB?EPi-6X*#x`i;_^RK5`m zcVaxTsS@-iSf)l`r#w9M3Ix_FIJj+@X+SlZAF)Q_abhk$z5|Mq_)TCA}GcP0tZ&}kH~D})Yb_}PGC|H>HCvN$Nl zak)qsT&7GJ!E|=4)c9kic4v|!SJ+9g5j-KHWKV+K(>NAW6jHEVYzvL^Ci^9@%xQE` z@SPY|pt-KtA&PaO&IA{QAQ2&n0vE3-5rtrZUCh}vMRAH2H2+r)^=uKiTth^M#$1A@ zs;%{{K5^2W3B&$&oa#!S2`<(`9>@4F39iRNhzP-40&dZr`l$A@z}akr^6+#`5V*YP zLF-zAIup#iL^Md!1UT|owfsH_reb}FXg)X-Tr_QnsM;|Sp6ZCyPc^0Lfbvv__2ajR z>6{7ObY2=4*US1k>Kg3cH;rA^NpM|*|B3{D#Kw4~fUV%E3dYBC#|kl9g1bXkEbrJ? z!Ze4iQj()|ttiVNH~O&xTlV%BFLN;OBkut-34iQZScQ$L4yi zgrBzkow~E*Y{C8+_jsosc)*rm_3hM)dSO?>ZeAVM)JXW*59Ut2hvM)AKYi;DYIRry zenw0^s0kPqoZ-d^qVfl(`zyJ;($^t!bpCr5WAR>;f)Si)<_=Xgh(`uWd9VuVTa9P> z1wVT%-2OK4V^>07=Idba4EkRJKfQ@}YJw!!NpKc*Rdu%O1$8_1W~09nTKo4JTM3g= zZ)D&+?11&Ht_9yCP>Zn&5b2;O*vE_$9Hn?f1k>zkmHve4sv6LX8z-&Ax#1+lj%nT? z1(K+3DClmjBF>BwAA)XdP2Sre_&_ktxj`bX&H;^ zKvtGf$0>f!7i@Dnsm4-KlBZz-$0kHM4;d;RHkrp1$SZaS}V)rS}aio?( znmtyk`GV;zr5OjPK3*{0b5Ft=b{CANGvMD&%9G3%-XN98x*LOYlag8z#&qw5&;!I;sMF|4#mXOUmvNqb+C;>c78D zS&cAc657KUeE;?)OnfH%yl=Sq?ZdGJivxJF3hIY?pBEgvlN^pv*GZ`E+;9p3VLPF_ z@lE&!S=2xbLiZ#@Nn&y2Qe}U8;@3*3ZndW$JH@#KKReCds^t?-sBR3ZmSmp^cC{za ztmKoBAmaW?_F(;Xfb*P%sC1ol0c&d#Gr#j`2ALs-%OMEgkk*Oh;)M>m}bNh5Hxmnt0(v4-(!$)@~cs`6&U7yt4V#G zE1{WNmF!keElJ>*6C+LPRjN*~&H9ZfhR05r#{Z4zjGe#zRmt{N&DBRqnC3J}llrDA z=U4mAfV%tbm#rz7=ESHAU3F}|lmzUaJ63pg(E>)dY7S~G;XM3y`%t&GQr&*mXS@Gx zh2Lej`UfxQ4|RV%$yZOgJ2e-m8iOROnDY)T*89=7oCFZs-);mqB>e0o@t|(6KJH^) z?w!m@L&k)2-1rV_s!ENgz}Rfsw$%Xx81}j8@ma2f{%{ib=x}}&^*96C!RNK((QX$&eSqzx%-xddThT2vicVL^ZQ~DvGKtSe?QS-W4 zeSl`XEo#fXPD?oEwi7qDj!=UAa8CQ^09_RNB&ZK}(SLut@&>6H4Lsdkztu#_srxRG zN9N`_L}BcflTh4x5~qG0wFSd?pkAGn_p6h#@k0g8Ld=64(zh@@w2jAmI|4UlTwuI^S`N`&v zJqWaiQT5T$k`S++aE$+i*H*d95Hz=b#{sZvcLln+?QF4MFZGKCo<`OCxmP7EETK9p zY~oO$voFxh&0g4Lmm4VhHwxZv6I$)>a=f z?3rTb&Whvkp%=%ZGT(y@@$UXb{Sf9n(EV>W0(}zN!y5b11#t{6X9dGVeS|1;I|cro zsK510aQ<&Zx)KGnaT;}O#q}hVX`f^btq{O@SYzMsWKw}8`x-VEyM;)h@PcOi$cVzM zx&(3LwofwlWHtcH++Py17c10E*ltB$lv!9O;b({3JM|_BPo48ffU@e?Nz|QmQ=8H3 z^XmA=Y7cbdhv3pzfv&*0Oca7T(D9Q3=cDF}T$|@6;p1{^)h=BDjDq8KDT><@#wKX) zT)Zn;^E2V;*6{Dyb(05AcRLzJxqEIJiuIl9)}Lax-hG-#GE~2#fc@Zy_)wDzwi5Pn zPeLt8<|Qy6qEg@Ln7#O@1=XDd_S!0EAs7#yoVPmuIwI%+xn2JKBw>6}ra(}OnQ2t$ zuB=tSQ>4JVKgo42b+kOHscYT|Jxt~wc)Cw}|8}7?R!gYvmHmQtncE=sRmtC8rMl=h${*B0Rxj{$ zI{N68f~EF?Vsg zaLn9MD~qdLujUr!Tpadv9JyytIL1Cf#2%P1%zXo`EOhfB_*pIGRz2h2UlQEiKbGSd zjBj9@_(3&!sDHXWbo<=Y;rQFX!QAGDC`|gJ{tZMsyDuLba8tL3fMIU)d3!|VzrgIS zgp1NjIboY~n%W;!>i7ZO?8Ou=Qg*Ds`AjLtKPZ(U=q8T36SYhLx}do+$R!p9nt`vn zx3}s=9sj$U|F`O}j7Lj&y8Hj3ZmP6T=x8CV8MR4%Xn8JAKd$vglXcxDGxv9)e@H5FJm!_la8v&53(qn zdTP7r8H410d&S{wT?xm;@~cSrP*wSWwcPs-)9a{?_KNe`W%lAQV9rsu2TJi8D~#&Z zG1E&zYv#FCy?XD2VeaQ8D(56rM<|IT%XD#Za$_TXQ2kE0YxK7wlE!X&~E0W6pE5r#uo2CHDXU8++i zrJMwp?L$R_PNJEhnux#<#jZ+qF`zj+mI%;=^#~j{FFWBzrzE(ToOlt0L%d*Av%7Xo z2R(_-^=6#VaBJudQjNpJoOLp@W#IwcodgCo8B}wWjyezTT3u*WhS<_dR#bd11x?UR{9xx|Y+Nr`_FK+P;lzaHPP$};Xy*oo+ezZ>QF1cD>7k z4HIpMXsf5(R0j|HB(u;yQn1|VKSWjar~yCcmqvJkyPuDva~-NkMJ_}!_fixo-0JfY za4u(`P(hLwN`m^ZYW|(`YmT{F!g3zJzBWxvPeL=9Eo58|O1KNAxo;p2y_4-Du+}3X z;zyPFRNUZR-QH^YY%c-LX3TFB1$;(Ab2jp3ab-mY+zl{QZ>Fj#liOQNL`kRWuDWFc z?#>GnY^Zl^07a&an*8SZ0+%;RcF4rBDzGOQt~x215A4fX1(cO*baDe+4niD-31hnw zhMUFI$j#LPeNISvtM=mOvQ&zY!Xvl(44g%=ku&y4IE=@s-_nW zs|DNK0EcMDPO=k*xlhxMMs$#1m~&cKm|dM|1l?5Fz#f&cE2zc~k)o{cGuu;i{)c*z ztS2}w#|B%M;`s^NM12(Hg~SRWSif)f{^$REs$-8Yg?a@*`<6e;79IB20^S90R^9?R)ZR@YZee!#a{rc_L-YG!_w3{58Z#6|} zXTr_x@|Sv3m~STBM2>!|d0eCX&@3rcuI#!-W@lc@OWZQqMIc@iZ zn;5?@H8rDS)fCjNG^NkIE;YksSFl9I4~`%J&pOQpF5H!%xeYC`f)Pq?}H z_)=%PvKQFr9wflLfS}NStM{eWl!Eztt@>7X)&EX#Gfg)E^GZ0sy<0bG)eYf8QGZQj@oyaDK-Of2+5Ge}PqgM|%BG z|E%(Ie($b7R-Y72Rbn9}^xscH{f3LpDdFL{+gm*pSS(oPuT*IgT z5o95h-=~bP-+q-n0nSbJyf1aEI*lA);O4gT=aUo)_EJ-T#^2X(FX+$T4$&d_H=tZM z9+7V~%Vw(SoZ!@PoR48G*!D?6L;nWg+|-!*Qp-7W3F=Mu;J11d61K9miCG6jw06yE zDQn-vEPScyV%xfa8U#+=;+^z_n^?JTHCF;F;imGKj5rh+7vEj#0!4kUw*4Oj?RReh}SQ5Ro^s0zkcU@e5*Ss;Dke zz)j@QZ}rq~1J3G2M8Ptk)!+5mzSUbr+ywJ?B+O5BtZJJ+z=E4-mEUTvx~f_m{PtJ< z{5pez&g_7IwPqy3!UJyngJ0?_XpwMJ+we=x`nOiXO$^kR8kah65OAKHI_PjkWe{LJ zc%a_zWKlwLCA8l+dq01BmPP~H1p7{=!Y4+;ez1M&Ta6QJB}{V~Mbs(wC)jSBkRUg( zI0VMCkfN~@D0FYsv~ueSPiK<1dQ#Z#BrIoZ&Ka+Q3NpYpo--9)Vj<4zOU{~+`dt6l zt7wy%mrGy1B{**jC7L!qCBbsLD2{^^=X}Wn){i@CdnS1VzlYAuH)TaGbuff4Z zP=80l{2Z}TuNPFeo=EhnF?E+$_x0QRIy#d8*6Jk-hY(PpUhbXjMvzT;!S6Z?U+UPf z3Cb57_jPvf7)c2?^*FxN@!C{}6}X9&`&P$lvQNTIP4u6(=Egxl9Y3Hw_>t>}S{F5f zn>f?2Cs|?iV#fmG_Z|ATdMgA}aNJ2?hvhZ?zSEk|ICa}KYmvo? z-dXF1uJdCg+~n1LspF8Ny0?J4Z%~;qiuwx1@A$zV>K3NG95{mZJ6`ikU1(Uh6P9^{ zh*YUrP=7}reW@u}UU$NiZU0eSjHcyTi)^@C+N@l5op%W{qsD`r)y3k8{{6lYP1O+r z#_!0{Uup`rWaYA(Y|UTl)APBRn(phUWN=AX4|eRw8yt&r7J}vf${q9TN@)JC+`85g z+*CRLG3P07HYma=sJ|n4zMebIPn~KKz_j0z2k?J9!}q z#Gn{;%rjoIV4L+ML@}211_$XUZk)uY%&KTO`_281DwpkDUM0+a$3T6(Z$xCrs)X5t zC;n4CNDiB zEH4-jd)kMZ1$ifIvllmtC2sQPRW;F^MnlAn=n<6ae|wV&lhE93pt4ICvI#deVZPOc z{L+MBd<7wj;e82c(Pe)-mZ~)qp4OAkYHpogj{+A<#Cacj=t50dMOFRoYVe_1jn2*T2T|h zo^Vrh^h-UdP`MJEM})Gw)U8Um$(s366J%ds_3-xZJeaRhg_=q%Dx+u4X<1@V?_?2o z`0pDVO6-+b0u!n^F*Zd7QB4AHUeLcH!7%T=N^t&fLb3W# zB}59`9rxRF7VT2N)A{nRn&sp$cUVq}xtD5f6!jQRcsNDZw|Xt^bOkqg8DHvCb>I># z6J_&P1jJdusL60903A5)E*isU(Ao@`mx*Ky!>3T6CSkf!r6PsYQvz=4Xy2&+)HZyY z4R7^ioqvJ-@*`#=9Y+K=)vLeMn@V^qVLjM`AL_GI_jcfGac2@*tx^A5aNG(p>HSRw z-hka7R?T-kJlbBQdeLjos44PVMnZXretSO&S3p!;1Kb@S@_noKoT~a4uzuHYkb3-8vlX|IFr9@*jQdgDMAo(k|6u)4Z|x+&`5kZn zTTQGSaEghQlZdDf!A;k)-)f303U0bo{8kTDLL^7YM0!ayRl%{T#Jy7wdNL&VZ;9yA zI~fX$FzUJGTy^iqZfWWKP{{)l382 z=^x913LAoB-${HGc@_ZcS%}?ZUhTZVjuVpK^xG8jv+rbM&ve1KsaXFtPBtBk<_pYy zTA1_oBCc1^oL6pf1*7_!7xC+6?!*p!(c=ub%kcYB$B~r)UBOM>(YHDd+U$Tpa~@V= z4Y_&|Ec+w`I5RJSo4zW))dUC)k0#B{XL}#$?=Gm23ls z^ZQVKwZ1B=wx{*`Qys5)CfxKY_*QesCeV-Vycv}_?(cF1wU;JvqcKLvmb|uR`VY@RRpG5WTf#%MDJ7&E0hJcvG z?zb!17)^k8*s0?!tDY&xe`h_iQqs~#aMMNWOHCDR>?&Y7ubf28d0@K{yGa|p^0(L1 z9n7bCai^=gx7I#Uaop>AfwLhH>0As9XUA@wCY;cL?Yy^?yVc1HXeMqZM5IP;LA5*g zrf;5}8*r=rUiheYLUX=eN=cBcE@;kLryug7QwnfXhxJ<>+ok@>fNKBBcqL792YAR* zdaDoD3?t#`oYp?nVJek`>iiH?+Ebqopg#BtKGgjp+XpB&^*FxNWvNwS%S24`zE2q4 zjk`Lao2cs$;XW=2+npOiq*ysYb!!y?FjLP>NQ{YLZ3MvR4js73EdQlG6mAsUbkX|x zEv4psDYePSX^OJZ8w@zk%YSgctFAMt!1l{ijF>YwP1K9Ybfy?_p8kR5ZdFcLbyH6g za8q6EOFdMlK+rCCS9iu~^2Y+z?G1?3V?kizdc#hOeO^yO5q;8DWc+^*8w@Qi=0-9iASi3SLUzNy7#B=HxdbK1pW7|`%e5!&Y;IewjXTtrNP|f{(Cy{n& z#0I!LM!K30N~#E^xy2%n@(`IJSoXQON1X|~KKLO%o;!^C^Qxf!R88W&R=q?C5Bttf z_35uN?RH+OC~-n{dAby0oVZE_$5@Xc+Voyc=q3`q#|nStiX7nb&{bw#HKT!>?&)9Z z)3rni+|5U-HDakaYVOl;3|&L-n>NPgOoC`|$thS5@q>TWs;n_~f?ZE2gNThRs3z7~ zBGm=0G>0&^%ZLu~C-k=CI~jF7>NG1@<}AfFR+usZcYjI8TWX;L1Teebjs>buodsEf^5|o?1Z{O;D*1QtV?@FIv>Nt`ds#;Q2-rZb>h+vBZTT?)O91?K^ z!$jvJQhQWEdzkZo=T3Z+T~&iVpgXG|&hed=K7#GuAmI?)?N zSw--1k_+sYr(gnC=Yrv$TOuruartT~@$beVG863XI%>QOEK|V6f_Cn!aSjQNn>(>Z zru_|cw>O00(6V+uXGZ74L!|yYfXm3q;{DXo29CKk43SE)CfxM%`cjj&qi;DVaA(zR zz0_^Fs!^24J%ro}x~UG?Y3EDZ9RN3xQRD1oMFy6Mm_oFL{%QS4C7J za#euybi4ei=JqAwDYN`Tt-gE-!`w^t8JjDIIUu%TW824Rr>k=wa8v90`?vWjUC?iO znf_A86RT^&#INYo>(Y4#s1LK@-8F%O3$`Y3(>Lh*uiVP>0>&`0pp*!!T^#}n<$%o? zoTLU}DFx8=&#J?uZVAWKZK1|_VUZU+RYI&E>ZO?mxR^5vi^mpx670EHBIYG<(|z|_ zJ#-zFJuM#Q{709HSSsmsB`~-7w40dg)pvrNe)HR;X_hIlm4iE*4eok@wH|Sp>coiu zzM2|=5?$=BQoHlc>~w>D!A(!+@870J&PcfF+4A+r*}2yl_QUC{*U#lhNa$M;Z^ zg@mUV?YDY0S_&jIXN84Lh4(Yzri19WdMIO9aO}5SQ*M(4bI<)(^y*UKzC7jG#tU4f z&rX1|IxhjP?iPI^2jNMAV%Mre%7@hZuH*Jkb&RRpNmPf>`R&7!!MTyJ-1w#>ByH0K zF1j;BgbeqD?f%O6y+Mk2g2`Z26{z?u3C&prOM#3f!R2Cw=u{pTXs)VGU#lsr>`PknRrimi!)RLpOJusglGyeXSd=s)` z1$N&#^-^;Rpj%%7Z8AEi41v4TmFT>{)lkc3%h*0ZbAGmSD$7p7c{nk)w>sv5`jZ@b zxw+>)Gv38Ya5d-&v5$k%NpKZ526i;nBn0j*W6%kkPAP(6<_@AmgF-qGei#=^s%XGKeRib{QVy$ORYR+0dmzUIDR;3B6OgWxIG@cr8{N{?n*UzhWL4=U}4 z`H*lP_J;bQ-d*2vP`cB-6H2AFmhe>9qJFAlwG`0k+WvMJ>pj6)kBG>gPH;OQEqw_0 zNO-E1`tjUbRa*gaD*w5DkBf9}eI3G-?1-JT>ud?`+z_JLdzlGOdH5xsNvDy-=@~=g{QLU4(ZEA{;k%{ z*p;xqZmxf;!$hLl4G@39`L}wktS`+(l{%d_BSeJTZRsSCk9sJjWm;W$| z{30dgU;GWKf!85K%~@*zAJeqo-nguC5W12y6j;d-ZRL{#S`qs$hNdm6a6spX{?GKj zcE*3zNDx>q6`&uN8wi`azK2*wrk}FxbS)Y1@?CQ8zk#1=9o&Z zx${cy{+WvGupeJ3m7h}VxkO+f8nFT0R~VHu+yBQ;}9vij4 z-%wDwA@2p0)}srUSef~XP|%OU8>z-SOw*PCh49A_tq+-TPvJ*P zxhr{0*+Zu?XEgHprqb%#I)v-Yx5@)a(dXytHh> zT{$OUoZ65pkF*!Z9#o+dc`JLvgVS+;0YlgLD)r3U_~%eR5%%I*ZKD{kZs5MCeHS9$ z4GXwk>C})QL{hZ+i`KP$!Fy}X`t~+%v{PI2S0jE0kJU=4VU=w7@&SqHVo5q?9=K1} z&Qr!X`o~#0Uga2ccT~{i{O2<)0v;CN^5>mJM%%8j+xoHv_YVKUH#$b4H;}j2RR?Fr z8Wvw0$ZcR7tfMFO7L+kt%LA-uyX0r(ElG|OOM$FYYs;%;ZTl+J+qw6SWpdS_M@w7u zo|!n}AT5rE+s9Xz>IDZ~5}+N66Z1t&~gnj=&UrAz3=WF14l(w$$Ry@I&sgr2n zSI2d1tZ{TEcbw%OP!fk+YhP{FrYhj-hjqcgt6Ax?G9XQ}{L9B z3z-{1?432?o0CQFBgGz}a*})n#iHD2JTdvk;ZXZ-m}qZoy4q-(z;K5y*F=CSwnYU* z2I0d0X(fOtsN8-|`b-vdHT~@h642PnVECYQyI5N^Q35jB-lPIL?sCxZzvIQ7(j(mQ zrdvs77C-_voUQn6`Sq@i8=^JU@4J!4l*X~nedWKx?0-R}(_;9vKg*~#Ux=?TUiHY;wP`1C8?Yt!r+fFgKQI3O-5~ zXtHBL;*M027ElRM;H}g~!q6Y4q9t<|M3Nzq_eBqJ|2niOo(qw80Mf9skFuADm1l%a z=*-h4RB8YRv8gL>Hn7JVsxJQ`acXDgWQgyZTx}SBYR6l|fBuu;WSQP2XMEsQ79h42 z%P!=xcov`In>^{FqIKA`go|g&U&FJweOh=UzSyx&!~Ta5N>Y!0w(}5b`4Q)%V{0X1 zwpBuqg=LO+O-kqnFkQ0;{C*?{GeZ9Bf)(AH+x>Au^l{BOJyq>ZDS7Yj`kILB+zM|+ zkfq74Ct)gR`I>R>E#pjAQq6EBbL=3OamsjXkO279vX=5Jyv{zTe2nuJ?|$v<--F}B zPrhGrfK7z3dv7nmyVYc z&QqJyNuhh&qfFnR8SaAl^3F`%KH@&e&MiISPw(1*$=;!~3`NKhguHWRuBHQ#l(veQ zBz4SB58mh`E*eqN1ZFEq#@2jnj~dFantZ9Ib;aX=?|%$(Y~v47CgB7HijKou zlR}SDGwIKH(UipgmmDqH{4IDPSE@Fmp(B%gqpV#pjXqi5T{f~QTbS}*e1{0$FU zv}sJg5DtEB?U+vb4hMNDROvO#zFKRO?alIa-y#Xk1B{CHJj51InaYc!otc4V4&IMc zdef?};5Vjt8;T=NOXjP6FIYYdfcnmv0*zl%&yh$C^qZ@!d`YfJ@mmQE{^MN#hhVTb zOa&~R0hQ~0gX|#Qib0GKvQFI@X9}G^Gm`+m*4xZY3Oo{sKX|NU|R|<*;7J z#TqR{(L4K8KHnCe+&+>;h@riH&rs^YEDPn27Wn5D4|F_#Eg^xAkwSv82pI~#!AmUZ z2$O+zdE8pFeL7kIVTfhE*gxIh$2(txtloQa&JDF-m3o~^9GuBmh|Bc?PhLW*@q2%@ zzV3Sio6N6+ih5Y=)_IG9$Ec(uMI_(%1YGOX6I4{@BnVY`5$pV}$tt(y5~Z5A z`>6*ec0}*#SP%yoX94Z_0h%x_BZb-0R=K7wB;6)NoXOj3zK33UfX`q~}J3gFh?tQ=0l|ce?nvlK4NK?Bi?9Hr`E=}7lzaaLL^g7DuXs-$r6>zR!4Av^)=ONm z)x{u+x;R^*;jag7s|!!d3`FyZQI)FnZ99^*(8_*qJ%SPZ3s})My$IG60P|V$>d499 zRN*Zu=2WROZ*2H5A@#nV#7mQ$gWM7vd}@lkQ4EkA%2{ob+^m$$k$Rxo>L$3=)9vHT zYst_9pKO3uTHba}3&PrLT!YOINvQADuSOHwbSLBFYxpVD^i&8EI2b=Ubr!zk2Hm~+ zhq1o`Hdfzch<2U)d1@bzJrh990%MgK?yj2v%av@sA9yoc( zC@)avYtSJQWNp~)*(3OEm#c@6+a)k(*|jb+^oV;$%+4!2Z5+j*UQ%vRrc@_839(^0 zB^UQ9X11I@pj2?@NR2xw6{HnF0a{r9{ov?VJ0?}FuOxGC1!>!!Sc~BOoOj4Ft}o+= zoPMwf^NvqY5!p#NHU(sgN`;d{dM}glMRtBh)4UVV?r|}o~{X)M&=(s}AodjL)B3?-_EJemIUW{cU6UszbX|C9NS^qwG>U%DmwA9p~4==aUqKQad!Y{U!Hs*;VFh+sZ_&?^6XK z0+T7mu;}-+xc&1EwFz7b3N=d%l89@)RVsg2EmB)o%7)c{mh8IHASD~Pw-cMQ5m_D` ztB+6u?dEwk1vCf9cVoJvPFU1MP&&(Nn&pK6t_( zbf4Fkfa7nt(0PAY>HG|8_}}`yZ8q@mTrX}_6uU(syBIzkvf`g=ju)2Ts1WSpwkMhL zC?^Vs;p0M22XoL$(u(s`FHy&;+?{Axk19MC#ll!nbYrq|&wn%({+~d%hwD@VTK1r- zUhxJK>tBG;EoT?VidFyA;Y1pIa|~Htaiel#qi8)HaWQ}W(B%d*t9|2%GQkP--*|4x z|2TW})LF#U^FXRgm-_s+n$=G$h;9D!C;_2%L1{HmWA0S15W7EcV#q#C`>QcfKXmC1 z&xiYJ#c=1f3O1L%LzzMU4eBc+vp{@2yb7x4tv36by)8%ssp@&){;bxX{S&Lk_;rVKh& zv#-yDU6Z!OOjq`UGZhnAH*!t38WdJ*+=AF@j#wd#i%-~JrH2&kx^l1;K(NZoFh zEF7LDI%%m(kDJyHMom5&5Q48+Qty_P8foQ9@PCRZ1)syijq)!OcyIn=O)MdxXX?~& zU=3nZz{^yqq)MKYi~PAFf^?cX>3%N~;%Jzo@xoI@9MhK-6;qE#*}UX0nLuupxhhm2 zsh3OoK-k{e`@|eHsbJdAHy{%Tk0p-!rQSe_D;j4td0M>NavN9{^fxc=JhYB-JaeRY zyROzr4gepNv4+$K3=DTPu4s}v?--kY@yA|~>QhSSt9I1% zBAZ3)tuVnMR~=r>&N_qn*Ven>POP}o!~C4!(Lb6Jh#B~UU8pfGevAXuvw*L9pR&7=hi}@27$!=TF&;M#zbm$_1lk&Gj+Vx`ny!!^L zsE^PP-b}nYtyo?nw&Bp@3Fb#x!HE4flAaM~Cd=349c_~L2K@doB`wFeO@ou%Z>weP zp`6s;?%{`!XUukgudqhu(=*cFDrok^76xEaC9Vy>8|>ID6gy1=osAl*3;L4qKr$kN zqM$5j(Un|NtKsI6SMMJdb)H`454R6l^-bwA797&Q^aAg^Boeo&0fm9i7hfxR+?XTc z@y@B8{O)VznrkBF^w!gSv_Zh{AwZW}IZKh;MqjiL)4_%p8XbL^H~bXO8CUZ?9uvAn z32-LGnBtDDtqU<4j-zfLUXrV)aJc-9;S;ZlGmqG95+=`P!IRCtH35JmMsk>@XPg`% z1;d@wM*2$(SfWpu!7(MFC3p2V@|D_Q3)U~0(g%~01WYbgyLo*OvoL-%&;_=FCYN!( zEA)uay*aTikzLwi@3ojM^(!4CEWYPa31|O{e}x(GDv?#{lp%P351_n+mc~@BFw5Sj ziyn2HAkm<3NxdmKvZ*JFCCKT5y={4D=N45!mHW zD%fByXRP!|@W`oU{1lRM)viZxDg7_iqm_F=tA)TT)PLPIbCX*?7?y(1>7}>kKYmMm zRpl8_z;0K=;eXlFrXBulB(^OMj@eEx%*s!rK1Mycx)f(7{pv0V_{gp$-dAcJqqKJv zHMT0%1eScVsVlh2`2hBha*n~D1B#BeuPdeWX-c5U{}#hJGeYo}%y`LCoDiZ0v@DVy z91awq$zx8kGj~s>lDvsRG0FayNK;B{8EN<@fzFR=?(a_j3(9nGuR@9RXPhM z+c%JZ5Ruz#;tGx)2EP?~pa6%@Xu?Z)i!KbD2jvKFU@1`j?;j#02{h|GX3`ST4Pe6ogL zhCQ&IB6<;1b(FEp{7IX6g^!HACSHo+j&mcvdIrYG3dROd-V-{L$<)AB<9h58_b6#dn6=w-Gz_Oq8SJWNN zfvv}+UlCg~Crrova%Q<^L;WL~7wRiwA_V_;Y^WpU!S1oq@Tn}Sm_vd)-FDjZA;e14 zj8$ePVf8LOVsr*1qu5e@MVj9Ndj;wTjAm>zfXg&s&Xf9<_}P65VwsHPd%8i75K1J1 zDIetuFW-O8qAHt+Yj4QRGN1(AfoL-gB(45Xr$_d?uHp_4p{&0q$I@ulboyzpl(wOR z2+LY>obBFs6!qnM9fiIbRMY(7z)uVo+);wd+|x+oRn)!_5$lQ&eI|iTLq?l_C9XRb zFc{?RgvmGV5j`QTtoKHJWCz;ZHf<)MTXGX?S@6;$_Br=-@J({;b|pYJIHsbMl$Uf? zW0wDW7V5YIISPj#VW&hFeSc;`LD-5|8+T#Z@8zu7$bc@pkr(fMenB=uLa~&Vu`p)x ziohh+Nq=pQuD0+@&M5wfBQ0@uiQi?1L8xDR7_?Lw1rANYu2_SZe)9Xi%Iy$}?`)!0 zWAHZ3^^4^SS?aKHqcxlh;LKZ--C2#APGVw;#g6w^5wO?3(DZ?(|Pn|vWOId3l> z{G)(=TkT$8!}dTpYOg&&)7L|hnu^!%0RG1gXan3P6dWYRO_6YF`k%xb%-z!{mVdU| zJ*F<|IWpf1*Q0@w6ay>8|85?R)-E=%d+C=>Xy4jn!q-~&t22k-7QXi5Ne02*4|DEc zWAmT?(|4tMA?{Qh77?cgzZlI3($I=>OX zQM&$aQ&-0M- zH{*Z9#%P-T-hg$3tY7{fi%sZr6`pRB=+xXG2sjMgQgrx)ZU9>9*-LtnJg}11!41k+ zn%BTtmnEK_!y<89^9@i52!HT#pQ2X3WfjETXYwAT4~Xy9AZtiF*4sOkM?PlA#X|+E zExlNp#zI}I%TH}eOp=Y?C%H;ys0(xpCK(;42>rr63%|NFpC7QXvc`$4>$w?U-BJg==NjlCoMZBOr31TPrtS4(voLx$zI-Y$dk4=Vl`ahTF ztI7?rMCL}&anM4goLMLydStOoc8V0gB}$mpWHQh{7Gm%wH}U7qPdLK8?eb~TVXz{# zpYde+8u1@?$dD5r1t$#S>VfhR$!_|OTIr3>%Qo<;!kMPI z_5xD{mOx)1pv`HJ2@)Gjxbro)MT)jH#^e@{50-CDl>Pnqzq*E|bmg;e$D5Zv)t`|F zzS7m^)2$bvv@Cj;X-%?U+L!p^1`?h|wI9kK!FEJA+Y2P($EJLmnhC2BjH2{uyjns? z+}fufM4Tc{u|K{{kkFv%#odWyGFh{qe!5$X9Rdct{SFmNOl0@u)o94Xm3-qe$i6O` zC!&scIqiQkLm5jYbxU4iObs6_L(cMqxmj`5PK8DChe*i9>K5RGjl8`DHes(xW7XU_ za$8^La!Zj}-Li&GeExLW;wM7@WAI7Cb-eP3d z5Px>U(}%y(7*)GRY>#8;R$CJqX{V#*IcpqE`1619K0cWDG|Mbb&!ucK(5A^~Th-t-vA+|>d{;GkA`5YdYZWgRqMMVr{>^HNr*e$ z=PM%=hK?o84sK-_Av#ru$62+Z9JQS^ehA=(5$~6Fjdl$*re05{dSi_LEV%5u)(`|Y3WarYEG=KDe05CTG-jQdIH-ZlixWR*o8PC6> zMi^6c2H!qvl!|4Ml$s8umGab1qUjcpz%+A@sxi>#Nik^XOUQwS0*4s)>0r;g>it}q z$`UBvEJX!i5Wpg*q~#Q05LdWrlu?=BB}@TS!Ay2`ZJD8mZlBL;J{&$_LoSIw+hiUK z-3rPOPx=n*FRwMvp^@0da#n;QALjjuB9v_`TxaG@ToX-xq4>F zQ(iQ6NXrB+9sVusVI(6?P^XfD?8d1YUQub=Ugl773GbwsHOqM`FV+4vpoG9{tI`N#1i z-LSZKpQ3&7x-Pt^hz$;Pury`k;#U}BDnhp`!SCu5X^5H$puH72d1=%&2N6ZNk)t}B zXRXa?3M7s*Z`WZ_Z9~u=PC}9z>VB54XrsU`PaBd6hrufB#^{g6hV2r7-o4F)Wa7)Y zyo`07w(r35iwg1Eu{%d{%O$BiM*KF)8YY9Bo4jImRrm;%;k@zsw-DgOkDylh4KI;o zIjqF!nn|o8L>;g7UXsu87AY`vma_@NPHRgUd(?3pfV!;l?*o?MES#P;-GHrmR4oFM z!S9l!Pk+d7eU6QKNOFDpwKGaKbvEeA@%bfaXkMQZUc~mJ>%1}kU%-&}f2+;(1Nweh zH_5?SzsS!+x1=kg|ASv+@3Rqn8#esk4@hlmT(!rmeA`aAi5{LRi)jIcGT#u@)V{h= z8D-_B{aB``0v2h~TLcEMhP<3LSu0BD2OXx5VQXGGcc5d+k2iV5r%Cj@w7y4*^ABvC?-{N!K%BN(xQaldY{1js~UDIT5!Hyq7&TGD`x$_;SV9gXD*Nr5E zEwOea2(`DB2@Y;C34K+ds<9{eF{*g!Qr5iW)OHV|f`1^$%i(JIEpXGjUOy>sZkjwL zKKrbmKZOw-&9`4jwKqk&^}XJCJZ_>;V-Cb*Jw=CB;~HPvSZKL>;Q`Gc*!bHn){gJ7 zePDY+w$B7MO+Q)wd#|#R*x?sOGM8#1I*mu*OJ~pp1Zt}nA9^YPS*A9-vyHN#GNOMM zAaCwsB8vN~j>-qu0fy7w+%-k~zxkxNtg`N7y*Et{Zy_mVM-%|*mpTWnz?G`k)|^I) zxX6HhP~(c3)9`obs=ze5Bi6wxH4_wq=4C*!B6JU9)iL1@A1W~#*YrHPUP`UKsCFo^ z6~&#)*ry6#*C>4CVq!3tzZ2;Yp(30dfFZS=E_@ztddQ(qr1~yM+eXadsR$3~F4rWFJ#LGnJbj6iHn8 zFvXlN{-~>VP(~&1P60krUJ(0(y9%pHOo(OSge@j~I5~cYxZKeB>th!_U+iyaz}=UP zJL)+wORwed#XRCmdt}#S?8vRkuAJf7V&Rh(G1~l>@#b?vDdlRRrJL?eW|fEX-C`Kr zM}BH;qn}z5GAI5=zl`tv{y6hxx6K>v1Y|tTT=Thtm3KZ-3b%vN`M{#}`5V^`&n$&& z$u5s)Eb8@Ns%dEXbfA-^NuH#i_}mG<{rDELSFs$B6>U`nZFBuWST&eclWE%Ueq)|L z1S74LVfWPEC%5G)!PJ*FgRR@3Wd2J1WEs1WSg%4W7JY!7k<-`H2gU9&xIkC>7QgXq zl<6#?Bkt18*}p+#pB6Q4_))m_MUuHRoRaCI?r|<2gunqmXmA$w`iRNiFUeT6Nu`c- zb{ydP@x<+Jn!mRRsF~Rh_jL~eggHZ+Qdd6A|s)sx?j5MLm?74nIQ)(JdQ?v`OXj z6W>qb-me)ql89hA!Y>l#FRm9g8f-&s4Q~xBD@{PCEt;1sg?O}@fwv?g^QBT2b4jnj z05h=BfV+q_*^BEhV^+LX(`(8gazOjzExRm6L|_b;aqss@qRCqI?G2BJf^%5NWJ1v- zA|DN*ty27mv7D90qfiaia&|HQnJ>Gp9h()yB74%Qp(a( zf&(}<@&&#@o3q^_ipzGesxF(rt+wA6t`jdJG!gHgL+#NlAA+X(%dO-&dDakZeSUhs zj3hJLMKR7h#w(QDGiA@9M`%*SOh5Z@ABG5>K2A{_t+l~t?r9ucQWoTV-a4ot-D}Vs zfBTpDPb};B!G|z@S7g_@P}1%vy$QoEIgpBjPXG2XfO2KBdwvPGdkuaEPD%{jzDsxY zf7V)#<6ug76Cmbf8&Y!f`$)t~`_De!24M(rE6-B;^N%g0FvM;f-gm!1F~R4!ST zcseVejEh3gt}T0f*Pt<=tEPOLvDN_-YSn)^8jaKKvRo-{(x*P_u6sGt(Vh0};~RIM z-3(FfebwVSi%Ay`j=2Z~DPq2Dm8)}2SA)8T&}7OiLUOASSM+btQ9zLh#rWYe=@U~0 zU#v#O0ogN=98ja%Gd4|r3_q5${R6_f4q<(|in_UdHKq_U!Zs(`8Pcck;Z3Ga^Nb-J ztV+%%jZz4b26dP6bQ_%YRd=!Q8}W6ng}!?8E*iKe3gEmR+%}wbB(okZ?Q|B5Q@c48 zAdt!GruUQLbIy&f=jP_IAcMCL=75v|iwkT&h*Z(eD3SVPzYMr}T@AkfF7A`o-TRs) z0JWbmK8>4lM3?W4;_03V7k8yb(XFsKv|Bo*kRc?cp!W0Q!bV9(!H2wTWy7T`ywH1& zlJy{G&`$-%Z+a<58GR*uiyQ`=k^uw}@E_9v(;V2TXjBU1Ub?>ZlVDk9-f$YSBC<9B|QUNY%(Dc^rd zlo_VLqsKngDodJ_(=s*a22DP>tj%pZy_^5@-jFxAkqDKJH?ez5^7-) z$+hw>CL(u`p|{xWxW6iDv!H(DS0QWYE;S`D#}pOvAEn%N{+0l z28Bcu7EBbbmxlPx`$+8a%8~=~nJUVGQ@E{zMx+4~C@@TGAUt_hh(dlU3Cg_dg>fMh zFdL7}(a3M1Sl7Bdnk)sU+F{Se3r+(#{j!(LQPa*1p-$~h_feNk_B491#mI;liC@uv^RwB`^sC_rngZddk**-B55?lF|r!@dGFN=YX>NHg_|XP zzo$z6TJl%+sN`AWVh6fI;jWa0N_}Q@5L8?(QkMD^WEZ{`*v2`1gLj%|(-+=KEs4he zasuf84~4oEZ=K6?P5xWc2wCAL6ht5avt(ULegVvF7kPJ-73z*(MYJj>t&%5cs0P#& zXbXPXye>>im|hyd*s6-xh(fCXGcEti0YN3LkxJ%#DjR3`e*f=eN#UJ6DzCKXxCF|> zr`UF>Y1Q(6hP=dxkwaq+TY$~hExdD0`s+tmJMKYLg*W0Gde0K9lQpIx@}ivBUCX@$ zZT1UEF&KtiO&v}a(CC@_ML;2Kqe$adDOrd6$R~JL! z5nn8^Qyf=X-t-&u8Ex}Pg0pg;zOC+SJ14Kioy8MW2w+#&ZDLdK<*4`fxRqv+9K;by zj!k!|NUC8xD7#Ey`m4n*onaNSlYH52t>=~gli~iNJ&LK8 z+3v6?5Y2I%$EU>Mkqf8_0!zyYtO@ci^d%3S26#-6dbvFaW|u-VMS z=6}N-r314_FHPom3;$E%%}?<)Wv9>Awa)vWIiakVIX`{`vb!Cql#iGWT(|Nxmu<8+ zfx$nZ+>dDWXH|DeVAzM@t?gaa`fc3G3afX(Wq1zt6aeL9_t!ZHzTCL!p;+V%8pP)- z0e0W&sI$_Nh&NZ~x0A(h-Jj#0n27S^CSgx|;ld&!AjC8Dcy zDX?*!_$q>LBHY(Z-oD~peU@X8rP+2uuX-Kz6SV*5IoXRAtHey2X5tp+g;?yyH=XLz z^cv}WlI{4dek;t9n|}zhA)e=z%ibjvCaf=6&M(NMw*&#??d%>p zaqR8+Y1sn>fMqR0LmHQg;5{9=iMc#j(tse_p%DnDLcj$n;-my;R9`kFul^{NPpote zM&F3NNH+`uc(a5PeXW`<$N)~(>KSp+C_y>VEE)n-;qUYa!a8v;65qnQ9-bs4D8o6A ziXIuhmIRxM6yr`F>Ie@2)~@E!uMv9TBHh>DYz5TwL_(t32qyBVeR{keobUD$4N03- zc_S{?1D}YG;wW^@uYY-3ak{0p=zAYW-*{k>^0K3_HlXP0upt|c^laTDyt`fX_beT#s+!T-Z zqv7=1T|1h&I7tQ$GAw~NuFnXWEw3@4nb#)za|pI(OdGjIz>ta`>E$b~LStPSlZra9 zupL!JctA%$8CN2xS7qd%Xo@H3S1%Wrk`Y+V6PTH(O~nR zBw^Gk+>HuiRfP+KBc?_tX-%g6t_LDVfJhs?yfb7ruagc8UY?A3Uj zTS>I5v#LL*D&r;~NJsI*97LBa@vdI0pkB^&CkZf)rku+o|L81u$hy7s8k26r{)JhZ z0#61$0}?*u>!UDnkM@cg;1Q6Lj7=*luZ5>UVEYWXcWCRvI@fyU4foGG!o`1~V}y@y-p2XoCmyS;`>N&C1 z#>1IQk+SY33->M#bwnq_&Z+xm7d=Yd>QeDpyxy!LjPGx=LIc3b-@BH*%2G0?GGA-d zdpk=uD{Gj`UoLsLOhSY}FzkXKPGICu0W=<5`hOlO(6yF!A_uB>syE;!9Iq}+Z2 zflL?e1;nl0{n~O2w>r{GoUS$_A267jIeBI{Y^bcgrYg_ zDLK*MlCVR=E!#E!IjhHxgnjr2SE+7K{9S?$>*=Rj0|X^~HL9hfuVYGG$sc9U9Nie` zGSsj(KVK#N=G=x?B0XGx>6{{-wjnW8u6O#%FEeaU5%2kIk|s-f1;}J12GSDlI)ZDM zQ+cUM@i%FeyNWrT)rPR|C($wRPa|fU`5(8QZjPTs_v(klX6$*Ex;*_xXrb6DX4K+( z1X{eF7Y`f!SypdIWBoY^@=rMATwcpbM@sBC(f%s%vtwcb>>a=Kj>dtVuq0kl^LeP~ zEVB5fdDN1bzzT}4j`94Z5WsnZ8;#@AkY1x9WJcj~AIC};1Z_o81=To27)@7|Ty+DB zg6TkM3m`Ye$n`HVLyah|wrpayrb~(T(p;zxK{h(4J?B4C4^fKrEZdhAM(tp%U(*W- z5l(v8hD`5R_#`L}%vD|+jbuGCbZk6TP);L#3WF3J@FCQ@YO__BdZOH(9B_SJFfp~~ zoe$fD-euM?rQo@NE-h$GK)?hyAObyZ z{%rMTkLAPPzc|43I@*0y-5cG0W}r*|mEviQ`Yi4?Ci^BigOynfBKnH;OV>9J{Q^4% z74N9PTW^fGA<)@t>U+ut`k50ElDOPQ|8y%tEGdhVG-}>!={`sRO)H8-q~SAeKkBhI z*4q==;C;cESCiI3IGT<3w<_YltG~OL1b)aQ=br~WGk%}f-D{bHPri=b2a<2U{8k{( zh54rtYaO-F_mo7HU@Ce1&|CulDh+P8QT`=rayYcauZ!ed=T^DfReKia7War6dif>Q zL?b&aZ{Jm{qxN;pH>}RSJ_7xgmdDwDJ>;e{WHDVn=*87>l|lj-ntLq??T_mSi&;j*7iKgc+aR*2f5^b3%j_)*S%DZWm>MP z4gVUG2DLKovu&-RZq6Q!Xcc;;%BEUe6}7_aSOw+zhp^;e{X$daqArE%fU-yn46P$Na2#% z5Xa?WQHS+wi$TNAhBPgY#}+0P5@BY6?Wd;JVcb7U*c>Hu_E;Ub{V13&(%gm7=0iX^ z2*nK-XId1Svvi~d^>5+muw0%S_I3UQsMeA%SHzgT`ef4#okPFio9sw~Ei{UJjm%T0 z3Oc5W;V*aoR=pMzK&cZ>%w2opYu7ou@J+vely3Zud;8K`!HHO}&pRO10 zt6q`3GP<$!RG{Ms`;-KTD z#K+###_PS%6d!dL9bmFUum|>(mE@BKALg{_VylijPMKWdmg|{l0z@g)K+YxN*9o zxTZYb^Emt&SSP16*5o>9Gotb8`B8@zA9LZua(AF^tj8h;eEL5Yk}wS8~)j4oF5f9rl} z8*LfH(>?oNArp=Q5$^t)hu#Vrx!1zx5>SOmcpGT@&U1o-ke~OVEZCyp*H3A>GhC|Y zAHyb|y_^4ycG-5arRnBYvZGUP;n&Ge7&_2|55C22rl^L4#_G@#O?|KSL7_3M4V8$3Z(|!g?4n#?VSR6#5%`sOEX0_D8 zs7caqq9j-80{dFOy$GX5YUj~96BG_bQ6oOt##GQW^g)EU5&PVy1yfEnVPLVlT*;7- z!i94sU2QtO&)+>m8BwD3C$bB}YRrJ$n7i1%Yr0d|9NnczpcSQia`DD<=lY6b<46kB z2pNqlAXE5-ySNqAKg>1A`y{z-esPJ;%^hhQxUv3COpsz$-Dvb-nX+@!iE@q)EotVu zMyGK+1#3Z0mr)aWP+^O8R;vBO-RH^03U|<0MUY>Y`0{)w^Bx=t>g*QhAMC+d_^bq^ zqt*qxZ$CazNeTR1_J`_))qLr+)2lVaE^$ZzuC$?mjONh%fqV@sr-QvMEqDvaq} ztrp-xGB#~~oepXgTFHC&+?=tdc2w!v$DpoD{G828ZvnIlhNY(w3j? z(pTjsee;^)wM3|&KSW-3sxvKgh+B@%}z z)~bh;7$=c5+<3A}qsC@QJX6!9y0;(YDD>_EDVNE6{mvUddEi!cz6gpE$&XmI1})AA z*+sVQ<*#88(?V;Ajo-H|)B3(3J6Gy+W+&MkDdLq;CO^(0109qp6D(gS8SR9HI@1SD z{2rX^a?}3Kae%N-3AKcM*KK4e4&H`fDzE|S-p1im&!F%ugA1qeiJgbgU`cTk-4q(^x!h2zg{&kwTR?i!OyH-EgjP?+plli zPy5e?W6r;8&b=5T6l`S>A1+k~fzn7pjq7ai)$wFxug~ zR)p7yKejDsxCpiJA_osfQ$N+E2euqGxP1NWVDPK-x46;9e*$HOoq zS)v_MKE$tG5ex*UzWpM+w}KsICvk3Dt4q+@Hm~+=+gg87shp9O1C$&Y{sz3P-s5xp zvU8_>-NX+*r1OsQGXS$8t0s5Nrg^SQMMTzd73#F!qf>bjyAU@gFduP7F5Gq_9Ayqf z(Pbdx#UgGmRB8LZe2v((hPJl4A!eeM^b|(QPUZ+NbQy=kTg^AS@gIK9?j4>(I4{=> zM!cwfJmJ=}bE$+W4}(=7UD1P|Iiv%IE^iHxmpplLn`QMR4a4M68S7qPoQ*f7)T8_5 zcYlHltZ0Xrum>z2`YCKeoIJZV0T9kUe>+CL`qeYYnlA_&b|k4>R-l->iWohJ*n^{?9;5NCcHdGru3)V*(+O1vlP4WHrdt9K#ZarHgi)+_YJC9BAdceK+yq-5krS`O0V4#Z>OX;s?~p? zakzY(vOZ&ejH=t3rDa`>=o^FIH>jcRF2?(w+(mJ=XphO)ghORx%7AcMYW29-)i&~> zqZRfJaX@+8_$fwl&!enafumhjL?e_}4)r zvTk;4dit-)ceZb&A-6@MQye=k>6pRu{?=H>PSaerVx@dZ4u}fxsEU5%(G56n%r$@T z_avzHzzQ+3t^Dgfj}}NJJu4P1Tmj4H#ZoD#J<<2#=!R4bt?;-2nrxMmk^b*xp6mf@ zjvacK)uCx2Tv5srtF>|u z9-_F^`{_Yizm?tgw?HaS|8_AmP6ye!9|q9Aak^)*qKvIbh*;-g*s5*Ac0u?qRj=1> zhXYDRwLSG_|J)~qj%4Ci5dkq!XEamc$SZk<{>K->fV_1&g--$xpjFbJJJmDc+`uY+ zIg3Edw(q{ZoJDr0+IS*Yr8F-UW%Rd9##`SrTTVU`9;R@(?J(4y^nl)KH?(pSfja6$ zj;1~fXmrD-J6FaSq63~6Qd|h%j#{{nC)6ypA=jW2bvW4n|8gpP+YX&LNN{+D1GiWK zk(phyW^4yR7~*q*P|?GG50!BROlfX_cfk#Y_S$RNJ6azl3V%_7BJVhD2V#^*#r^u1 zzd{W}Ela0e!N&m`55^kNnY0`LCqF{fmd~z*aIoIaU^D61l<=yl)otH^yaQ z_Xyha=BM@|vRBR%ou(*1h=W#O&pM|qx-%Q`w%V%KVkH~D29l#a|1F+Zx9*F|zbOvZ zrT7^Hc>f;V-m?arQ2<*?|9-&rHbM_{QcfKP`e|!o zRK;zc_(vA6&JqE3VMqOK<*N&s2LmhFxa@JJivG(c)*gG_xA&v89oLS3JpABwGh~iV z!6EJ%1?-d_)$FCe4s5yTUav{%Tq=(6JH5T{czbmYc~@9(x!SDD5bk4oN3bp6T%+}~ z^dPi^!>4@SYe9W?JJbxJp#Ji4x9~$5DcKWoymLF7UY2~8BsFZkx&|Uv-A=!3Sfw`Folw;t> zoI3+1e;fW%Cw{&t+C$JvZEoijm9ynsIvh~MjMmWjuP85?1sVG=tZc!)q32@kyHL^P}pThn~hNq>^u%|GO3NfjCeJ2pd);5 z`3?A6B`5^RHzoKvl*+%fMeYzZnF)PIIn(6;n!YOC^Hr$@{8w~moih{*=5BdU5|1is zfJBPP7!#^)H!mu@Q^Y#Iix_?qDH(VAR1B4?UDRZ64YmKy-g2pue=<#sMIQ*|TThIQ zicP2~8?|`)ef@n_!!mbB3S0L=tp&p?h$nn9vvs2D9&a)Wn*K*P0~$0Ne_zR1D^#C- zY?^VQXgV)|civ_>mX%J#o!25$`EA6&xJO8~Xu-0QW1kEf>yBc!Fb9sy#x2Qq~d21uMilHYR2zhx`C3t?@@^d^f_UT;0o5iak z)py2_fGX8)m5{b4tWSz5%3hQL(E$U`jQV-hl-^ocu`(b3%io8) zL)Y1KmF0PvB(BS#R`{c)v5(=w=h$NOmbbJY0)l5M(v6#F{0)V#D~n(8m1sj-vkJ?9 zZV)a2MdOOkJeNzq!wa@$jFe2WFSi4Jhx|50=n zj&T2f9GBxTb!u{Yn(5rcba&@8hl$g1Zqv+6cTDG)?%G$5>2Y%EaF~fX@%#M#0c<|! z^M1cx&*$UucMWrcvfOLlZ(8H|z(Dnwq3R9l{WWmtEH1;Z?h0ca2`7cK1IK#CwE%0L zf!og?k{?;Oq%W?3%;x9?Z0|}$id+vRX4kP%KI1L(dQfnOEr5Z20>9UN5m8AO1-v1R zsDLl7Zw=abb}o(#nuV*+FruJH3oS=f>KQ^2eeJdLb_HlI((Vgf^8RKe9^l$5k3)D( za05$Sxolb1c^nkSH*KS4F3?7{Qs26o2N88g3+$Bfmw4Y){2I9>MfT#yp>Cqsv=1Si z4wboWwjGoT3nOkiE|9#*0o)bfeb59%f$IH9Z|Yy4S;(sCyMnF^RWRwEq!s0&CrF>x zN0Cpzlm=m1D-Gh;Qc|(}ddo41To?b=ZH;`en%HYIk5U232cma$qEDYX4f=`Uu+S#q zCtIv+3z<*nN1k$bh`Z%{JjLlU)}kVtewvm)d~(qeJrMU|84?=9Zt(jWvp2-&&X_v% zhH6c&d)Z?p{D37#ZYX;ALKPdhF8 z7A!@)$uWQiIK_+<=yHGwU5tV+v9)(SlD_BDy(P@E@lB-|Ru+5iNc@8-%@$(q->&;Chcv#nosi@>7{huQEc0;Z!*s%Ug9*$xm{{+2rOp? zLFC4=Oa|XwZB~6QV*p*D4jMrYCZM(Iv8qqMCi=Mm+J3D0QuWb&er`mmT;K};XtMZd zyyKoNP|2ZAu2*pfj>J5kZ;JMgAr^t=rl{^%xIv+Q!OVYO6?X>3z1WMkwSnxH!>7K8 zs}ip$-a*4^*fnnI4bRbSDxt5Z*D$*z>9PYK2}vks@)tC&2tn^)dv~0cd*3z4JVJ7r zu6)udpM);m^%+h4c(S}Ij$|ht6u3bJUsxnH7c^7oBGySu)GlQP0(j_3#-3)+qmF}L z>?S*7E^~x8GP0vRB0Cs;p4t{T9l0}I@4RDU0opLL z>sz2;Wv$1!sjqui%28FtE@P&C>ZO+YNec-R&{bc8e1b*IL(P&Sn}@TX>q`A* z1gQ!51zPH#v2kNu{J%PH&H@CubEJHRpRnB27eH&2*-V`lJhGCduSA~r{_Jy(R)Sw) z-go^$=bMK_7iDMJp>*gBtOSoU8|)nGQw14i2rms7u0C&uUVmu6_srwfk64oF)N2!) zR`VA674fFO!Q;2|=}%}(h{?3CShK6K%_#dI(Ej1V;~}biRA3i+F$Z~7_b>Z*kM%Ng-Nayf_7;__X1-Jo zG_)!B=@&fdEjWlZhN@{fAP;r^wl1P_=qrzaDMv;n<=4SI?Ss)r1`hFzYh2sb?64R( zhZ$}^^l>70LwVR{<%c5_Qgx(pkH9^@PD@mtSAtDwdbDuQO*4=RPs@VD6ONz0S1;sv zM^U)>rV?f=*L`~Jt0Ok~NpKBiUcNZ-{~9>dtzI8oQ1Dw()-Veg+~8r7g~@@F&tHH0 z%4y*U`-VrD$qno!xmqm-%&yj407`Ab#U|A8aKx-XfWkOkm5K zPeCc~YZJqL-tx(S4zwTTW*2VrRR;r_raqLV-SlbzS~yQ9y71mc7m@utMRf+zYq0ZC z@zhjaI2`@KQS+mO;BhQF>QfEByl?@1oNB1L_YtVALJ3Nc(;U{$DJNVUy0rOER@*7> zb|oQd=W$%OLF2Wee5p0XVGiu#t$T0bTdsGyGYC&kxz^cR!i?OJ&tq&!U5C?Sfp-cN zXl*P^IAr>?@wfrNX+Qhb#=tw*iN5?ib!CFU$J%|0#qTDdB%3in9lAu-r`J4{3BR>v zZRFK|_{+nePp0dHFM==;RCsHkpy}g|=IY zpj~B1)?S;%ap*A{<`qA7>XS6vtoBCIsvMKS0LDJjlb^u~zODDq_ez0Y_5c4e#|&z~ zf+YzBl&pHO@~AT}+&`#>UA-_Qe-e}Zw9@grx@RF>YOglu$@j@1C3rN*vV|b5vf|3# zHs@ehBA&Pwf#dG}$yxr;e&D5$q-iM)60~vY;5DDm@)u^L=^w2;;9?(#Gf{WtUT{jDxgn0c88ggv86p+Bu?D8!(U*!^G{%F_-Rs7#^kvWQq?a1CI`BCJ8@7#qtBEfN zfj{JD61ba#!Y?IpazzIM+9sHPReumTxUwkdXy5>_tEV>eYf>J)TRe0WJo25!)|3qAcYyg_lEG@eGA9-@K#KD<_NOdG0 z(r?jzy16A4nZTs{lmID?)UHUy$;^yWO_#?3ye_9+V_Ac8W|lG%7T{DF z$^Mq+DC?qK;P=Qn7gp}+zbx&@ZH6MDb%zX0S#nh#quYzjjQ7KT@!i! z-HB;vZB7mrt6e*~MJv+R3UqIW=i%j_CA=(0=he_$2_?}0c@fU!w>hQ-%~X8p^oGMGWMTsYq3_Y!OrN_Lo~; z>^!UWCi3^erunzg>a(PK9unYYRi z-0T~9XBQnJcb(Z}u=PjSsEm0H@xOV>&KJP)RVa?e1)SPwjw% zPNC#dC2UPDLe&()ZYNbkoshb2^t!{6o>5c+ttNW;G*K#%W@RJto9JUKd7epf4r$h3 ztdMGHzqjtJTUFRVhB=jmnj>X%Ob#G3)^0?~zA4Od(kPAp(=;vadvIg*g}>EAe!Y38 z=L@7f=}~U2%-#p%`?Sa}hAmIb0=Lhhl_Usq@|=rUQfcF}y#KccT|JU7z?E0sY4@Qa zgFcHyo*oC9tc@W;J#<}rj{SdiUQ%b?U@ei9&wjKL|Kv6cL6Ydx*?+nvtgQe~#@tdK zg;E@OL`YM;))wqHsNb^CdQY;=jIm@>J+cmeG(B4m+w|gey#2YYLL~w&<(rKMEiQ~K z*2=wgc4wxNG0J+-Wjc<6J3Oez^J-3_aGJ3{waZ5s4!jf6jlC*!?&$x0>i09i8%3Bg zHbOuKE#6zjR(2OcS@Q`b!B`mnoiHO?!=;sPUQxu8iUSZ;5=jFU{=9Eb2oGVKvb(R+r|3vO1 zNZo!L%^a_%?=&dFc%lv3p%sJo|67Tz;7Q{oYd7Q7yYs;U`s@p#jVJIuWg}*#P(S%9 z$S@T_a}x`7&eLp{A#G_zh;)=0W5B0};)14XcA_~n&g|$vF7LK&@{t2Dy@_nft?1G+ zk6kX^OQj}?HXM`Lvdn;|sD!A0hT!{qlkGZl*#VR*Fqxwo-o871F-H2{&JqyReo^uYbEM?Fe(D;H-dBpp#Vb`L3K^7dGKUNEU|U1e>5dwQSq*cv60m z{Xvzi2Q+mvNSa@At(9Qo3&GcGaO^Ikb7vsCG6ZTV#Y;N=$Af@!M^n!sKv(_srj!6s z`ZeOQ`}K`LV3-CgWE`JNRN7j1%1UIm37rTx&|)NFZs~GE;rK~IU1q*eO7{CJ9ew^) zeq7XVM9ZR|Oob18J591#if!Bg0c*RJI%=Y{3XS_L37l5A=T`z!Paqf3B<%T}S-mE# zzvWcgn4YoC`BCk<7g__fKUK1kJX)_pWoYUE%pKzVomO^<_d{AI{j5c70X_^NTMf= zD`%sdq*G;JcKeqVpYUiSsM9c}E07Xv$&6^0`A^{!$uh`2-naGVRxOOEIk^HhV|037 zSo~qHhSrDm$H#yun7dj( zo8(32ea5gfGR(k!#LTX2?7dpn+2=#cSM!$r?0#*&OM;?~vRDgdN3j`;rc&0DOxCtPaCA`QbX9C<`8|s=u(-*IWp4G+@kL+XR zY+&YMl{UcbUrv)YwXzeXan8_rb}y@8FTTid1JgLo(g| z<~umLR}*+FG#w;n3o-&jTutfr#V*_SNvcbCc4D1PH^wiq>A=y<@n{JJZV+zNO2-&X zAVefbndc?Z=lauAtHTm_p4&?vpB8unm1Uy}^{J`U3le=C2cdn;%vVe$LFe2UeO&?S zn7;OgsGRU`HU70#}}>zQvI{PtCw}ebHtCJ8BHC@2W7rhKwVfx zIvdC`;HWt4&Z-j1zTDNR{Z%hS{#&!1Pn8VV($6_IIT($fVdRy#Q?Q9#e|M_WbqjY2 zHuqEDJBTT;{aPuFsN>iVvhASDC0jg-+YuyPt^N0Br{qRkBZ~vcsv|oRKyzNyYPO)AWX7j-SHN9ie;a`K*KlLRB0Y;*qG<}%B4(NebBp$LXMkz z1oYtoSYfsI*)DUshB6kEQRa%r$A%`toi;!P&t%dg5zxWU`W;FbOE5j*t9Bji>M#^p z^exeVcUZKGyY`ipLfA6&)`D+e8CLVUAJ&|03%lJs5Bc$=8gl(YeVI;WO4l;L(0vmV7a ziIeMsv2^p5C2G0ST_05O_y@eDH;do^1b7tuylDjUM=rPg&$l)c=j<8}R-P9~I894plZGF@|iA~Z`?D3B};-D!hH-8J; z3N2?gmmtGkMO4G}$bWROLP>`^$8{~CYvnK1DWNs43CdpTFE z9zO3S$bxufRo4rWdvx!usl>2E)uhw%=#vIr2Si+f#4z>}tKph5ikjZuH0@BCyY zz&No7o}#HdUw$cmK2S&p!!9XSOR7^ z2r*K4Y&GPz2GR*LTGt_O#fS@oU49h5HD-dn@3N(!j`{wyb9*tW!sz1Et1y5v|0Tv! z8<=e23wg#x{G;<+{f3U>XAf6xbws!a$C2f}&4V_!$eulFd9B<{7o* zrQw|NfKV&RBAM)G@a>S5-+fKFdSIeg6aYarPjY`!)RjBG9DX=%LY~vKXj789*;%2X zB=o^BS14AM-3Vvmtt|0v=to+vtn%%l=dkpr7H7cQ>cS~>UL}$uBKG)@U*kYDi+as(# zuQL#rC#H`&qF`jX?dJ#Xy7`~&O(|}0T95d(tl7SBg747fT?h#fDkH#~Ce% zb;!dP+vFZ0-DQtGVf68t$ctrwKg@jNN*?7aW~lwaPR^MvDMBlh65|seV(*i zH^v8%-){dLF+OYVo6nbe(D+9>@S`hH+CN{y1bpTHXtLPn_KxvdRbt{kAU9!<7FGx? zg(9`1SJJ#c{QEN;8*}+_CLx6LbDD!8=Brc8(|NmrzkyTP4#6D_%}$);%DNG2X=^8Rv4Y9a+dgb-Lh3 zp_+C1kl($a*|H49dF8$$Q%MF2H~!j#<{^xkrM~++wRU&K9FPdb(!B0 z(bzYLaW!ev`RdvNv5x#^!dnLup461~q3IZ;A-$^|3ck>O!T!1gwpk$?c)dEGzH@{T zlC|zbCWaMC_V+EM=ULCg>+bh04Fprt*K%%E z;32Jh2xPsY3Z<_RX%zN8<}kXZ&kF8Iy%YUElIx(zaQh^4ki$br`GUldR}L{gvNPh- zwT&dE8YyA)hQR7lFn9GNFudX-TIz@z^L={qgf-HER z4dAXGXBw>-UVL=KTrq*>3j8F^J# zvmM8XUe!S?prx>ihPiblYco#el7GVkTAMa=Ced^NfR$rdapMt&FaGUBdkzNuAR8wB zy*^f?j?(C}}d}=-Fy!mlDU+Qhk0cDH*C`gh!&}$c) z58#h6d{s*kj>^yMnTxwomw@b}-few@BNydazPJ#+s05hX65dP!{gL`>#LGrYD7L%F zzHTEXBVRDRuL39G*(F~T9?x-|ljMr<)|gXBjYAX*tBUTgd-Q|!zN?AV55n+U2e0tl za@D^zVEP?0W;w_i2p3UpB0Dcb&bt#+X^=#dQe-s|I-%B~vIqurKGKwLFJS-2Wp_WR zd2Jh!8-jghk4&@vo>*Na(V79I6jt`SX;xt8%>fT}orYBQk;{4&qq|V{;~4+5)5;tR z*-V^^cfK*rWal~k4~OYpMh0KrawNu_-&d6j*eVEE%R1^|>r~8=__I^oa8`6l&g57lVhcOps;s&G3(h$v^A@mOv$5%r1 zzAj6;|HWR%jw4SDfPXT2ZO<9DfpZl$hfOA|D~Px$Phf+1$wMnj6+KwoDRr4@FPYp* z{=-2Jd_ONP@9+a37v}4u=IcJPu)N5;Fo(MsP?uaMiFJG=JDP_@vPTtu# z+m#6nxRLli+&{_u^8@(UzUczsoxi+ZbD=00_T@y~C=H#t?#V!-Lv4VdgS(5n$n2F8 zz4?DW+Gj{`%zJmX_S_!QeC;C-`3TdnhsF7x!4Ct=lzB1dR_jQK*CS|c!E4u~VZO^S z>~f$O%CaH4K(gp?uM0$LaF2qsE|~fl#Bjc?ML__ZTXZgoE!9rXJr8-}#c4;hON|cL zn{j&WGA5_9O+WWS=GxNlR`rF``;RU&rR1iggNjOr)o{0~iP0XKe~~axWsjlkF=U$U zF*xo8BSFhV->K=dgFbALW@>{uTOS3y03jZnlI4k8!bAqT5>{t=-FN`TrVgDEv6`8p zO{i=-#b2wlQ>}z`khkVP6^W@(78gvUAMu@I|DD_ZUHFeZ9Y1D-P|S#w(WlQ!1x#c(7j_4Re$kt z3V4xi<`#+?%cB6XwJH^6ckD$F{WcRI+HI67Et6tASl1+*b=%mo2oJ)O42Z)>aYh?f z8m-27sYTkp>V5V+3Y+^kd?Ia!g1fM{Uxc#aXML4}1!W7VURIZSJ)5iTAk!$o_3Qwz zq0}vGF7q)y+@GlZ9R(_c-E%Ku7ewg%eg~Ys-6k&(IQyYxJfrSHBK9Xlj8q$153I4j zRI>5zFb$j82qdK)mdd$w2kYG%ZfckQRc6n6X_tGU9qXj05@ai@G#Fa#%EDZPsx&yQ zVjOxcTp{I#G20+q{EUf_k9%=3Z~wOef+jQ$XJO?E=o0M5c?BMI_+gJa-sRk+8J6kl zGCquxh^q9ZHcm0HqOR*Maa!ku*NmARIBn+Lj5WSc~{sgV|?JoU~A+1PRO6C>r_Atapb?@PBm86Y(} zVKe%Izn12`Rn!0$M0EcyhoN_&e+m%Gqsy(*2k@2=+e<(T1d*TO9qGSyo zIkN^KPM+533tq5I#^fIL*%Yh~mf4D4l8DWEC0X-Hy}{;J;1g?6@Z^;5Sd$od?9F;w zhOwHhOcNvZ9xt&_GzBu>lu_xA-`Jgl`|+N9d9;nO#1!{%f$%s+_T-d~=)#5Em&0__ zg0VzRZxo06TZ~Kf1y|!FpJ!Qr-r51H|2+v>t z3ywO!(Zi?FQB{DphQ$(pXtC|`*L^d6>5sLRq{T|?eahc&4@C;|CxNAueu>B4>p%h6 zTDGzY#9WfFJdVPp*(HSh#mAXSBUg@rH~ZoQ9J(q>f`~&G{UoMWaO4L`bZ(A)6DOMt zLi{d2lUdSmhk^+FjG<)l#l?$Ch@stV&0H<%jMf&Wfnfm*QSB+$-g8%wE%CZG%Ohy^qhy@|ap+!pov=s@o z41KBys{tYnEK|}Kdnio9oPB2H^~_pFAXje-I}vy`>EMhT_cv=_PWLB z5G)Ihzi4DBfwAiH@3U{a&+)4^GOk~L_x2EQjD0ytR;^-goh1NV7?qHwq6a9HgibgjMzF@ z(T0@w+Tjc0p$g;#o}aX~*h5;HVE&_04_!)l2v7cPq0T#g`?|;e=+kq>DGO`B^*1;D zCUk+YR!7Z~bKd>lo;RoOlOVhP_phEk77iVRNC$RJ@h3x(o4UZu+2Vv`>tso@5*YY5 zre3b4zty@n*r)(NX<+;nO^#HSwXbiW_VC@~;M{5K!8?1m zmRZQK0V^_=uI8A{mo0AZ`>OlWyhl_3w)QC7yYSjfR%u>sMM-URS;CC4{lADmr>l;% zFMN4&1QV~RQneGYs6mQdjyY3oeg;RZt5h}8RkV!vA#YIc-MA9LQDM>l-PWK{FU9di zMw6I-k{1M^bS}%)Gi_-$G~=%{WT)aaFU&K3<3L%}E`HUb=o%X?PesL)e!s7z`P#V{As%uUD@8xl$MtTEAQp@!D)5?*0Zd!1%jeQVv&n?KrmdZ{DM4ahZU zOTv5R?*WocOmL-nalesc(Y4o51DC`|Eh)3PMFcy0=@f$|XIs9s)xbL*oQ39jvZsOL zzawg-+10IQG#L|>s_BW^4|Ykn?C3z#69TgnU#N+f;oRk+t|wJntB*z=%!OPTsZ3QAo z#JlmLkR1c#Q=M;89X{p4v)oo4jQ4CUG1U>TZiNE;+e(E>&)q*2mmx*4Le`6ic_?vl z5jUSnQ_1pY7-iB=VuE|-`7MMa-aY8$28ML@SOwKGlYBbQbE?B|p1|EK{qgy5PZ zHf@>fT|(`~bU{S8nzO2Uv2%iIn}SFlUxVBy^kMgfaasw?3irlmP|d`qcZ^|g{&!j? z+~MmPsb?M~mdOdXlCvXJ|4IZ}UHWOaD)V`Ujsr>S@~iS-S#AF|@xGpncagYrzxSF? zxLkFCHvzMfv)g-o!pf{0dHTT80%{Rwt}XH6@r0rr?h+BW2Pfbv5B6H^DO`YSxGA=IH_f@bQ0UE9huO7yi(}OW}3oer~a9X zv_1CIvDC^{VSO5E0?cS(?XAg1#K(w-+k~qlzH|!7&h<#lZakJ=QSpT0Oa)FzTQNRa zr|KUWhGK!$V5Fw21pyz7WT&ypOuuD}-Fedw1kyHME5_c8 zecCLE_LjN&vOz4tAs&1DK@kA}v7gn7D6n?21EsS2lw_B$1XSYvkWgTx&xxVg=3ArB z&53E7b3D*UUrxUQgKX<4v`|Y{!BCJZvGj|Tj;Q-a<1jy}gfwc|mMEmtP=OD!;`D3( zicuD4=LJ@V^eG&nt6D%sv~TeW9@m_PlW6a@5AD9>jfkxC{{H-=lTLOVA?&;F2ipxE z^n?04TPhXcZyO?k>vd3VcfZogrkFuGe^dejS$1D|ce3M*9VGhyuR8{PIyjc(466AwvpN@AuNMlygnpae`4BT{OAB zzk0E1To8&0c_Z^!;M!D5`AeA7aL0dd-SXKGtRfT`@~Cxv5d&Kq59rp1g~*Er@b0?c zHO0ZM8pGS#g2_zNbtfg%E^87NFAbj_7+77Yi!QaG@`pA1^m_{SrelJaj$@6V!B9sZ z$5+*1T}^ru+_$Pur`*1|I=`p}MqsGBjo24S`M?P`BS*5XmpnPn`?bi6FFfA&YJdrR z$S|!a+rYGeZ@_5NXy{889%#xCVNA##j{&mu$Bw(b)bE-W!PYsQMHl=f5Deqow$!%E zhyUA=d5n12k9drU2>;8L^ZC{Vv|sTaS}__#56p|45r2S6hp<~Q=iT4;JqxGA0`G_B z)o)x)5cgs|VfL3}z@v1RPB^!Z>qFBX@rlIWq)l_+wuxm!7P^cz$uRIkx}j_1VOIPg zM(5C&;ZiH;M|$MxlMO+!jnT^+(^`B5H`~YAr#pYH-%7BefIrVD1Ov!#V3UJRx+0wszvXqEKO zqVH#oNG(a-0UB-D!_VgA;RQ&@LQfk2IZ~l42_Rn-+Cl4T{uAI8hPHp^)WKL@^49s< zpk|{u06|&k#~{0ly3FWV)?R`_Is}*s;o;Bvc{4*eMdDBbH_w1=Ncq8kz<-3a6{6Z< zJd^ci+`P}|dw7twAi^BD4N2U362v@)7&=B&Jyi4=0^#quPjEu=fd@13b7L*(ta^wc z&fqiuJ4U|G^GVeO5fKxHr?)l>)lvVjoYAD-8Ww1{@MDj$kyZ2D<(~f7Uc`@o>P8yB z=Gn8zEE*W;o>_TCIyx!t{%SjH!`>T`x_9JI5vO zL-sa&3!dW}JtyUD7B>_oH9>$a}xkRzi&qO=S$Uc$6I2)GkY-5LJ?jvoHH_Fyvbg9S6WZqkgsEK@~6k1llk$;dp{IDCWx25u0IJlyI-X?6o=QM;QU1+M+ z9q@D#1Uo2VtoEd7m#!>=!vj(}41817Gns7*(JQ(eZracEQ-Z9nPrJe@GnH%m1LG>! zs`d(CNM*`8yaRr||N9t(Wj9!2tBYG#gXI?&f?><61}Y z(wk>-9+ua_<9qT`#u;wt)qDap=azHQCky|fc8$tu#^3$F{x)s_+%RI(9v65ayH}#04_M)zuQQ=P)tEk{SH$2epKIc|*24rC{Fsioxo< zIlsA`<23nH|63cZiHCsY?(`na10eKWVUdHtKQ5rEcmum5cN22({v;ajDIi#|RUN$i z_X}>u`TMFK#`6sfJvVKwr+;|tWR=8#AUXhoIAh|MH%a+XVfd8sq#UCQmm9%KZXTlw#^o0PzF_k+FDwp~5Ua{sZYS_(4B=z57xY0g_*=;AKOj_KKTzzcos#F1LhiF>V!+R*-uy$hF} z`g}3KT0}Hjhz1NUBIsBpiJGgq9`6Dx8%M5 zq1h+86~3))ea=i5rH{35;kER}`M*zsOpB)^qG~Oi1BlbNOJo0#Qim+`^D2KTzdvj9 zAAOk~TM`6w>P@Cq(2rKx9F0@jAGb~sqC(Im<`-9=TD-nEg})mYIN;F7nEm}k{;U}r z(G%R1#zs>UDV9hrbPhP-(nPMtG@h5vRR*EbXS5P+VwF9dZapt+ZuV(c{d%;fB5bqg zB4VFdbQf8XwCe>9t_-W(}*7<8XA$a2fEVEe6MW zljq?;$MI(mhi^ay+nO7vY{ZEw4&JjZO91*`uJn1VZH|SaoN69b@Su>m7N0T~!v@af zyXL5n9xa50)mg{6R#+JMYuwd3(!X9*U><%1Zru0zCI9pVe98YqCEYKi zG-b{8)iVqDx0JD#o}0X9D^SWO=x}=p?39J)aZ?GBM`S^`s9W{GKaeoM8CSqQu<~Dv zaJQi_tqbsDgw#lST~f>BbpS9s`pXuK+adJB%}KVuR}?Y;MVj+(U$hiXKN z{a`PT3UJ+6freJ(+}i6nC6#zPF5wR+w48=VFCPYsC5da&=pMa!UXoc?A3WvVDTNS3 z+sa~&DpCx7_Y!b3>h6~l7rg@hdHru*R>u?~*Q>J&<0f6oT#|u((kS#q29++z1R<3R zzTx}!3C!DO?4w!s`)g5`EZ=m-`*Lh7>SswtMiLcc-Q3eT#pmRi;~4i+R7RG5K`2(9KS9(5>|BuUA^cym z+2Hz&QQI9*zBN@)H$y_Gy%~FVV>-XvOOgWsSA)XTRMH9}VSBQvS4dBYYe!eNYA&_N zj%Ts9-<2j#>#7w<%TQdN_jko)28sXaN_q;7ns2j8m8U+`dh?3mr|q=i_r9a*X~sm# zWyn%lRQ6`Q9{O*-CW9um41n7IDl$-Y{BF zPLL;uL69Ox9oWfnJ1%|*y;oM7&YA7f-^PiP;r|-LAHRJzEX8quQc2RJv}@2)m9LXd z8=_F?=UmTfCtLg9>4Pf{`_BRVz^`ZPeOj?50pP@~T^UKsckW5~!wwxC!s%Uk$ldOD zEy5nu&6qDncPgH|r}rgai~Uk{qfgQY*twy746Lo0aMYFk8=ZrRW-^E{(3w7bY=LQwAE$Xb0DpJ=99q(r>^H;?$U0@UP&*RRwsWdjX zgPV<5dd?!93g1>L zX0PpUscOAj@0V7kVPlq?LYv8CmV1ti`NmUVp47ElV(sr5U#C@tabN#$O&+ zCpD2NOqwT-6olf>L2d}?T6?dVQ`&@NzM9F~*PN1VI>F3LQ}3Gof>PzV5KZc|_Mx!? z@dxnahM;bdtA~4c)rxUCk=I)zV_uB@+lh|dxd)DM_$T1y<|7sZ<}SxQG57g-B7OmH zcgm!ii>G2QccwHghGnan9864zEA}M%04e;l>E8Umb4D84ZPbrsjvCs&tJzbs&23Js z<*SqV)7AMi{NVI>uGcVq1>sqFPcoIB5C!2>!KCUeZQu+I1=A{IsM{9@WErwKLR zwtV``QNe24#qjZLZgQ8}NQ3(7HV!mbM=snzWg^n9 zT?__Uty3+=@{KDMB{^>$=dL%pApPTR+IA`mWzUO+MigRiA zEh42_-QZ_hBfzIRKB-lFNM> zuNKHM-Lnnb03Z%1|EP~ihdf5k1@E!xG=EYx26a>%wt%(^E3lB+nW!B$0a=E{Du1^? zu4B!EypeVEwwoeQbSsw`G(M8cZ9!ue+#evsVo~F$6BtkS?j02o<13kKpw6m&rv7yn$!eFa?i2fDgcmSvL`zhnZM>oW5`0uU7VCu0 z%_Bh;Z**{V0H~^4{OvRxvIrJ67zxR>GMYlElX=MyfTkIh{0J!RFsZMX9Z=R``8eXx z`2r<|#QBQh+UVcHlpjQO5!P!BWVvOxWlqJa6~xU-3>F>xzz+ z+z)aSvP3pvXrg=l{Uikh89kNmbE?Wy3iY6C(Yruye`-R(uJv^?Yof{IQ)k2eKQF=t7+C1djzM`yoFnEx2luPTpK7oA!y82(<)FKHXgo_q2B}f!3wF)6 zuM6s}dSvrRV>-GqC|7GJvgT;uC)T%HY&yl5o3fXwQA!2noJ%<$8LdCp z%}>ErQ5s{ceJixOL5wzHP&$0Qb?n8B~U|Wr0(E;9VErDpd%{(Pu~RKPyADhO5oA`2Hn=g z)d@I8t8I5n{N>|3>J8yc_{w>LYn^l-K@M!6V|5gm{_+gGDPd*t5iHK7G44fkP#HE( zB3$-=iKBl$5lJ(N3293?#{;(N`lXYdMV)=09^h|R32X1C@PCUEwq6*HI`Gz8M|$=Y ziA1H#;O6 z36EkS$gL9wbwgD_8#-s@VzD53l;!+Ty)pc^eB+KO7I|8J&U6irodg>Gsm?qVP|8Ru zxz}WJjr3V1ZfJz*TWKkOQIfQ8idr)tNyp*Z#)t|uuG6&}Ffgeg@n@EiwtK|$7eFqn zE83=w%m%HnU4aU?IYXiGAbVsxF_7DEB8^a#10m4OdE3>e<|>6gz$ zOK|~Ng_DF*>v^NPr}?+Kn{r!On$AiZZCs5)~Vykb1`l=JY0+jvmq`r_3B zKW93}cd&SRzds+L-l2fMOkXqOzRUjrWI>z0x?bV;#S*DBXecY!3(^-!(qN+}eKzlgB zygP*tB92BvdssF9`jlw-t=$zUZl*CdxSSQZdP;F{ZIz@!fW(Y{|2Bnm<)cA$)`HvOa;^kd zO#;zu>MOn}ylvihe3`oRSK=yg_V?MbTh5ViOn%y55ul`%f2-9M`+D`zb}KTX$$klJ zlTXtBG@cXK_>)6t#hx%-&HoT_dDIe?^Aa77Irx+SS0QQ8a%-#G@x>kbzn_E@(~W@y zoNBy8e$M%7qzL~sSX9L?*x~jot>*|e|4^| z|IUCKD%E;+f{TY37k!BPmEcb3A)>t56b#oNkT`yYeJ2=Oa}eWK)r-25Iqcp^AM%I@ z+%1-RXj3ClJ+xh~uz|mi);I!ZaY)47Xu`w3^P`70&LM%T;vb^M-Ah8Xvtdv*Y9}}= zjFbji5+^)Ws(3$l?5dO+1%~rOAPW1xBsiav3NpsFnlN0aS>kXg#vu64o%lch`#=91 z|MNr5QWZy5K!Kb8RF6|*1zi6>%Kjx;awJ){MB8sRfc~|4sTwtbJO8mfSuAijbHEUs z6`6Es5gkqoY<%=1D#8NP;~s%}sO1eA%M!u9J)Ng|%9%gm-u>&3x_qmteFOS=8>@c& z_EJMg!M;`GJk?8i2nly@!yoE)vcePGJ3v3Ld&P?~FIcC$k{y2LeyV${ zy3d|)&!B&*C$rN?sQdS`{8CRgxe5EWL;s;(Bj(|RduN&->OE-g@4SK2x;hh)U=lO?v zANX@+_jFEPow~2_2Uam-)s9z*#eiVA@kEC2%ne)C59aCYvo)%E1^3>AKh>HW06Pv! z9qaE)ZN{n)Et;tc?oKQ})MHVH4(xkw`7ia(Uex5+O!E~Zs`XMgH$rTNhOH}!Wx=*r zLTPsX_PX!tp6a3sH9<_5=KWR)7UMUt=kNLQ^H$Z2^$PYhU&AJ&@;nmmo^{XPZnet?p>`P=(#Jo!>rZJ`9My02mCFg0Nw_P5i0Nw5$1bl6@p zO97)JjL-!(x5yHTy(fhfzYFfQDnHbNeDQ>PpVS}fSvf!j+C9$p zOHEIPgnQTEAL`gI(jO3TBeFy*e|z<}6~~ua5!nk~X26dc)2f(V1$WPk=UYY63B4YH zWv{z#98D9NS)U+k`Q$5h%J#ZrfTHg*;PxctAXkO%NC7ncbLygjK~Sqz*Fzl^52eTd z_l^-!!_rW2?*#Pgw|kivbhD17MTX3_1>LMqNKBmSd`i0gVb(qcQ=P5R2OkmpbYiW^ z?nrR&v-ec@Lq)NIyT|Jo<{w^{1*SH@*?bPiO12j`(wO2NJ3$Pe{u>hB8N^S^(p=^K<__O3Cm z9y8Yv>WApw*tKWTEVZcpaBplZ1x~$#tO@FWM?^w2;%8UFIsSVm5TeaomT>PQ`M45Z zS3ViRKFwRDbhh0S?yfpN)MOUdg!S?@e5*T42f_U;`iGi*+DN$9G(GMK$2H`f3Chb% z@=?bskkE$U?tc2bk{(;Y5$-66V}mw|MuVBgX)o^zY-_+>E77Ey*Lu;a92YAIYuAg-Z$unddk63 zaIZM~pK49M+>wl=vK|Y(H znYd<%N@IH_xYw%uP>;#6WkPrFAm*!kCv>;|{v#4h6W1)n9Ptzo+ z7qwuofPbu@yu=~Ow;G!;L3@cnebfuj$qS}AW0Yu>L$L3;w?EWP<#OLUZ#au_5xS1Y?_LNm>}`GJ^fGcP$?^JC`%Tonlb~)9K15Xl`HJ=<&!~ z)#6FsD_ef3+j;U`Hl=@?F}9t(ZB`rs{q7y~Lp=%HCKP`qbE%Izuut>N6AGKG2Z8P4 zY;-N!?CKhZai@7><-oFg!o5fQ5A~$0bi%zto)eh& z3+|myf2cVUSb}=*boE0$ieelKn-|vP^S4+6}=YFcY;#-2)AG_b~ zOglB+ebxSzd*pk< zuGmGlsERFm2LtTxsKqX)j0>7M2PAXiJbFvG*XjOH_d~H6z@6r+XBnN`1ovu)AL^lO z6~VoJ=lP{kV8QAUxc9t${`NZfI}_S=W7U!ifi+`x8=$wLB z#=7602RXV4)n#UkNyFuy;IbqO?cR8;fqA%7TG#BW9*d0Iolsn?06!JQ9swIu zqJ)+{?t;5d2L1T$SWlIOhmD$UkRhRzjChl zjH{p?FT119vs{_zD%d95uuwH$h(ie6i48`3wAvEfJ6Au{vDw*lVG+F5*nFwu6=WO) z-cD@FM~#7BO(<^lNGl>NRRnjhkEeRD%G?R|jB(A1HF83Cqy3sq4Z;Q6z2A$bfMnJI z-OWGo-h zE0|kQ@8051^FKr*Q;KK@3SM8%r ze(eP4s&cq-Fcwv@DmfGETSZ`6>neeZg6^~DDJ^jCPx4DW$fFiamnlF-epwgXy*q!Y zCshT4;pSqBH+KH^JntPXf2c`BzQBI-bnv4Mx}dswo)&_*DJQu1TV-B+4Zyu0`VTb` zbzKQZ2~Hi8&0?jrX-59?U6wEPV6X({-M{L`Z)1qP5{lcqZrYm9GoiTnAg091$pNyCV>XPK6Gu?phznz%5JJBt`W{#4w(B}oN zjxj{^!ybZpug(9ZmPlYv$gpQFtW1I7&H-sHL@}6PxfmWk(UTPiuwA5blVdnWLUEaO z8{d<4K{Z)qF}}z4lD==iy<^l5bv(itAq3l042KA}YpG-TYre}Ey?4ymN5b8g=$D#$ zHogvsse%g(kk|`oX-oES!*)cl-PAA+*u#ze%t& zxh>?a_CC)9H%oBN)$`D^1nkT`8ZCelh8-bBZ2t8uIW4yHP7H%byTI*t zV!K71*-Y?ji1^UDo+T}6iW`wv46Lgsxbw>pv7EZ#tt#y6w~GWBU?$xCV;|}>w%qs<0kK6l?+I<( zX)YviC%|}GHiG1W<;KAiJLi}tfqm~^{8XPub`1vH9xz4gf@OBmAv#G9NqAeIl#jZ! zm~RCA?q2=lw_`z12j~DsPOHA|V8Ar{9aa#NN+Q89=V_ICVXUQqb+{QYR>(2PF2U(s zqYzPao8an>Qbg}>z-4_@Jp3zbdx4v!xPin61$Xz;AL=x!7j(0WX6d!HG6nALc~3P> zOphukhlwVJsIk{ha5F04;s=W*l&Ie#+`(_PvtLl!R<=WO^r>(Ax?8KaM~|T-D7sbbUT=H#v7==lI#;Z zLd?#XoZx)U5Y?*DC*a<{;isA;ae+JMPEotytp?#+y|Kp;oaThA%3h3>DX>k3S|y!C zYkT1C=>1ghL;Z^r>^<4^MQRClwJy;yFED2?A(|=-5pW^5LOq|>gM8jUx#7^h0YM>?4MEs@*@+LTh06tsRH3JSxr@$ zB&=ge5^)(kAsYMCm&TOlz6Krm$5uqJUaC0iM@`q@Gr`r6#c13qP)oS?1bwRUxFS{n z^p`KKe$>}W33#jA`up27uhEVXaJkNv^(Dpwf^G6lYp;hK1%XF@z#r<^detN0-k0{L zx}DAR$d<7wQ05LNmK0PWrb>X%v~L3eRvDm^es5lrLk zL|0cek{8UEZ1M7?rUG41oa=U#9UtQ$Fx)uERmWGBaPQ;(LtQqfv;jH_bm}@41qF)9 z$&rXHtDw8sOpKHq@g&?ki~UejrdbnQz7MNWn)N2U?E%Y|T3aZ=b`kAy#QvGl2bgYM z6;rC*QUq^v!qlQ!dt8fx|Achba%Gz2^HEs)pjlp(Y~He+;%s+hA1|psg6aP8#UvjS$PMk zth9HKLJK_|3K{LQ9QznYg7Y~G5qpsfmOINBn6;2M19anMBhk*cD)xddGK_@-P3;m) z_8UGA)b=IVl~R1dt?5ddw%=AQ|8IeF7sDsIU_>SOAFd=;^s6N}Z*fF>YpiPcT3B^E z*v1Q%sU8ebm!GMiyEQW9bXdCz+RJ!SzSM&>A3?XH4|jxB7%=U4(iSIU2{v98qRibI zoS?gN*Fn@xQFzp(GnnA67I%^D!)5N(X@0!USIqfhKw zhORrno}q^byVitm*P3If3!>oOfBvbSbWJz~%hjsXvGB<~61vOA!tr0xEx=XeR3&C; z#X*v!+x>IkKXgxM`Db?-q?fWA1)6^91G~8QWMViO8y7)!vs8$3mYT%d+_Rhq zewHi2W~K!? zQuKf9op8E&hu#_(Gz+SI^0Dl$dODnwxaihYuJsfYS6xLWR>!ekP+f#|(o_0UFW2vg zzzlYOf_A$7jw%dULjdz7PwDf1(`JtqwMq?LhH>nL&#re6yBx2MS(Ruau>5royzLTP zrxc0S&~J}sce=5lXXZ(mFJ~EF_hhZ6?~(?Qc8sNc?HCEwtu;eLjsK{g_ba1o_zUNy)U(koRK`S@=9v6XI>a{pBnA4?s2(t zv3(ho>Iqz>9Q{KowX}j}pAJpm|G=9kO!-o8hA4sSkyF_$XumTtpK!Y){xpud1l^9v zR7UFQkbvYeVGz-L1;B*BsAG1*LQ=3@T?S3xC#!Y9=Jn2btu+B#Au{Pn#6Aspi^u+> z#w%4|vuvjNL7oZSRN+<90MGpGQJjYZqNaUXK!4lmettW)fCPsG)znK7vnVuT5Dd3t zoOE>7dlKNflQNMt<$?muUrD24G{HG1uv?5LI1^lLex3X>=kvgFnYwfLvb2r`>`a2) z47&`#RZ&;k8RE(ya85&rrq0s@FRPiqPaaDej$Q=Itu-n8FFn<}8%Nm6yF@rs;5xj7 zsLFC#;JOTj2)n{G;q3~<6zHOIixqv|6B;3Swf;DP&5WiAux4&NwFb-3HDG}|&f$3ac5Uom_4QR#>FGR69 z<4Y6Rds1H#Y{)&M8=H>pa-n0=A*b2WzErgzYEIAIhj$L$3 zV7~)QmpK}HI@)9=6gwBkjEXr|f}Kg|z^Yj);LTzBqvq&ev(#pV=iU=U`%+i8J3=(a zrCu%yx{rfZeZq2awoO%4n+e@TiKOO!1I~3jMEg7#zk%YOC9P{)Udf&|9=p#j*X;y% zzN799?EV2;)hji7S75o>b>jlX8UnC63!4>3)0M1px|Qi>24>ABaI4-wB7u#ZZCTSE z2{u;1K*gsH^utA+>D#9m9^hQgLqxG-6EvHZ8cW`SQ$>Q?=_0CUO&Bir$Z{HQalv*g zrMQ3Ns5QYkY=kJ?s|D5nm2nCB(s1~(UqguUYrwO|t(5q|!kP`JFTefyirH_q3%31= zDR*S=3i!XW(h%!RuqTq6BEw4!S@lqr5-{8v0+BogHnl`90u`d2-wqWeE@t{iB(SwX zyJj#h!DUZumfh=ux9+=N&k`$vm0)+eh$s&L&Np}8uiwUxXe2mSv{W;vN$pNM5i#)z zwyRp-RUR@4w)%v=a~yaGmc8Hf5Yb;r4-vPH#ixSSlmh3KT4N7SxcLGXb&mB~$F@q~ z`o7h;57=cSIB(jDBTSAEgTRlw-(zc<6!=kda?H>K#nuVLnt=6gg3aKe-=Cu8fL|#w zPm=ctytx>C?qJMLI!O)auEzq2E$h%MwF$)k_g@*4UW;$wOmO{OW%^CcwP2WREPN+o z`PBrcbIimsLpw&`bUT4Yr8r5n3Fey*5hd3JjXpGDDOfbw^vyrH9_Lk6%X9gndQW9K8oe*)kGr{$Dr9!2%Wfi>j_W7ts+!6gY z8LTFwJ46&VT(Hb~uuOYq+gBg9`Y=M9J^XLSOX+wxZ= zz*O?T)$wo+J^29FxufCn65FE$S9e4apqh^hwi|u;&h=ISt}_iHe9jYGlqf`TBg#ss z?){D%67Q3)gqvgT31x|-K?tV1-#FvaYevvsG8xN9y>-+Dv^Tfe`lW7IkO_*J8xReR z5T|-TxqCt#=pS&ETOn%dp$!nacj`{LqrheuYl?;ka%K~kUouZ+2DTu@EU0&U-7`Mv*t+5gRC zl+YWlV0$7Mruth=;wbWI@>a1UGL`wICXAO%f%2^;J6|x}JTLWX3F~DK`2E}R!xK!i z=f($SwzDO;OvVsRbqWXQk8IVA&c1?Xaw-vxp|Og;*3Klc^orLh!LF#uk0W7J&|Ye; zKXvn2|7vQSxn`**( zO>n(W@ajEiStW4&(-2YFm*BeCN<{8$g6S9)qN#H!!8Y|fAzGUdufRq5L$sMG6S`ed z57y`_VVaJ_B#X+RLg4&nC^0SAhY0L`=TvWqo-j@Pq~X@G^?t9~TwO+ps5jDYH`ZfD zy|Z1XpqfrVA);%;DJZTxX^1G9mNwD#gGj{Pr z%LQ*|b>&+%5CQ|H>u4!8RgbB*^W~<$R8JW0+o8)5bufbB)@+3&EOvFk zajIB9_?u;;ER>Suy;a|=;FIb#%T zli@%fXeA9<;LeK?Q7b1H?yL?GJ+2boGIjrsY?#F3$tZAVM@mM{E1^uujhgg(qjQJA z`L-Y`HGexsL37fCpKc6oHNnQhun52YN=%-t8Vkvl;QBOC0o<^P7HqfvF7$(J`fbv( z?EM~P5kCp2F1K7FJYIoT-Fgt`h8&Tn>Nu==P`)T|T`E)ztr(uFqFpo*dkDI)0PG!% zdc~TT=oTGKZ5$#*87=yg^O#D)lg?zy_6(S*8DykB^?dL!DyO7AX1*fY|i}!EFv+sgXoAIP7g|R0% z2X#7YHa2qt=d>Rp%KncF+&Ea-^4FTMUF3|DQ5xn6s*BA~b-8sV@tT}=grFNXXGg3G zouhY%CT{2n#YHW!e&bk2P+oS7f8#!i!q$X#xEMz)jMvZ|46wU8)q2d9FzmC8ZTw06 z1hA*Hl$heGBbcUsr*RUgU7f(4b91&xO{&1|FZG3SPk^_6(O+shaJB>+(I&%?O{l=G zda?i3ilcF&W->|;HS%Tz_GFJ3C|ePMow@ao?qK}WO)e3d5#x_wIwwTLvHcqf&8@-+ z+v}VppgBK0E*hufgz09$Se;Nto!~0V5iML^1j~JCO%{$L;jL>z`KWu2-vJ!Gn%~A6 zm$)Y2x`J>6=@^tS+{qpt2aZ2bOg(pqi1(Zcc3<2WeVBIuml+*TkR}g3p}WXJ!;fQK z@Rm9HrH=g$5mvy}NFu^tEa89n?bvJY3!F<=yh}$c3wpwGtM#@E<^u_DHSYDJt}+-B zY?Gr+bMU6@31GREDNVc@C+@&>Ga!wCPGSW>bt6RAVcn6i-O4oP$ldrWf&LQ1D<3sS z(HFec(tiDR%w@!sx@;xfZ`XKDcw>P#2giTZB&h(!RfsT(&_r2K?P|SqJj*x;xT*p& zY%sq}IA5|mKi5qn3^7w+8m|w=vy=K_AU-wsp7b-LiKv~&D?VBsk5^!vCXX7?)=mI$ z-kl8}*2eiI;2Z@J)p0>{ol+2u!DAez~+d=vVk@g39eTdA{ulj6g$Rl`n1jj8z1CQd>8$dazL@_ zUB?A)efIuQ_x}G$y)SUx)*6kJNz(*IvEMu^Vux=38wdTO_e_T*pWMPogSx16hSB>*HyWT3=@VHrOsHfaW zuo(`VE^Xa!vWMNCd(Z=qF_u)n)84^LB(S@Tah_@_E$D84IY@aWV+wE`pe4c{M=;&K zI7Ceu#zWn2+#5@{tk;C?c1PHiQ|=E`dvYD3JaOk_@^BHqSlubGDtL==e7>E+C_fWi zwGSmEi&AUBn^WB9w<{ea&IFsMgbTDX0)h3iTL1d(eqfm|c*_F*Qjdahy5Ox#^WWX* zpl(ZVRa5I~bKR_!h=>5Tl4!)zacUe?N~62qZOK7KU{^|`b9{9qbW=A<*PyB0DB%2k zLbSRv0s$Kf3(=&U4p@hMs|NnHBVpUO3SUjKg8-X_KoXl)c))a7*JDw#wG)b|*=w}* zJ@qVQALZ1sGSxa3aGk4?TE!1c%>$EI)AgOenEx7I|2NeVDe7paVe z&!)|&z-BnaerwT)0ob(tZbc z^A0K>b;W}>!LF2+7BgMYoBN7mJ-={J7rgamDZ3Pn*6Sr;AU4=|1mHe z3CqlYl;FkgSy!^Bi-^Q({=gtU!S$#NQT!7RTu{v(5K)bdQNl90TlC^vu@V&Q!X`BHCu;sf^KJOv~o*evKKGb7deywt{tB}IKF4D($|MDhV}9`=5#jqOYr zCVGpgA9||_Mo{kQu;#7%0#hUTS0q?w)yptEi{99R@wWXdV}o9L!tmEk{=o&^jP@ZS z0I?IA{Vq#vw%3u+>>FE#yix_uFYK>KFiqxvPlJkwdJH4#qg~*?w1!n`OK`CnM1xE(f$NviX(^2XxuBW-r9>nD z%BhkIr>4DULN)s|YUv7YKtMfB??FVWgAw@e606a33YP0;B2nzUF12xl4N+XJ?svd? zQGZ=`tV}cmcK%HFUB-r_>$hoaXfnM*R5(i&kc(;6t7`NCj2Ey(*vtrq$;si7V3CyI zylFe3#6#ovBscND?>C7|u~yrHdOl+7A^(5=&qsY0mX9srdD?kX7p!{X0~0(~y{RsL z)tzU!5}t90+i#C!QWcxEd{n=6Hb_ZjXv}=sWf~N=O{kO{@>r0?MJp~@>laix^_Q)20sAG?@ zd%}F=JwDW}W2GzTkF&amdc*?dNO+#+-_=B>d1KY~;jz|yswX+Dz>l%&t{(JQ>jJv< zIyDC$7ua_&#=up^k?_oC`=Mr4AXx6Zq@4gcegKPmqn_tvumoZQ<9?g)WIqztqek+n zRv%!&ew@|a)L6|@^C75@Ecd&bEKT0Qg?Wej?XB~)kzoJIU1GlzFO#4>M*F*3mJ?M# znxa!Lt$GFHacX#1_dEmw&hqkB{T(6p>gvTIc-A-k``avw4~me1A2kS1HNLG0?Xgly zb^TIzGXDkDyn`WX>~It2V=jKEH=P6$p8ZDe>KNfMHxDRm2Ap~ndJ+lVW~uU5&9Ja{ z!g@rs@9J35D&EKcZ;T>gieCMG@MnX~Xv$Eee!R_jrt zCO<8v8(@`3tl*)Zu|1jz`>~h0sn6q}!BoO{)Gw@+w~44#LYjyi{6Jss2^>b)?w6P%B%)Vo^E zj<=vaJ-zPg7>7xK5tK)T$U_|qo_;1gD=lvC3HAz@=?I3|OBD*rvC=(R@Ctqs1=ng@BRoqicXg~Y=xHH%`p4eYi!9omFib40 zV98G8Oz`YL_f*qID`7wK!XE0alDC9M?Zkhp?R8_w-v#}7mhoTeVxksB`L67x0LqpA z^P8s`>|o|rSJ+3k;qA9+28)*s;P=HOJu)Q-$|EcFq2BU-OL*j;+|}{%R#r+tcWcdF zi!4ktw^QdnVWdOl0`S?BjIh$_V>4k8e*LZ z)2&QLCzE?8G#ANz-iev^f+- z6#Q^-e5%P~6U@^@Zwrm{=&m8Ck6fZ3YD9wh$V$DdX{f39J7AiVj}XNQNl$*3^Xap2 zSK~v~Tf#G^@}WK{El(IPD~`W!Rs7s`%(JMSQ{++RuM zTMU>9cIIBAVw)$3A2-i3?sc8S2Z8;_6u7GwF~^bctg-o_Mg*MCo?8z!oKCGQoMc@q4ooI9tMW_nY)@@_Gg9kK%#$)PS3Ejn!H*Kim0~0)Rx9;jjm*$<&-55*iEyJJSndx;`Q#;0r z8YuSZjPv>{NfvqdLp;IRgr`sCLyei&5VWT+=v}SlOck`J)93wJ zGVL)|F(cQPE2$rKb{_!d)6MO1B_YyZIXTU8PiM;QFJZVbmh2N=J;BpO^se5sXon7z zM}Lxk)r2)F?Z<#t%y${0#aJxZ&pu>#H4$yPmI31>YVo&M*ujC+*aKaQhu9WhU>5V44*kqNQGsfO`i+ z6x)k36Hc=)rVkz&>j}>~n7bO^V0xJXPyec?x?!aY^k?SiQ{B(wQa}4kJ=C3MTL~^U zlcKXHEHk@?Xq@V97l1XNQ`6x3NSJ2lRdvv$caWyAW+#BCbUK_LI`4K@62l2O@xXX? zmwu`T!F|CtXI>$Sdh(r;v;Q8yQ>3q0YWMB%H}(r$T(cg82=D-G7M_z*lhvp>|tN=HKVzfErWN|+|I4iRQv3EiAU zOQep(cp{XSe26bKZFeqM4+pUObuW_kPQiYl9Fzi7Zk;wwc;IB5Yt+)tnCM)94%?OoLwdDE>P|H*2v7M)<+d2BA3ZB)PPc<=ugy}~6lNP3QnmyGr z(Oow=WW2Fw9mmrR|KHU*-9syW>p*+~CTzDdEpfe*F61N=9WU4Y_vt8ehj&o!$ZoVRHP*(lbP!atpRa|MV#iL{ zZs!%-mrX^ypt=<`XQGp}Pf@Cz8SOc@BoiHo)r7kz@+g_9KL3Z`Zj?0$o*iE9>W&5C zN_dM8mM`@t>MVG=;Xi(R=3WZeCPPgk4wY+m9C2?IrKQZ=Ky!PkKO(_2nO?Pw_;0BK zs`jipM$IXh=c8)op_U79f{XZ7PR8QVCp5Q)DEKD+f#v3%*eobW4xESm(qb>H{SJi_ zhdJ4kFG$=L!RgMTYoVDF>ytv>xZBmmh_+h0pugO~zi(`fEof&I20R@f?&`(y>`r+0 z7{00L&a8b}J7k6O-`Cy77<6F~JgZ~x>RD8$E0`u*g*>TK(H5Y;?9;x~u^sL$;ptX* zSK|dt90G7zsU%Dsi|F77Vv4=y?9~RspW&qjn4*W>0WY=MYhZHwEi- zyNtNn9``We*_r+Rbcj!$J7GWSi|@b9ITLdO;7)@=6zjV9WCY9P560q^bIwz+o^Ear z^`<;2!JUm#>R&M_1Z4bZ)SQYh!L)lRszu4Y6}YN`N|2va9Dwb=Rc}55fy)%Y%Z=*% zgtsWsM_psJ#@qmOlbKP8htQ5zP#;|ro@&mz5>8XGfJkkk1$I3c^BK#`3*0VxWTI(; z9b<7^rn(9k&wem>bqrCYSP1&dsme#)k3m|j;OSodP?KCP(2;{t4~%1W!n4NxuEwvP zH8S9`Qi)VhgplBJLF45<)wvq5*%M3%m8x|?b92KWY{GmAY`4o8drthn1>I!1BUc!?+uuN@dRSUne5~^GKOiC>DEIG<>CwquudQI4Fw4YePutNu)K4oBz{A&Q}rF_wI48@H#J?dTUgJz*Z|x$#F9JbPN))oQ)O=+mepbXTG-6;;sQPJln} z2`y%i3%1)!VJfFJGH|+e)ufq4YYA^r(0|lLV^t&W;+_s2k=eTf#qH-OiD{fg3tm z&R@SBYqmZUnti`xGrFd(BvaM?`%1`kB_10v-lALIYE2*j<@V-EW^8X;Pk8pCeyTeL z`-1Kw+A*Us0wz>D+RuHG-~|+y3tBNfJ=+DverZdmqX|z(?;q-|d1tXO{qKtSsuiZ* zszvFMyC+Mj841l~FNY{z6unjJJ!>m?Srov zC@(A1k2*f}&G~%5b{XBV7>#8WyEiA(QEd;eqBq2$&R*_H z2C=7>P+uY<(YBQChm_{S9ohmqOI1etXcE&y1R}>MrWc4!HUTyckpx1XSnam!fi9U`{76=~a=B zz}4@BC|)aiKmpF#3HPi)nX6#BQ8Pv}3d0hdA4j7B2cM&0xe+2nr+PyO+#5?CI<6xL zFOHdi-zp+ng^nYD%M=LF$X;QBZNB1Cls|oeiGnt2U({s?nC8UhuSj5LZrpTgTL~@) zilE>;`%0*8CMlFL(q#y6)!&4U+o|ggc=k!Xt0`w;29(>m8#P()^pWZ|%<7Rd!_LvU zz*SKr8hEG)s{1a>h{11MQ0#j`0o3^iUiN9_OHJ?7gyF^$7E^Vs1h=1$k@Cn6i-4;H z4be%rtpw-n6r$ZWJ)k)EINcqj`wp+3;Mu3?p{_E@1PGiv^~@2Oui|YzSUzfuZxT+I zTTZvUgIJir)f!%+^8%CohNW1%*FE9Q$FF>;i=I5t-Is=F9SJeky1$b64P9@5XXomN zIz~TM90A*TGB#$*b@Fs5;WDTB5|OklXvR;e#n?lqR>6Aq=YFW;LuYLdJbSR-)q|AL zp0M1CqmD-V?1VR$uFta^SjWtSVop_5E=-VI(A_uR*qF1{j8*L6oIc~Cu>flc`{ld* zd)@KDv&IBwOb+Hsx~ep{V4o(dsbPjsa-^WS*)=|xSiRDB`NW6GjP@alJ3SJn$ySjF ztBeHq6_aO178%f7v>egZ=elTbJGe=dYwO zJZogYIS_ClnzK|ubtl&$T5>`ZxU&paNypIr4scm}E$jD3!g8yE&hh-Y5}I4Hje~|X z*`>L-{O|0$yjylsKHaK!#LQjPI~Fh*Q%&YHQU}-_`lwj&90|_3tWkBj)M=KN(Zx7u zk4mI6!9@H*M4rk8u^ljKOrY7-0mFEDHDzem3%02nRYJFVmIKvfHPIdwD-%IX^j3{m z>r5#2J&9SqsUtDdz55!*YD0bYfM-9Ln;P3HlJ$UTV>9zCGPx2shf;MAo$8j!D#)Du zx7j&i+6nfJjeQ^8aT11!QPWa0ZpymAy|Ig+7W;W%xp_WB)V2$T{Vo^Z4zWj?Y`l3m zeD$crz3)tTc8R0A>DG|Lv1eRTB-3z+Nom6R`a?8}Fsf1O4D{%ss2JKd;cCE3keMI&m8)e}Br?5ny^ zTq>CEyIg0Cnymr^uH0|exXrOA5tL=V;?>p7DdDST@8!2SW{8cV;LC~p<+n!+e(Ok> z?v2HT`Yb)+tJBqQwfv~){sq+j`%C-ufI4V;QxmGY?p;`7)`anL2R~O*2Z><`U!5dg z)GX<*&yQ<-V3)+ySls>oN8R0fa)|)75;f{rs*RoSRcrX7-a4xh)Z)fimfhI32>NNh z2JHLDQxe$GM;*@dO4ttCE0Ke&V0^`qU)99R6NY&@N(yV)KMfPG-+$$&`xm9CbbFAQ z|M}l<6}=mI3kBn>|GnZMPqz_)613&6gk(M3W&yXZ!o{Rbb#lWQ_GNwYQPT~U4j4c& z-#p&Jjn+?sR{whq!HGXsf(dRWDa44Vnh`8B#ulZ5j+s!*H&17zSaGa`ewgbf48n~Q zeEIVKhgv<($hkQF``e+X)JgI$P>z@D6rE=vTYVJ9Yn7lzQhU`_TdfkKRH>QTd$+Wx zz13E=RqefM@6jSgP@^^#t3;@xg4lb#_x+v^$-Ot{{?9qj^Lqq=J$9VxWs*l! zX}sERVE;Owyr6xrarnbKt`z4yt@s)Fg+F0`x|&T?dK}5S+h)oGVsp`IMnc6KDopL7 ziqoJU9pOkTH!>b0p|!IXxNSO5xzL`~Rr#ijCjt-f*z{+8)nQ|c`m}aC+-l(c4|Ihv zR0C6@z@r!Fb~r^zOH4O{yJ3}$0=L>WQ>ey46$R<~pkNu4{d7m;?cfO0wWoy+VR7!O z$ToD!%SwB;{TZ5V&3KOmAmLY^BKbqPqhfHh$p_?aH-cw%oBvWXu<;%w1+!uEo6(IU zD?V|_Xy*LSjAcw|r?V1Qr-JOY=NCU;@e(BvYR8Irtdq(Q zCLEXcDHn0MBG7bod2GUyS%ND-H)J^GW09COY0;G{B>t|`+_fw4h((;b`@>Q9;_YEE zR*awqG&UCEne_KsXMaY1_smD?Pw~!E1m3hcsKy^S3u^fgiU=0p0Zn*rclq-JNRR6_ zZ^f3}qmz+@gJQ0_&;Z^C=26)|c}CI|Yyi8a{c8jd#U!;nUT@R zY$}-0ckx9q_7Tgah@i0B7@+I{QjsOyG=K5ggc}1Ku`$(EUI(&{L>uf7$D-?Q1mKm2 z@EQ5%-?QvYSw`Z2v+H#a0BbPMsDP(afT@Y9E%K7Vk)Gy#Pp0>0N7lE_G&Ne!Fw;sI zh-~{IHi}Jr1(eKV@3aQU&?DpN{&5y>xtCVQJSLj7*8; z>iF=$(y+092^dpk1|i#j8HifL8yr|nUmLc6; z+>Bxs6U@Mg=1zsfXagxMaZ9Kl9_C9h`Y)W&LM}I?&g2J;IQGR!*wkppbEs#(hMR*l zp!gfWB;c1s_)CkAKfr`2tkzu6Bj-y_s(y_4MxO_sg;#ttpBxBI41aCHvS7=)!q;-G ziTaj6m1F1p>WE4!9DnnZL=3@Oy(qab-%fW5`loc487g zBBJH&YY@`**M}Fc$^s63nJPpdyoflhi<1;W^R58crQ(gR%DEg8`lTCNISrmW3#;iqc&hhm#h2ln~_AbKnPghMf84R3$laK98g$ZB|@dvT#9TYpbI1Iwf*k0XVWLnMX zBW3b{G1<2{O+L;x=jqX(JD!|>%+%^p42$nn`wU6-Bqx$_c!d~Nc1brp2%`fg*jGO+ z&;(b|R5;->G2We}PI_my;k?~g`gS^Flx1ecmdaZ6+l@m{Qt{Y)yo4JDekShmqaaFY ze4YbJWLQ!F%dJzysy7|bhvm>GeYywac_DTN0omg zM0FB&B;IG6pA*B~D$c;Qihqd@$ioNuInkBIm9c|5mh*sz!fLUs>FG0Ba7?swiTIhC z%HGNP{bN&2jn8Y8J^mMf@b80_=<55x!-EQ$mom~|Gdrf_*Qo)_R_J6uS0V2(^Ti2W ztz|$AHTtgMQXS{!jpg;aMW-hj&p+o)@sZCpvMYwxqCrLh6&3tXkH|`=4hY6-2oKWb z*3}p%HIc^3gUM|;^IW(uO4x&1Kx19uMl^G&uQagKc6|-82*7hok`hiO3j2GsdGsF`!C5$ z1iF$!Ge-klDy46IH^`E)Vk2s_1s2nMs_Cpw`gHI!w{5q~m_y`hbT_P5EU78j++E#k z;>&KZIcV|k$#*<%d|7qoiiH{0FF3JQ831UgpGeI zWWby{>aKjumrd8Q>;6iObMuD^8gBs*7v#P!daSCCp3w-6Uo5u$wQ*MXfHvdar$c+m29 zg@|8L^79Q}zI6#swkYjoDho#d(ef#j#cSs#HYfZY^{M->V>kK!FUqxj4xy{J!&G%E zBH$%DjU6g6d4LDVTL3xbx9@$T-8R5$P+QaO+T>UwacBm@j;b%`MiBk#n~v8%iQ~jC zCZ9yjb*t$*JBL&O{;?g){Zzcl=o;P7r8GvhgWEI1#-Vdn-&powMp5I*rdnK}NAKV9 zaqd9lJ1ftpLs~~>G z6zAix!v}$9Gwk2=axk8ISKqZ=rAH;}^_Xg{&$+m)gF$IS6@BLUHU|-J+y&^hGJRo0 zW)?zZx4GxhR+$|qT#22}F$&>wWfnl$ry|XsE*Mx%@TTkbm0GBhpIMC8Z#JrPe_yf% zH#HQ0vU&3NU9)}BI_)>X8U)^{wU$18f01W2q+`Z1sBLU~dq~%AePBbe{hY?<|uIXmVGlzmqU;Z1acZjTdR2M7rkR0Hsa`Ed7Mki|>Gwi+T zUMC$adTn!=Hi-%)sK|OA2~OlqG;9um7;Ni2la`salmybO)hR~k088n+JkZ!}D)JK}O~(r(d547bc|6?y8kkKX_1>?{ zl}dyyGC`9M8mbWfEPykXXXGU<7a_0m?#wqIg3=2AJ{i55=lEkec#Sd+Vi{DPkRQdM zga2VN@J#$V1E+EVLB~4H(~#=f(0A?%3^U)w3ULQkZ5SbC#AZsNEoyCvvHuF0aX^bm z$>#o57L@J1P3^GMCDuXPZmd;(xWEnu-?`c*KoISYQOz-g1lFcWKlz!5&ao^N;#n)` zjt{5((3k_}KO5WA%)23&$)Wkg6aGBR!N#{Cg_nDV8`tppKrab8IpWPJhe)@aoHrXUe3qEM5mQc!+)++z`&s2zryrw^eYr5W8k#96ZTSk6<^C{ZKD)sF8A|!}{**ru50%Rhm-Lay#r$Mr9L> zcBX^-A;VO(uHed`K+z4LkO3aN46ioHw9*fMS3%g~o9FTET=I`wp*lCO()$~gUOBTb z>IPC9zajc9G4bX11Nh**hXS--@s3d-Q`a;!dR#>lJ4+8L$vBH>d1%U_5Zo4SNmgS1 z;2p39fL7y5nP>o1tpQhM<>(cx^r7CG?XWM9nk-(A`rQ%Uyjoor0NdA;pNI&EyDUt& zAz`Y5lm;9gjEEr4A~y2gTyMihY4Oj>mjDGu>&DAg!~APKcJPuRBLaCGq}F=HFEb;{ zeD2rdzW2zW7(grx-^%8F@JqXBXCuSfuu{q%dXf;BjQ9$yE@YtiF&lED^M=sit3_eD_6-EEBga<15rjCU`} zbp2w{lXp@U^t8p8%8CC7kW#lFjR9aFZ)}8D#+3b-ODZsFnj5g?`!8b=CeZLH9(yC`QF*rzT4|Zssv9HFVTCF%lrSE zx|?p#(hbV5-W=onBXLCPabQ|R*2O&N4T?Qh7uk-cPS8HWN|_|QJllxDN(!n6HK1vf z!pXJ~=ttb?dQ?l+w?r#)A3{1h*BdO#UVj@P^nyyu zx`M0KJ{AppS4yj0(9t?v-T^nO73ZL38TKH54z*^E0bOAWZl8CG@V;!_bd#xj{h7|4 zYxM9c{-(U)xc?@lyLYu~L@kpKE7mQ`zW+%&UIYI}{Lim1e?=L+ioDt_4I}O8xH`Pk z5o!#_Kier_aUf$;>BV4MK+mg_>MfU(6AgK(2i$pfNx^rW3H?L{d2~2pI}Mn^?;&B% zr_iYHW(A=E1V~-Ylib>D;ujFg9=DX&YN5oI@$gFhSgX!zRymAsCny7i&4iPC#kwdL zle_x-@~>>b@5%l?wEN>awPrRnJg%Vi?5qA|dqPC}+JS9#Q2Tv4s(NfINxJ@2rZ#7fHz6X+m=Gaxb1*i@6l zSD0mP9`y%eC3yezIuLf7kahF*g^POYrin~&HeM&-*v95UgUan^)Kt$pZ+v77=-kr* z;pT|U$g)j%!k&O@R|j`K;>&j<6~${?JLkrUtt9qx=j!3S0?>&onk*sGNDC0s%}lYG zSVMZetLn9nr?~Y11NH>xH3CqFKflxQ;BdXj)Pjh<`1-HLs`W~?T;lit9Rgqqk8kuV zKnIQlUrT6eIP~Ui1Wgwejd{um;w))u;);P%N+Syk=m+qSGTpY^ z^XEg=BArdV_m z4d`&?+4kJua^G8GlE||(iOYP=N`y7fWkzT^d{rh|!FFChXA-6Jz^h;KZG3rVrDf_l z?8Uyw+i<%{cHfgh%2b3b**Y>LURi;0y_yo58y?vFqt#*GyPfK=PJ=wLc-@j6I#U3Dge2-CzC?&X6`_o{koX;tQ z>%-~B{(Y(UOdCcc`XDK87ym3MPCkg??uTwjO^LpXSG4KfNczCcAA4S4F{>nCLEeK> zxk~ph(LGZ5(%yy*9M##@G7{Qb8B}3V$6+Js_DotG8zQl!uV2DO9aJ|yAsA3c{$cSl z4MqZdF%XkMT=#d6aHP`ut;)BZez3PV@VfaoC=CG`Fr7yWmm01AGrRPK)D+$9z2>XQ z&08v0mZlXtg?vueH~vt(`Jc%+cctFhzC0gt(I|Iu7!Av}F31&f9IHcHJk{o23RneH z!i^77-}vvDpL|GrkKuXdx_5YSy}Nw+NC&hOP%jq@_14ixYZ-nWyOG#v)ttl%D$64u zqH{rOz#a|`2AA6PqLL3T7IWyq*_0hQt-*OEqYoT=JJpX8(aYvHn2?RxtY%eCT5eY< zwy66cudsJ}K08}n2QF5N0gB7)P|p?Y-AxDDW3<@96<+SK!*8?nj%FSpzKC?{Kdfoj zO>IRcXX*8HO5A;-SO1;rTDRn1xz#kbOx1cNEx$KZs$=Ak6fpo0dJ{sImR&|oK#q|LyfK(*#*MN9ZaCP3gSP}3y<=DG`5k35l6N=FA>kc z3Mz0ibZsvsAz7Vh0S2Jekdu-}LhOqo+94O@ULF2dVeg+zQJy5&Ca$d4T*@%z;Q!WY zh0e+cLkDc72=V@(4xjuBw)%~DVo`A}54t78h+#BZ&q6K>^U7q3Lj&YdWm#fZ`Vc@$ z_N6~mAbSPNcG-b$fU>ii2y1ka7qPPNENpk=lm%8C7z4R)QvHg}YV8G?A_m{uWbcN* zVZ|$8;_*uZ)ckj2LSBLZX9iwk_ehMBV7z^Eirh<=MO8t3vtKWh$*2ny8O{Ew92+mh zL-lFXx|ZX9&rTKY5??g)p6d0|BUx3~y&C`}M3dSZcH|k%`Cipkol-10@!Bv2rD4$( zIpO8;__zV5^6J;WcQo5!5$&eKM=d0?Jzo z&wEs-d+}miK3C_j{bd;_=HflvR8pb+>buNLAHhuUr`L_?<^2eZxZtTWQK$&1`d&1J$Q7Z9RMVs<&IUTy3w11m)ljplr>94aW`Vx!Ux^45ayeM zZ6VC_0^-l8&24|?;h_&h^hZIsxm;B_`k?`wceTk&#BKL@dwO>UyUl}e+19DQoiaFbRR9_7=N2c-HZu61;K z(RIuELVlu_HzmS{%%K0FvMyD3^N$O*QuSSS^6|%ZV^!Nq0^3)xZx4YW9!qFO$nx4vBxgqF1;v<|9e_BoA!=H&rrOr^%8;^vnb1+Ww1P@w?#$qp(s#M&b5@Wys(NXaQI(uRYh8$8A4~K^c#7*eiOR1?ZW< zkuPEmLN{bV+R4*$D_XT%^G3~(M~9nFO5a}2U}!}lb~?UW`@nFBAA|l3|j|eOkyo}7?#STi6qDM+8Z1uXFtqDU~NjD z=gC3~yZU)kX--+>N@v9*h^ldQ&e0u#3>ccqwTID&f( zzgiu()PDp{_A_s4rxftHeuHH6(eOh<{m%w2OPCixL{Q3>?}={bIewga6B#X-#FzCb zGFiEMJ979V`sb^p##QjQxs|6L4#P7(y`Aj8*O+n|(xwbamrzG}2y1|5Z+F55%fYgZeVX zgXWw>nRU3zujjd%9ByS-Af}_`=n#-g``VN(b9#wQ%sVDAU z0v@UB4T-&^zC=%(8!#m(O4mJ(13iz)t~zFWK~cEPX3Y)ft0hAXNvb%1d_gq#@-8?# zg!8R;{&!a%>yL!npK@+0}zkBZ$wS70eJJ~Xy5+w%rI5d0OACj=WUk}i^>k6ic4 zu5@Li$%!?z-o$SKvFk7%?9d*VFF-keOd&d!eY#t7+eJJiU`XJud|Sc$b#G_-7VKpF z_T8r2@9hLCfB8!S=NeTy>Cl~f@HAsD4Up)}fkYNJB3r8(;)D#leb@SoWT!kjidVPC zTiYINK4VT_Z}0VWi@9$%^v~&1z~oFBS1Wx4+)c)yMVv9wd?{#~&lyzf5b39+tQYpj zpp*6Yxe3uy)7K&T-F$G)5r?6I-xol!rZ-%t$3~XouvANbV`6KbU$2p-85?M$@rbW5 zgNj*mHlZ@eVrP34%0szcgc<93@k7`Ou>ij4 zo$k)f-9YEo%d*{wdGRcCLgwFYQTnHvo_x@vtsq_I8+VIq!$2MMCsW^!s6kZt?I(W? zT;i~Rv%0d&QLU!1H+Ekr`X4qz)I>xdu}OPy>OrCt*FRjJ8$*`hyh;N1tJn*(#Ghc- znrB|l+j&2rbWQ`l=)4B|QB-})|4RNEy_C|&tvcwlY%cSHGm zQ~#{7i~bL(9^jlXk*B|>+HLy%T)uMwIY;qS6KnAObuoHMmGa|4S-;tIV-;lOLp1Ws z`|TFOE>lEs`;Gp@vea9e4ypdYAciXibI^LH-Chin1k(es8Ix_4!>P+XY}fU~6;n~J z?D{;aO_Dmv|C~TfQhn$|&xLBg5pit>$}2nOz>OI?15tuxu5gn_5~GM>?uCcnb$tO> z!ObDGPw%c;RKFlKaG%OMC^_`7x;jcINmJ46q>J~>dAYPr6JbVV?zCxQ{K^X8fQYXq zBciH+$$7bu8)W<%cN33_tubwa#KML#xc%hwWnY4J{gNb0QFLKtLH4hnYlJ<`8&*k zkJI&AfXjFb*Lh(a| zlno=`bh!V`|8sCXL2*{KtcimJmwu>6HTPD;V8$y;4350RbU~QtCTXDW6g}T=AT`j&m+2va zauKg{_n|l<0YI?~`D$5T(sa>e0(EB9k0L+5s8L@ZI-Tq?hgw`?Ri@APT4I#(1=ROg^DU;LJX*a^q0Le-gOqOh9=3&NiDdysF#3%@Sob zQN>Vg`gn>Y<I;I&v;CQx6vn?w^9LT53MSiJ+gBL z>rE4eO7!tp<15+SnWEl}E0rl0WL;iR^xGAV;-^0@81H>=wx9Oqs3h;XC7v)!*4lbP zB)mI8|M>5KyRqxu=&hdQP7+|oUQiE|{(D*s*wS)DiHb_O$w?*4yv%*oT>~2^Dn8{~ zwU&GA>FN{UZqBJQn5X_FF}wAN8rO=<>Ru1ljn{q1vZGeefG~geJa2!);M`C8X$mI3 zWUD)et#XOQ3# z4>tVE1;mUyI^#W3Q_vdm_v%C5__%__3g$L*$UsUZtcvw{bHzTSwJ8|DGEj6#`3F}> zGBbdF*fRY()%%f1S7kigqcck=U;Xf25R{My_^);g{fPs z14bSFTZHVcK}go#n=Rl#zIs#4bzvyHr1q{)OtChndwsYh3*uwdcJrpXJEn;GdA7@0 zUR*Tc2mzMyvn$=MHo@5IBFRYGFU$C6we#}GuL{i(m<>M-7*Qip<>KDY;%qOfmCA+R zQL>(Eq)e{n6wj3yjztTzWptf{VANm3cD2$s-KbwQhqhQG1|uTpcb)nJLsk!o|483= z&I!YM-$9xl=8;*!d`!K|JmeH+qQJ%Az|qB8^kv!-`|jB_}jn=8o)$6o=mB2 z$GQUDayT-!Xq0d7M(z2klD$|D*_uVG-#1a|HJzwA)NU%mf_B$QOM{S?&mqfN{Rrgf zBWPiUlSx-dvbTg(4f%TXHHzf9%3O?9eGt|e+07SA&y1Ow3Yq=GSV1(?BDYtv zi^&SH4p0)rRN{eOG`bVBE?H{?h`uls2Th<_jEK@f(EyaF*?Gq3@Fw()vIMSA_-qxf z&wWUd5p#b6Wq|o2rEVM*sqwD@gIR|Ky4JiVX5!r@CYp{v1hS>P^pI{G3gB*yi1Z z{2li#&E4kREjm$pBWtKIVg>CnXNhTzqxA;gj&0u*V)?$OdT|^F8xXLZpasX67I&LC zueW=S)>;PrBq~|)*YeAbbWvpY{d!#IY95iGNAW-^|Y;!`yk>%bsi$mPc3KN=? zcelf|R<`c{Xq-Gaqg)H|ej{@2f~y{Ls`8wiB@3Z3<4~gO_Jo^@#Z)haUc@p)KDeD4 zgSq#mwAz17MF2oe8561?%B*g-ht73T>ybkBYiBVf^xo8A;-k~iy8>QEpl#tJGL(jx zI!;p-l(y~>HWD;u0h+-F1e0+NdFu(Xo*g`(z)VV;L2l)!XTnQH--aGU!_0+0wR7{& zW(tJ{uZ1IK{zh}_N#Z^6P3Fz1A2ZZwWYrC`VzxV82+xxfT@8L!6|AB7?JVFlbd^K+ zZg==>TQ<|vx&ln?v=`9sM(;(=m$)oJA8>Zs(U_Wp z0m;V-dBrX6SmE2Y0z8o!IqM94q5OpcPmNa-XcqvQ+*jUo;wh0TwNdway&r2SJeFhN zI$+_+n*QQ@fGsTR^?nHfG8on$Y$l14e)$xvcQ)|HLi5-_7zL%BM!%95vU)FSkQ*C6 zu5m6IdA1Fgouj*fH5q%X<=dKmIOq0XNP~QGts1l+6xH|EC{N! zHK)%nUO@wUAIs;}ueDCzRnps_ETgxTkCV{_&rf>BF=fx;%$_woGL5~NW~*MYX$Cu6{f5pPizVRS8Ioa`gw9*G{|cRO=$~o020V^o9{K}&>-_Od zg~Qb?>X+-VmD2sl>^7*V4;r*9Hm7N57AfC@IMG2M`&@isA%BzuA~n7Tg*g-zH0TI9 z#o_nAP&k-liIBhtq!P&FY<`9wVeKM{{$H`$5ZcqazTvN2Id=wQ*9L>vVqgb17@_z! zq?QEX!%uv#&&79tim2cpM9RMT%%7QS-O3qWO@T6y?eup zr}vJ-E*k%w%cyrT`pe?}2z=Kt7}dX#ixs%OMn1>nw3t_{quu z(jt_hd)ftwWDw^rV?PNqS#lX9Z#fFX0=fh8v)@3Z6T&vR2d8oc&6s3~G5Wv~T;S_9 znidz1bMO()2hyHPCI>7<_bI<0f~JEWT$!y2wfcR+MmRkLG8vEXOBwiNx^uS<`&kH~ z<}04tJzf$HxsY40UzZ2s^_&&efguNA)TK)N%X$kfeK|wL6PYBnf_C2>VoiG_{;~}t zi&r({(<(gg2176#ypp)I6P7d3t5fK~^cB@zCVe!n~q-?_l3XZ&@FE5Le z)$2uEWVtxpNaUuV)KuT!`bu{PEqDZ1xV1Zg1b>A3Ua|rH!*;r=+bH0~*C#H2(?$}n zpAfM;X5vw$_&01<`Sq_3HDf>l-QOT*0In&3%dV6r&53~F0WcdXbz_%aKqoRwdw)*N z)&B1BC5bb4{hP`s;8F(hg)l7FqcgO*WRaUffbrn0ke5?&`brw!$x(}sSuN7^@s^fs z8N7?oDVe-*`BW2@*Q?gz@|lTeirT9E+-j##^2T$^Aed3`R#W}Twgi5?O?II>He>@c z5)A5%GpPk`qHIR)z1m-5c$5!YFenE~SB{+1#b|Mywrc?PsPtAxJ|hQz@x{fKz*uy9 zcdoa^`^|t}*X`uazbC;x-e=jTwYy<92&U`6sJ~9)KId#rn4bn z+1)4~Q8T+AP9_urE8DzyMc(E#0z#D{wybV z&1-VD`Z`5`y7&qS|sjVfspfLoub`W6a%kYvSjx~RQ)k+hguumyU#d^oGq zJHmv-n=l&XLKi=G>$19zFcVDmqod|qgwb0SR;@|9DOZ*LeTFfoIdDUhA0(I&$-9~0 zAV&6?wRx^A+M@aRaUjBX=)^L~3Z9w0Oph_4el?zMHYs%S_l+ix-L9^iU=}bD_|6~> zGqxLzxOF!;Ybg_5HA+wd{%2784!`CD@WU(q$IN>6-tJ4gH_qD_W9G>*Uec-3h4=z^ z-8qYwsJ#U^K5MQMJun}P%*Nu0*j#ESvmX?3PyF23J2OBU3y&=Xb6$0vvKMCSyF^9l z)FM3zT9kQOi?$-*x7Velumr2x9-?^(iN2b-W-x*W1|MPR>MQC zaYmk9(A!<7dWuF5VV>^Xl&ZxDx`o`)|6y3sFr3tUm~zywYo2>AU0df(zPL$Ptn)`6 zt}iB#NU4#}QEUM{K|@ow27^q0nU-#zTAZfm+qacfZxW2`DBD_;dz#X`dSg4I%QST_ zT~c@9xscaW?&*=7&qM!o1JNPx8z0=o+%tZC*8&$D7>j6;9a2G+7isahoB<4p8E4~H zmNty*#TM7OG##&5KOj5bLizul=1t~*W|rz3g`JT_Mwxwyu)Z5^C{?~Hy8nL|p4qIA zqEC}!;c$@oD@{^zk1IK#L49A@%M11BNGCYqWWLf~!_k-!SZPMY^Kr;3;Mu&qt--RK zd?CoRLR6REC1iQNWIeJJ9;MTW`fYYX-tk&IPSu!?OjX-nOR)X=e`8fW9ddPJ#(L8$D)f3Xf(&o%lfG_rwdu|z=+g} zimcc@;>*>6WQv*!KG3`~`TUl-zL2Y5B9MaV%lVLo@1N+b9L80SFz`>VKcBsk@Dk8k z6j$AS=zhY@;t;N7nHq7Y5Z|J#C^F@KI4shnI$xTQbc*pLRV&}K(cp0w7k&A43Bx0wc=X2S z*iTw~fXCdY+mz`tJ?l#WNu?AlnFvPP!-F+`qSwGcpe5PQsE(kjd3xl%j z)}OtA@` zLL>#txlCW&P|a}b$5W&HUBQa4XGr4Ge@DY;4wIQPP3y^%2er3ywe;%>I&;Kk5~XK_ zWtcJ<;n1hBS$+1^vCg$^_@|E3kS%wm`f7Si8Hp1OU0SAldqMm8+1HmZ40WCodBV6i z@WyIGWPNgXRFT0J*^s6y?!213aUd(jC3%^C#}lVLE4uB_431SZ0{pAVP1?Z4%0BCB z@&DIn#~Uz#+DkL|7v}8nllOv(15ON&4bw8N#I(4nds#j&UY+W)%9{G5{Ft*&DUC!K zb?|!|G(8p~+Ev#9)|f)kGFU2VBm-CiH!FGJ+ip+ow)t&OHNdNn++e5IoB(^g^igAg zmRHWO*Q28U_NCNV)LR}tm2;VSd8OYYd~vE|WP_drn-MrhmAXShn_6?9MLnH>owtxL zxQJ^n;%Bozu@#&KEigK`IU}1~tJ&sVq+aDipKcL107>R!kLdl=AZ3Adr4k(0zoE1) z)5vt{`W=Q5u%hLUrG91V@=x-|p02#&L84LRgzBz&xU5A))*NEGtK8b}Y3Ad;$jd+W z^>}k$e!#+k6+M8vZzp?AzO?7$Z#j*=DwQL`HGHStFu1x#P#V7|BrVi%guC3~)P>}* z17@eY__Gz$cM&a3@Y9-Qr55Usp@M9^!lMI#!MK7r`lt*mNq6l0fYQ<87_`Re(fpw* zRlV8uO88_6tgn?GiZvi;_-7H@5R+B^W31Nbn|IfJEBZZ<5?{Vnw>uQad%f9(X8uSF z3!`iaw|S#v;wG-4ap zU-C`D;vEI)Ke;y&wi3(ZPQTE$?^cEwTB$dP}`9DuQkAg9zjQzx?_5}5&S zhrT5GbZu17nu~STm)O2HtF}QV@xmT|-4!4%m_~uM7yeumBWs%e!J#-HA4GrvK2PZ7 z#tVb4nQZY0r1R^=Ty8XY`LI_Gq4|lXu|YyJ3)QAqEylnKjNiwAVBLIWF}LJS`|&4H zVuLO~k`w?k0>9UCEraYnrJTX{5u73OM@^bSd`GNr770V+pA|k2@y6e1=crrluGCtQ zv8L}AVl2J4E-aEj(VrEc&=&%Ab3uNy{VTv@XZ}LPsn--VE>zu6S=7KLj2<^f%p_kY z>n)av{_`tBm(DNWq_WHT1O5|i4pb?*TB}D+6Un9|b|wX0`rXBJP{(b@`?mD7rk}70 zq{*F;qsE)mq{1nzaSc^!pevE=qg3aVB>SFR+jHEH`#p0r`^nT6As)Xj4Qkwj%Ihil zqkJ^Ln)wD^8jUa9bMX4xCdCzw_Xcx_Zq#ZSb>yFb)@4r zX(60`z5GWQPfZGb$k{s$O~NX=mG|;@J`p^vIaBD=nBOf6Cwu7z`_aWN-1Qm};!3)* z0wL2gejkY#cJp3oGPYGXeY4y*m@(}_Hi=l`PLK8b`mNG)W^jd zqr`;(9x&v&cZHCaE0k7AgbUaQ*W5-k@|pA>T$ zR+5xL9}sJ^d;;>s-aR2udPzqrUSu6IUXO0AqsGB$79^nfRGjIFi zoqRTl(2-*Yqi;4ClKQ?Un3(lG0+-DY7I81TQI2N;6-$+92Qw9Xmy4`m1k+`7a^<6c zr)oAMnoAawfaNL^0u`$0=&%%#FK7iQyaN$e_H})nBtv8*!M6@ zSEQCunXjxXaZ7^ZgGGGl>3e17C*EXITkVX`m@~DL2gYP^Vx;oZUqU^t+Ni|8H}J_> z6ttR6Du;w@DersdooXLBjgypCWPzsgQt)xqO78a1%uY|d6+Nz)F7MU!GjZ&pUyMuB zt^23p{CHi}tVLsf`tF$*nv)vG4>e!ZmL%}~Ha^Gq(Wxw#0Y#Cv7s_E2a6VTj>k$e4 zA7`_!ZtfNXIZn@YzB1T$B#Js4j>`f!_z*ZT;%XbZ1pE@$+*OLQ39>l`t)RRxc@17G z0_}i3IPYKB?jQXvZ&Lyh|Evaw*Tsn>8bSI|Y0|MEGjEC_ukox`8io{7B{~co|D5+? zqWYcbtFw5gDcqtg0C~|OIsKL}8SZ#VRlNI#87Zj;fM0GUQ+{m)|4CP)YI_gn8puu0 z4WB7=y#kK2SzJn3O9R9ODcvBG7H+!KvpoRPs}&t8kQ4WAJ|(^ z3{Gsf{-UIGD7BXzH{VSEf}$SP_Jg2V6u2=@ekD~Ls{g?lLD_53-dvh*!!&O1fu=`89H20LmtJ+U?J^es_`1S; z(8w~xDcTNZ#0CFIv^|8Mem4I&uWmOC0FxSE;m%sq8eUE@HdI`$+!E%GUqK7omZ+6P z%A9-LZ>;`Ni@p26>Pl%aev?>vd$hA1x2YY)6Jt&@&$mU`ev7{;MPKB5mXNlR!%eBZ z@_FB&L=Q3Jhms{nET!_`+^^?;Z4RAHG6Xv&-kp-LA5$RaA(hdqsuAk}yuTh6 z%ft87kRh{iTU=t*WKg! zbyB#$p_Sa#3-nB~;wq5nMd>HZ-uIsRKaS2itjWKN!_Bl2qgzBux{+qUkd}U*-}^UgT-WySJl}K9=iGNzBEXquS0fJEXYnd z(E6g}T#!_^0vZjVnRqRd0r}^?If0VXC5I%Sfp|87S)?u+{w$Ny`8!yvDQ-*`s^~}U zdiwF%yD4Zb%k=&XK80=1RLN=eU?ZX_@~Crzz3-Pm{-TW^xoT3l%?=P$v@K`wz0B|{ zwU{#o*#;v<7Y%ha{o^r@XhnB$Zj8O0hNH4Nt~JQ%gTS^}htrt6$C20b{HtAl#*iIz zN68%Z*dZ4fxhaLDp*$coAIA!&VJ_4t9RI!oOt6y7b5s6G`UI8E4Tin?d(PNa(e!Bt zu~|MIH8LV=WM}imReDVB_$m{GbBj$*GhW@3xK?a$&N?pyjmBHh4YxX8&uDuLv!SSp z$`DLoo!{P;y1q*`_%a%4gAJgqEe56p2eBn@O{Ay$1MJ`UEwlw2Dbi2RfK%?ho12Zn zeF+hn<9R^MM3==+pHsP2KE6B%F;vzpF<|-LRW_g$HhCHRAz@Csp%@SEsW>1j*RT=0wuSga7heygOliF;S;16=w}trHEG zd+(B8SfN{8(eRhZ6K0?<^IzsPn(A&ca=DIJm>)BUv%f4geD zS=YLs@XdQpFn(H85R51rc*mmaRDYu`r%kQQ1-WCzKTd)I!^P1VD>4(yPpv5kvyuSs#HfPrEcS{vXkJ*u zgvT=1@qUGUOm9Plq>|kb_CSpp$xH$@?$# zNZ*se^uLm$T@^a<2P@!lHFiQThmub71p|Z+pYhkSNomtoUotvg*}2j3sj6P{e)XL% zrfov*(c}x);WqNuQT`lZ{5=Vm4%w|6xRlI z`d1ujvvcu>6d0P+tJM=c{abpP|KJyX`6t=WA1FABa@Kb@7QZPl zijL-FCVVHAVUfIjCOs{CeHrfL{m78fn5R6yW5@x`-_wUd z`hP3^153$V6!$Kr7UU}*K_yDAkGDC89y`ItQSvCIzpitot$O47Uip7DT%{#ErDk@2 z2NWp&tSYthENUW@_hzkDl(Lc-1eFYvrsk2(lyYpJ}J6YbWEnHV5H5yFcd3?Ruf=V~7GB|rsyPq`eWq4iJ z61Y;wHQyPHQ8DT#;0F5|$xLT|?T;*xPz4sKybO7@0Bd#)mXI5Ld3WwpQ#f1V6{N?y zXKLhLxI}#t`nZ7wr|U2@_uQG-LExB?XAwBvJ*!cf) zam#}$W;a|_-W>)~6&U8?8d~bMGHhZ1S+P=0MP^&)f!E|Xsz8$bs+sTL903gJs^8vg zOuYp3_6i9%&j46)Z2&)V4599-eLFb#2D9e9sbt%8>FEQ>9Z8nU> zGk0rykqINk|{hw_fByzeL)A%X0ICYk6&V=n&rok06l`88)9Edq2 zoPuo-6+C4B`IMr^n;%&p9n8}sy6hLBZ#kA@3Uf5)T`X-S&*Gt@&NVOE%mvi!-}MU< z$<0vvj^#qY2+8+D-ui4Qc%E~?!fcc~%~^`FYfj`AJH#PyvlJv7e#Nj=r4Y)4YtfIzlNrvfwK~E$w6;z z;zCEx-@L~G(gc3JkFM3x&BME)vBVjbS53QiLW3Xz7r^hDlHX|LJd_e6X1(OKdHun`m(F0P92F;(F z2-)vlFW2{79yj4+^4PP=E9#FLu&Qr+kA{ZONGyozw!HY3NT-GG!Q6h*llo>?Org>uT(wc0bGwL zArhrl$FVDsMS*!4JY}X$>=ZTvdi%WvhQ{!sqbG z`;A+Q`Kdg)^|Ea65}+Yr)mGN!pvfYiyJsjbxApmR*BA=t(sD+3G3iT1%mFaO z?u8m(-k+javnBQYD0g*3eam0R^%4?Jlr1Eua=B9EIY_A+P_upU<^xocy!QC{S(IFJ zb+hh{)IQtx56+%eBH*WFQG>3Q5+vNldzCfcs^}lJb{f z=+7$P*8GH+^%X|uYnZ8A@~2RbVHWKHJa)6zQqYc{I2(EjHOw}w#6u^{fXq|;SFJyR zF5suQDiQcwNEg0yXF)va$^2*G0)V|w#_n;OUfBuCYe=TN4f9ZY1+C(HwTX4R^!`;g zP#wbd2=f%4njRQ!db`XV#cQF%Odc}*--RePE4{j#Yu8d) zN)D=9w`%027kU~EWY68?8xy}GkJ^{n6AieqNtX3iOatVgSn~itMv_+b1#cVTv}!N| zT5D2Y-8CIqKLm2_Bp`O{i3D0+*oyMyaF0Qr%68AM<>Y-R!51maLz4l$`_|4v_GZ{v zDZnZjpw~GT;adjx=K1GLfq`KhPCES1Gt5SQBg)?~yBuvS;0@!6L9Q2EM3+(B^4 z$Y%5>U;8*Kx!_Ccpwcqu8n7_E7&Ps-aqX{CF97O9dAHprRx+e0h?cJN4Xmn8+B-{ z9WK>Hvz4?rY_WMj&DAZp&eKq-=zDPn~9$v zp`j|BU03-4mJ-UGW$>3~x>vXB0R-AIig#G1Jb;VxC&7Or~Ag|J=#589?L42ZTaWz2lJ^8`4QU(Uv%1 z!{(pwS$~0q*`&4GhfZ*m_yO9z4|N4bUfY!=ox$5nw9#)>&ytEG_jf+(6414vgDXqW zjtGl`Go^8Pq!r3(K1qaIKPtUx9s+r%Y#5$Q(|(U=qPAeeDQk}i9nH&S8#)70>i1Un z8ZZPa+Q`ly{y~fAC+B_JKxFn`{EerWSP~QKSZkQmu}_bjC95dB0?5EUID&bUpfNFO zYiOAQl2pHcSZ4iX@ERu>C+MhKCQFScQx5++XSC+P+%?1huQBqd0G`AmRl?EjEwQ3F zO)c_C&+gpbJ5JEC2OH*!(Ck}yntp@l82Fj5+1-FO3gPUGtFE(*L&;-L+~B?GLE4yl z(iU7c!>Vt(D~RBu@jpFxalI8RU@b4jIvf_l;r%I-u5S-?2b$#0yGZ}*>jVdsD%0>i zh2t=bX`=*Rd0jk7TI>lt6(H}t9ER-Fgr7C874_cThUnyW7$f&LBIcDTMw#P{eT``? zDs$ADkKB+=XZ!10yE7+6yDrBlZ(|ZW=u=3!`2zFB@}L`l~%~Qe|ualYa_h)mPJP zK;E;4n(uCeB~?#*mb$G42JUKYKL7h4gpHzk99(@&UEqg|fAZgHy*!m;8*TTsiRj^X z*=o}^qrt3c)_wTCy}i97s|&YJily2SY@nSveqssfc2@TEz9?C)3V zVyZ7X?LL4TWS$?u^Zo>B&6LJwaT5d}LBbi)ZERpFj05}DX%2f9(cFJTRjfle-8;9D zwi*Q(6BYxd%69rX=%YtI(QBU-823=^3{Yh@VPzJD9S7aIOqrW6rXJ+v*ScF|xXU?~ z`@e$F?6*N~}yk z8*E*Z2{Dalt78{4wA%Qcu^Wz5lNh$$KTJTI1LMle8QPJg7z~-mSKKK3Z6VSPXx-0` zejQmHoO5vg3Vr2qyHRxhJ;k)5V%D5j1mf8K-^QK)Iaxu(b$mY z?%=b!MM|d&thNdZ`ZoY(%zfN_^KbcD!gX6qb~n9xc;=LF>vj_>DZF~I5mO8@xb(}Y zbGnV5f{Vgwp?);;zYoee`A0T*7UVy2;>$AM5N9;_)!|^)@ALgp-PU)Ir-(i3D>*$z zn@is7F$J$>MIZOkFx44qgU2W&1bVCRoNT;YB8VBZGAePnv!rmb$0N@?p6cK0Idd5; zd~1GlyZB;{{>%Dk+4ZebF=H~uE!mv!*G-k$nGA+S&%J`I%;0D>+KSZ`l1hBfXzg)e zOt~$7tl#D>kKQRRJo}!YKKwEz`Nj(;z`7T$nea`tC+YLrWbWw26P&wyRAZu|E)RLL zVwZh-WhAmEYWq0^`vuC7qw^}FS_I?3n~V_%0H*|{P^!HX!qn{wSG_M8 z>}-OcAfJFru+ELm=k$Y4l(h{-TP6PNO>vjFzMF<0CG`E(}jXdvnk<4u}%jYa@}s4Ok{=Q1=wtUa z0p!I1I)_=uVY~Fz_bF^&W|buVqJQ?rpJFr~1GHWdX;VD~?+D63BP_PDc3(u}asrNw zY~IPe)WsTx8_5K}umi!^Qi9R5pwq~#jGsR;*42Le5TU-#!Q#3ltB4u~RVPZAIg%5C zm-=yJ8_6fR!_F9)ZmKjK@>&yYp95dXxm+-tAk%zrK-*SYe~X-TpRjO+tay?FN%DP0Mt14^TYwSs zv_s2LOn}=o%?|1}-3NuzP>!%`>3JTH0uS8WDg{)3%EC1rziKX(e`l`ObtY?64 zUP2BUJ6i!}jbMWYz0=NGUp@AHp829HFD-DCXu$WE1g@Y+u%^s+Jv`Ud>*O1TueOjw zDY0085|;U{m)e)4(&z_#lS}^PPA5cMb8JG;_$t3%JNJ*o*}h2DG|fIDb@f)z_GsD0 z+0qBOIh!{Edi3n(=jev_YSDYTQov6hZx58l$Rswfb`RnduBF3wYM^!jU{qj9y@&L; zKz^DB2{f>qBCXUTdQsf`GpyEyZ2yNcGI){r+LCD^<#~#~c87wYnk%i>8&W1W7cdH{ z!PqQTCai}WZQmB$hVWpA8k}(CwY8!rt(M(ZiA|J?DT_fKC@!{W-c6{+A#cU5_l$t zj&07O`8Susx14d*uko%>DkYrfW#FK2XKD(cDnOw5CX&W?r2BSm{m$uz_8y+3U6gD( z>HFI)!rR4BhT=&O1Teptbg6a?bBhu_E;0KbVdtd`CY$-^YcLqy)jtlX^y&Yj3YT1jW69=R&5~@B-(}(LD9F%>oq~1ToxHDrh}2 zN!LIsaTAxXg5JaDh4Js}1c6|WUxIeg>X?98TT-G2*P=ZusZY84(+h;^-eW{Y#C4?o z(ER+)j-<@+kjItf#&rlQsl>QNZ5IzC=NS9VysGf zjif$fx{E}4@73MSkw8S(+Ecq`O3aHfD9Q-bIgfx2qZC;BTl?1q!{vv#(d~<+ppF^A zpfa1~gp7AbN_M*f&Dn*QFZQIliZwc*a^3J2cQ>%^Xm1S|At>_Ja1 zH*E*Jdh;x}Y-m>OIo1BasD|kJ!4_uPV+7)W6ML0o`zQPvF<;giLG^rgFPo{T43cPo zKt{gTl~WTbc2cI|x*N)nk=gMcEog1Ugo@h9yrbKbO?5lV-k!~MCPOQTcL=nUtvrAW zYu9rX=k=l0=a^L{y5?)Wi{7}W0}pK3GA(}d0cW}RZNE1L(v=1u)>45|)DUy(E&L&x zqv|Z?-kYcjzuZ7}9qgAN@MrYx`Pr7m{HHI>J#q#c8X}6>V4mRcC|V-<;k| zQd6%lYY;tg6biHRATQ%}drg^6XBdYYDqRbv@f+2}1+80)IW|{4FF--ic^? z#aQU{dULiDT@=B~M|ft+hQGG2kIrozD4Ab{oJWe3?1W68jN+P40jqgLqXHoTZDX{_ z(VJzhVH1TCL7akdEF^@2w}TpGbDC0yMQ;c8)56`+`#RN!?nfjP@QQ;{+A4vk$;ET$ zeq|H=?xoIu2mAfguNrA=olB?-bgAgUeN`3~6?v}3-CAzX%V|1rv+#|Qb3e@zg_bR7 z_R0M8##%Z@vXrwQ`1NG2{Yv%E(tVD*YDm>tZJDzO@R%$KCw~H`xA&vZH6+}^Jet1Q z+Bu^cGIzf7f9JI#5xi)u=r}e{i@qXCWa+Kd)!nN2?qbbOnK8}z3RV}AF4SYq@Yl<0 z)DFGxEj+rmF$cM-+QB*CS@)?LJYsUSB`DsLqmZKeJPD0y&l@}LxG3ogm*94gNoCW1X z-q{TckM4C1F>&CI>hG-{?)pYpcuAtGiy!PP# zZux$=Ya~qChLz9#f)`M|`zw{$zn=bBv(( ze}#iXssm`=rX$d^gwxkp=lf8&d(K8$O&tI{E5I2KfY4#K@Ngx2XJ`B2)QTl18y{FO z6Ks0LM~}6DU2+OTJQ~4&NxBtX#ojSwTx5Z(Wp$!|=3(oa+LR$)6jXypt>|V}ERpzJ z)ZD41#(#Itud7!7h2v$=3i;gp1ZOdk6{Huknj#Z8vTti+$mb1%$!lU{u}mXx*4Ca< zWs3&6w|D?%Iq9yTL8di|`FVm*74tQChl@FpzQ4g(GgWxHR(n$Dt2tKJ+MIC1DjXxM z@@>m3Q`qvtv$8i>uC!Fly#q2m*-4E`-|Shxr8XeCOzIHsSAb7ePtZek(go)|T>8u2 zGEn{+BoC480)VK4`PoJ3kuU3#O8;8bQt@8p(+*6@7-WCf6*coTYC zW}fWS$rWpwdDKyzPN}I?hi!OMJrS|LJPSQ|Lsvklrr**1c4R5W>#!vcp{2XTH9L;A zTluV@nLnIN6o#$>D?SxPGhmM@Nqp&hGnvp=wczU7r-+|6kRA9p-;Iy8)rE7}Yht(b!^z;OZs*0@V-Arigp1p@Q_2 z?Y9k30T`Y-x2(hgB;6Mk6>&D1hj2CH@{{$~q!!zN#cozUF_BirzQQFyLQe1?d zR6J_lUo2J8WO)Ou>@Q-k=rhpKb-Je5q^xy%Yd!iZY4TaQB%Y2rwlUSR3G>4TciSL` zW3dsoOz zQMb8Vafd!9l>ggQ+sha!c%WU(5Mwl^Ai6>wi`!=K`ksDTOWh`K=?ZI(SbI8DikB|U za|Zv=-*sXgkq~+F75~QX)RST9h7+e)9t9tgjAng$;nH|z`LwD_(SY|suYN}D!reLR zUmV@HQnZq@=_ixb;y$1(57(kdeIemcS_@*Q6^NnlvOPB2)vz5A*d%J?>9dyR=8W~g z5wRF?oZW$L?+Pk{4)@5?`p72aTO9G%XFj!7&W!6>m&B^VhT%Jxp%L-h2w-j2G1}EZ zb^h{x=hqS8yi)MZNb}v%KIhm+nt_0D`UM3&)3rQpMijFU^u#Cui#n4knPB2e?eZ|r zpVwR(AmN%lz#;j53k{!BlErUkYU|@9;>4-LYp@x^-<#p z@$QSDBn7X@Zi9riRQ*Xjbh0ot+9mwch0y&m)9GKghZHT(sG+wl)6K)`_PK=B0YFSi#1RkMT89Qq?F1|aR&m<@CT7wtpqdCE(UGrC=kYP$`W9zw^wN=QiW0=wIVJQEnZwg#XqCa4$FRSy-K8IG7Tax)^9q*%Jd*FQ2)V*xhftTRHkg5fH4~c z{-U-Vwp{=h&^|YS;lQZjy_qzd4RlzBU6U~eREh5H{kj8Gzg~Ic-6;D77i9j#VNodm?+pFM2TlZocg4EGpi~p@JNr?ZQ4cu#iDpxmLnR zav_-ln>2)_ZidLi3xJidrm({NtOrE)A zwWEA34r$}!^%ES?XA7y-397+;Gcn5Vc`q707E}Ef5$e!01lyEX8LoOetrv^#P}8t8 zO2Ny=NLsVQYb7GfsH9lj$8h4v1*Sm>iSfY9_~uiUGXaPtmOH$+?u{+cTh?}n0DG2m zh@x|wp?s+Q99!LlVFqs*RY)saE+<3Ur?V&jz50~Zy0>xj`H(quEOcA4i6A4svAAk_suraT7+ z8C=6qez1)ucbIWO@&w=uV^qNdxkxjb*e^e^?)7H-3bEMNhah6BG?AKTaD(=U%Nl4S z$qt}>L9F#3Xm&W(U?f;4Ghqm(jMxHAz|COPm9Gl^Op`*ZEpU74Yer6 zzKWHo-#Y1lP8w(afaWjt%29hNz7lpflG!VS8qN^MMi%eN-9@mA#Cca&#|v&N8A4JK z`)W#oZB2GpHhTUW)wiEQk%oIOL@g+Z=Yf!4@rGmQku(w0!2}h7X~hD02?0Pw;GfTM zbDflrYDKSQf<_BN0AN-f9&u)D=vi%Y9zzVF6D!aptT$XT1TGLv#^0hZnhe+(G(g|F zN1rfgMCgS{9T}2Be&Js#CzxDoh_n`~YIJ)_ z7%?{gw82DVPQ7XTW`#NL-~F$zK1O!^<)jW3^a|1}0Wl5w?nnlZ!3KI4(n|%_x{U~zqjKg zwO5uu>{R*GBo-mMRwe|b6gB$33AXW)4^cdBo-oZIOw)!ZEWatK*ZnUZ$XHr}7B_z7 z8Zw$L>cwE-*E5H0b3n=EQU0*#$kv89-Q=7LTkvKPc1vJI%>j>_%bm*z(uB{D^;d@I z&up+u+oOvR{t#A|e(sj9QSIR^ZO8%*z%gQb^nyi~!Lbe)%dOx0;gn>IiF&D6QFz`8 z)$TBed;w)%U>JMWO~#0K&kAz;ciXEKzO6Ts?I-xHRGw)v+7DsRD8g_w;er;@f=99` zhJN%prkHwc?BmqAS?N-0wzX41k*_FfSJjB5kB3pn`zP(*R5v$Hy_Z5g+ON2qx8HMB za#JvrBvi$&CUpd6&lSKtmA&9iAE^%nFGIUM+Y0BMM@N%?G|W`Yy~Dz*ZQ_W&1Wn!a zyClcVMed{4rAr7num+??;u(aPUe+3strq(|VXE(rXKR~KtY+ha@2(22&`RaE&Z|V1 zd-HX>E^r9Cq*}mUwMXn!EuOHBijg!MFde^~T}7b&Nv|OA3e^Yv!~V>&wb8L<;=lI# z0)m#~VLD>jC!s=iTqq925Oz#I?5!)Z*N$uQgQwYyR7ehW;%uvxJ60AZtqKig60$a5Eqsacd?aR&gO&e(1uNlm$|!`&cc;iJ3=5o!Oy(v_brXx!?S8 zQvkQJ`mZ2^TBjlHWRh}Gz=tLWkS&LqpOK$86_lybpL_-X_SJ&hPOYo!?HQbf~(bt>kxR zrZbv;)954ODnmv|R2g|KfS>A1OJL84+SNgg0 zP#44c5brhkXAEm2f_Ixx5Iz!pp+1#^mF7Ji*Ksf%C|26*F|NT51%?4yJ(OJSkc_2# z7ZREuq-cnxcoaXO60j7PgOST7V_xebW!BrSE}+5@7aOca(`z^BFl`vo<6G8TOr#$D$mq`=aK0$;aFHq9gXp zYVb-w>`i2cA6cBp_~fa&#jGI7YSu{SOnh-$xSe@owCyH$+CU}4vmABOYSuemdzB|7 z#+@6dRmBe)>*A)*?Z55feY}pb5Jh=jDf*7QyVW>WtriHWIC>eW3LBPC$bH`+OGunz zJOpCZgwyI`(QRUADlaLhe3wt?KEird0|u6BL`3q&r#|VqXM;(Z4K9iU3cIuOlO%K( zZop~~nb*uVj;5ryM%tAHXYka62%-YrhJDTyoumNq6Z)4lSS~@!GFRULHWo?wFrOT0 z;e9&as7k@X3;!-^Di&0*2(}-}bQ9#`Q%iPHL?&26_pA$4O>Yrx{$({Kuf>gWqq% z5npMs8bvO45jZm$C8h|qtUE)tUJl(sd8|pUXGcQ;E0eo96E@y)k#Og9lev|XXR0q) zDc%do8EGu~0WA-Ez0T<^4(65|I72VB536HaM!JKA6z}{cts%|_d9VvDRp+$Z=1;#c z+9MsY;s+(hC2!V0f`**!lo#3Ke2g^QBw0S4P(K{ZX0h1fI+#r+ldE(eLriNEeK->F zoN=RDsK>91A%hDORL$#D$bXlg-OVVn*rA0l+nLO#N=`+~Jv;l4mUqT(gY60DN8d-1 z>XZ>hXjjU5)Gkgd-S;?nep=e`UZMdSgk@-u`x zwqsfI=Vg{o_)2X@1W9)I#SYV{I>p6XCEpRyy|zf4MQAX><$?J&-UWve<&ftG@F~8l zX5Gf@7sVu}nx+AmS#G@ot|6T>i)Y_gkAcimrHMC)zC6v*)D`{==5E~VP#n=NMbS0-+u2i23F z1tZl@DS5s^7!iZ4g11M4MPHe@B;K62gfN+PRWShYZky$10JktJ z@2a2rJAR+c|9)Tx=oUFou8=8f^#D2f7xIQGy$00-y9oAnsd0Q2faGhg)t$qfm&+RP zrsQ*7BCOOo^))KYn+iP(T|%gc0l*C}b3<8%%F|GZsUE0tsr1Mzn>S4^hvIuKseGy? zWS>M;(A9L>xYT8KfUUK`p9j+-<2(H(U|C1yw98DBw^F*GkpyYZhEQ}B?J9&VA@0AX z@p2_;BCaHlkEJ>OUHlsGGqv_*@z-cs<5c{r;*|J-cg)b~kIXIlT)j!aFaE1jx~8@X zAHl!9O~Rm-0jo1O5v>npXVkT@aA`^qGj@h&tltDaFL+`cewO*Vk^MKRs88LXI`%#m z)L)F_+QsV+fdw2Zb(4Y-WlAyee5*@7cfQNBcPJcwlMPS^r^jSW#~!GK-|4%dZCXYC z(}2+yf*qmM#4H=(N8XRyc`N+@Wyp?w_L;a4z*!85ZWLE@6;ds;Wn+^Tn;lU$FyjewPb>SChite!u`C7{obX@JJ_HU5qt^ zBh6WDB;Fb3n*PYZ2yO(A0q`}h3X7Sf+9NNx?(N#WEaYwYA8!nXXG0fDeXT#EaA73^X01&@abib+xn*~IRnR5`b)103XGFNHJ|ru}z-)_m zwF?(+r={|5tM7O0J{#~hOXntI>$sr| zBR1|&%Mzku+=t*xf{$Z(qEEnvDH4oSCHQKa{2xkH%|XCeou?VI0aG z1HfI1Mv9XC3Gj%^`KO&Yd>xpNHn*0up9gVFP{x{BE+yAcRAN10`>{XF)F-Tt@Sx$YhvvgQ$LYU&!GBggus= zu47gV1!n^al+J`dwDEMZ2VdT~wnC%FELcQM7@^;KncKBw?+WFn>jQ^6*w_ZxxO?R8 zHnPhz6w!7s7}iXCS?!)Xies7DJ-5*Cqy7&Kj(RTPsjU=x@M3KmCmNS@LM5B0py@)V z9_+bW-AK+ziF4j(g&t-5*`3ohxH#IfkXhH+`SYLCvVicJCY{Y6ZebE#-`9T6{=jaz z>*1ij%YJw0qes>Pvo%(|Fi>{Qwx*71d~2c~8XuxVqH+M536^+rJO*4^IN}JyCqVwP z=o^XI=K16^H{4`2m|~ zu3m_^UZaxiS6)<_p8Gw(>ZP{wt)IAWpldmXiqDu)s+uNA#{KU%&`DEG#dagvO3J|V zK@5QYEADJ1ilw5Xyu>k3(BBniW~1Z(es3SEUF$o&A zj7tI}oUm+;sVjDivsw>;cas`2uy!qDqZw^pH|XJJ<^ASVsa-A|E~5@H3@^)diOJ+^ z<6t)DcunkdRDixKG9?({8b0wu(%l9@G9zP85oN0r>(%4yhx0htwJ9(q%+}Lc=d1FC z9qr#i@o5z;*c+!WVQ-#7Fg2gtg5C*Ak}<+qI9E3|frst81iDu|RzX+jA^=D6?DWm3@z-W6&iRQxUw)TXq zW%t9rL3hk)wi4J2CGx})Nmh%(Ou~T9s#Y6U2G+*)B-s>Z!SFRT_zD!`UtWbdlaVjjFy*RBgt@N<-Wv zrOZR*(dz56;l9F{zpe`|*}4M2+{ddTy}I$>3tyZCA!8N}Npq&6|FZC57O44YM{WgO zNcXja()UW%Zok$|=KNxla5(NX93i4@7TSeUW_DfOh%Q!p5{A5#_(^8tMA$9{OX0mJ z(|0pnQ20 z7hj>%I}0R0Ib}MVd89J0T_qi$MU*&npUnL7^1V(b=8T(nL2wjZ?j>o3PC>gwkC=76 z|5156D8|6;Ir3STo4Y5c=BWuO2$WF~vU-(uQOQu*N1SkP+O!?Xxs}9d4Yt)KW zWa*^Kd%mLp5?=6IAdgEIdu)|X(Z2mYcrZy?Iq^af1y*?Ql zkM_h&xKO^>oVIv+mZvls0YzUd@5hzg&_3!jE=38kF)V{RqJpXzP*~8Z1AWvEEs38Y za2|n}ga^T9rK98my2nB@7sq%+oi3h(K5AC*bpxxu4mZDr>7JtEw>^!I!E(u$Po3?> zc`v;EI-sadw0;bf$efbt7wbs2avjMt0;eZgP+T0NK7lI$4CDfF#@>cTS6!2LP3;$A_q&r6C<)EafUTxg5!X z#SV0?+uigauz?Flrpn;R70> z(30{XVBp|mD;3XGz(ZTN@fN4pAjCtk0?%o^>e=;yl5lbmaQf5YpLKAOd6~FSVPibp z95g^YTHwN#hsc5rqj(t@-2}$4bPtO;P{;czc@5K8?&TMOWT(8-nM-`pe z-Bn%0c7SKK1h&B-Ha?Wp=u8svTf|#FbaavWMB_>PrEdp9_w|3XqpkSG^h|5DN|mtf z`m?gPJo@bD-bQbsx|~I0;Akyg5-pO-l4_2Kb26NH1md-Mt zNTYN&NJw|544u;54Bamx-JrzK-Q5k+-Q6Nx-}(N}pSdQU=j^@Kea~aV_dn!JwHoUF z_~LQ}>z4fj;|IH63>W{@qgfHc-cbZhC2gGtagU!swLe3^U zilNp8^ovY)IJQ!ITkvyn)xR37>f+6}^gaGK+{}V6bLZE*j-8jnJGSB81|~cMfr(JX zwRMDv8IZVylqOW4Lu6S!qoK(T^)3*Y=0PvWUR=wD)v9}x6HlKOoJ z2R`|lG1ch}za7ay8{0e4g5XkNQlV)5C=<@rXDzn)JFLjfG1KNN7hdv*gfx~9*O+Xp zBV3Y(@2S4H6^U`yWiY6AHINT@-j907TfE*@nmK0?0$@W-oWWY~d=fl`p|~t`Z`FD~ z7pwOZQFwIDm>1g(iq*)`>_Lg!`>E3ig;?QQ#>@S%w|Kn?UWBLd!5$?R_`tP`|LQ9q z!%Cz+o!h4F#$*#V`H^f5k3=C_*^d+C=Ox;9-LOh=?eQYo5vr%V822PXlX4MJS(Fm) zDhU-q%vZ1YE_Z%^c2sq{)Tt~+k`IP}$1Ouy=|#-}*hNT1*+^WkB$+PD3mcljk52ca zDz6vVhc?){GiJ8Y7@RwBnl!tqX2d)p~Vej}w^Cf%zk6tHWclv)rFM0obZr8cLH zM?7R1sNP;v*=MLatu^qyX709_s{HEEk5>5dhXAwe@w=V$SyEqs4=klzGjh5Ke_rHE zy>^Z4z}B!Wj>Y-MPbxnXP+C@AE)aPu={((RUW@skO*gPrHk9l}qzyk0nN6BB5 zFm_ulwrjSkl<@-!24tuuq9;jpsNCFWDg=$G;m5DP`KG~ zy*PM0?ArD7ttM6?zm|e|&Ls$IhD-P$e%l^dx_KCjpWDcSpUqOLYiH^;D|OY{Pgiq$ zWNrnWsT}=DtF8HMVNP$HJHrFVsX&9xkgXxcJ05NI(d=3V(-0g=e4Y(a%D)3SUUkRn zzH?!R?9l7SApsp&GW3HNi2~(7VW*AHmyd|&b$AKBfLlc9dm`?5BnE^rmq)r&@fa|p z`m6sdmP3mo#{w9I#iwYn@#%H5l0(dQI(NUz4MM$xGGuRytBG`=HrkxHPEknc+M3wv zjAF;2lP}nK%akbNQZT+M8HT80xQ@lNN(|9yV>3>ebfv`>cwy8yC)c@8&I!#7$H zy?)nM<&8j%#|812`?dJYi2qZM91)8=hh$9=zD~~8cxv-?;{pJcYl41`Cn0jDGdAO6 zsUjojoSVyLQ4cPf@$I@T{^j5nH{??)+ZRys=|pg_rOVCv#H=(i=o8<#l`@oK6dGdv zSrS{(I|5~ugkRG6qk`2wCY6cjd)I@DnG)CCG5O6l3|X!Mbt!S~3kdAG(el&&N_NC+Q0V0drEvpT5@}p>0kDAplS+2T2$!4LH8u9`tS1> zy=l>1_WQldr(01E-+GfY%)P;{Z!uZ&$-7_d?FJoqf4%t?7l=80c`_R9<(4#;{Lx)! zPbTXD88_jOdFz6gU{CPODp31N=Sfm3k}WH0XTKc!7g88C#!Pr?0 zXKN&Nla$m=MnP6Z+dsH7ktp8BFNlnCA}9d(Oe0hSgu1;3o?j8(P87UrX!!zsB%^1P zg0AJh;*0L3ZT?LVv}iX20@`Z~R1*cb>HN)7vl9#TE7pTLlD2eBZPJo|36woOO;^J8 zFpqz^a2hn*s$28mWj}Ve&FPr`BLs}A{>2B{z@hU;F3}B8^Tw&>Yk7+YECMDn9UEBO z4w8G!?~T55aK)Pt6$v}t&+z-ZB5zef47-#!W>rC(9-zJ2-PG3LOq!YgHMN55?albz zbJIn6SP_Eu`>+TLPq6=f#c@G~<8pHzdGH?+0CLh2;m&l9mq{odF_FGu{(E2mhGr@u z&i!N}dD?_)p#2|qJ))^#(1}bYRkMdCd)Mty^}`HfC3(rZYlE{-;1D6=V?!9S57_p!I4!WY)6#c4*6 z>dBPf;*Qss@#;Z?Jl%yMvzhN4VG`sDG!w$ZBYI;90nU^Bq<|Rj!!2Kt@rNW81s;O@ zKXHUY%?C+X_0N8|r>lX*ow>w>R*^aaG^xd*&s%JCiUoXvkN<)pI^x{bRTK%iOLwg~ z%M7T?z5K&P-uC4^aINXxdp7?-2Xm5g2SIr@Q2zJQ8d0}_@N1d>eKPzvB$!~-q`6b~ z-=;}Br46Y(O%nqS)!#JS?1991ULFfB;K_hRD%Q%Q+z;;U3_UI#bW@_;J(yM8#b~_J z+=Qp=8B|J?{6?8~1WF{%vX5N8n2H~(@Z09Mixts;%ZMTsfc}@12skZEmhvvlpe~P+ zlG@QBHcHpD$LTX{5^QO15wlD>Rf_dk`G)m5SU_r@Cf)KiX~mpxQ%Uzo8}J6Bk;H>> zP?8Pb$EY>T4;2VmQ2N3?fqc6IoahUzesk3z!PKw6(QCAtl4@E6Kut^v6?l+oAJW|n zvRI@81Uu>pQ_JG9&uXpL!I!o8^$O_+pVVu1HqBBi!4pUYrIF?PS#T6VRCb+si?aIM z>N4WIx)eY8v7im$eFeB_3(l0y3m-Dg6S2Qk3hO&wV=aL1(my`^4ZQdEyz%yO@#b4x zLK%IAz!q=R`K)iaZi9I>8FFj;``^v7qBS;j<3`PB?n-#S-tF^C<6{hc56Sk=aH}W( z4F@+}Jz>pikGc)H={a^vbr8@)gN;&F2O=T|X zql5IK!=2(uk}3W3t>l~gS!xszLS5`#8#Yl&1=j*EH$ml(3t=z!1{yy-7pZ>;Zv=+u z_;8ndZ3^4wxJ*`|&Mkn=l{RiKNRxbClfvdj1bVY_{dSUNr)NaNe>_P$7tkQoT34=6|pDSbvv%M z4T>(L19;tR0CDWQLR&GEeaxZx6Z{(M;?9%7v(v^^^C71iN`mr> zKy~8P@$e!W?kgJi`nKkmQS{Pe8I`q6DS;J}3ps(6i?JAoy9o4kNJGA;sU` zkswSH1K)D{%JEd<3^Yhec>76sOAV#c{aMMA=Gr@npODy#$z8tUg_OmOuuc<#-! zn zg!A6MfSeqKV7b{v4}Y+$Kg+O{5=;KyY=Gy3u4*aG)by{-Ph~Jz$&I~q5TFY9_g}SO z7Rrk8+WJH~g0Y`(D<5G4R)w^g_N34BxEfC&Q!eT+pHgqhVt&)@vp}8G`C}9D5qbid zzGrzi09Q?Y?JV@NdV2K6bg>Xd5*4g~jlaDB1;Q!k?rD7AXoL14-h$9j$K_6A5dYhl zpZcZPv;NfDa`>}BAw_cU5QPt0SlLw}5RUkF20zj_?L_^IkxX#*_Aoj_W)B+rzD zyU$~4M@e3z*W&>EZ|?A_@{D7A;lw!;m#~x@1dU1pYnm1<(l35|zx_(9w;;%fnYU!5 z`sJUH=E{qQ9ruY8r^sh7=!wwOCpa$JKwKtR?J9Tn?J%(rPw6Yzd**%;|mf_;@E6({#S{4B?T`WRh#5Y+?l9 za=g-GwDI za=9As`I=7BA3&E}KNoou`U%3{Y=O4?=&yeKm%)&_&&42F{l{$ooxexPJ*?2qk9N_I zu z9*}TrogUyzy<4*i-k!bAr2!<-N`WvD%r!XLT9TWAD$Ob7L@=JifG$27VibQc7zd{z zd&76C4aDA7ZD;tE$~>c*EAkdN%D@>|0Zw_Yqs7aaHLuJ2AsJ>Cp22BhSSl zw!#t|_^YMO9gDNLQIiv+D!kQby3fl^!LF*(C6&c4C#(xJiMJFsQ%DjZyw@lHe#=$D1*v&wqmZ-+jag!9g(1UeQ7(2);GHR$L$NFhc^xqG?eOlkm;GxyWUxLGxns0 zP}*0tPp;j7JoqVYtsUGF1#>^%a-28*T1w95ja37p>(d6Ti*KRnfB%Gh@63vm)s`>b zx8rj6_uL@|_jYL7@N)o&6fS00!FJ>zr$!Pu+j$iO_ee2ZEG(jSSis8^mEV_3sxMY_F-DAxfD z6qcVL`y|C0uu#Pa#w{Fv#sV~n)W*&2v%kL5xm^9S>5skD_!M&sbQTK#k>=(CW`|=O z)n_I!&j5Rg{bmNTfs%QYl(Nn_llNy;2<`uYEVVyE;kbCk`VKxfeT zzI^1ef@AX@>D>j;2-uuU63-+=Z`_V>tJAAQO&08r`aM=r))Q!5V^{SfI=-DwqwzYu zj$|4Sy{h$aX(!5$UcuuJzK8vtXK?t8Gt>2glA6ZOxac?HN9u_2ao~vc9~@ zjk|T>3mUE4+eQN-81PAz^A%>!=!?6RvPnh_BCM73T_&xc##ue77?67XuxW@3+1jsb zp6sU-sfx~!9cu6QQmY~d&aX+c{?7zHNZm(NNt7n$DVMJ4Z+UTR2(8qRR-n)PzU~%& zS>kp@-;jM@@Y^OwJMhu5=#ySw5K-FnaHPeMio0W$A@MAW*WBMD&t!QGKNwDCK=Sp+ zL#&QKM2qKR(FmYrPOu#&zU?Y zT)~6nVsdb2yLD&T8KK|xkR#kgxSzy>#e!G(yA(FM(IV;MR8TiX(f<2u|To(IX+ zmM>za@wq2}XirH9R3&>MvyZ{wBBdR#^37ImdG2^$_8h+s--j z_Sr%UR>|Nm&f-2zx8dHUDmt4R-IQ2WA;zC2>p;{Xy5dpcZYK^hdUOwHV9L|SYZMRn zR);TjTCk&#&GQdAW1lYpI{|YR3*v1bS)$z6?VzH$og#tTC|00f_to%8RtrxBFpFD& z9<0!^ElRN5z6(6S*-fwssw^X*w)jC4wc;7O=N5m-YzRT58y0Ah*5*D=)n89F;$7BN zz$k3eqiHn=eH2P<*F477ZYvGU{+)*$)L@5D_ zEfXzFa#0s0d1-DdutZE+Wn7oc!nh7q8Mqu)DkO)XfO<1s!i(LN)R4|7Yg9yMgB9R| zGFa7xjW%mABxX_LbuTq0UrOSF@@QQXZE^A^g5-KQnj0hb6FQZ0Dw31kMS`cuzNniN zeM?05hBTk@u5$3RDR{!B`~$3$Jg;T=0MufpKzzvefzRkYUgfv|#nJXa0`q0o_4_Yz6GFbHm4k-vAAx3=}o0;_H5g)8?=>; zCH>+d9=hV}PY>9!&hyDCiXxJloyGn%3NC+f^5?Fm3GVMgu8-RsU=?dA1ksc%5g7{+F(V3iPO4ku{{~U&XDsxp z3&P>+OvBck;{s}Vn_A^x<(I04w|hq2 z<%JLvB@A$IF?IFkr;#_UQVC;1Y)_}-yzgzf!n1QPt9M?GUDy9FU*qhPNvL4`-_~tR z!{9lR^rZV~>rzav86py+d0{&kuzjK6n6#%!7N%>9s_>09EJj*%GNcIQWUyWkqXKXB zK`38Nlt_$m|3zt9hN$qBqoMCRP7EG^0Fn3USx3-jXKM90eGhU^766B`Pa^jAQHqCv zdlZPUP7-em;)F5xn-B=sws)-{RFsGI9(`A8re=uQTBJG0oEr`|^lqKzE5ZF0kkso3 zl9jpIS_im7{{EhJ)LL>e{#8$B%-em-9%shmfP9$ug7y4Rgp4M(0-i34>WA`9NGe&2 zq~YX;`beuCn$8Ca)y%=wr8`p4Zc{W)2HQj-`oKFkE zyhLjaun_jn7Ez|B%Yx_@s3eo6Fm~*HnSCss#fyzGPtKIJz90$63Io z+=8u|!N6VQXaTH=%8GHbl|1ODwNmSDCZZgbMvFpHJ!6kZUk9)0@QyNH{||noS`aPh;;p!UF!|GcNQFiLZ6(p z$sx^nLPL^A+w`8?_QzKyrd&cF`bFwm)(S2xeSE4!INmvsHj_77dOJ zA&VZp(pq6Hjb1#_{cE!6@#kZG)lhcUfj@!M(tTy^kWao3zoPviQxp~`3~cT#V3WOP zzp)c|sjE~~n$ZXUu`KSprYS8}?-oPdo2;T^qPq3`C+l%>c(h zZkE-1prP41EkbbUc}vqont3VN-){+v6-KWm6iq|1PS`z_+Tl#B?3r zfLkA3eXv>wD6@&T{t%Rs71_L7B|~Pj(1wWiDX&k4G}QH#1|9)3Oe(LNw>{^njtxFm z3QKXfqV4KZn(EJ6TuJ(xv{{n5z-#1Ab&kgAJpXbsJGiaU?70ZJXVO(_Q?+YKFar_- zISib{8K9f_u$SA7-1-){l^OQ)ZxD6T=F#q>AH9vA8eW4FS>7;@;TfJ1VGs#Z`=S^8 z=-!uV77HBesv-y1>O38(Dg#oguPuq+zK8X8X~ubd+mzyo9Jzu5XU0pLyl;Fv{VX!Y z{9Qfc!6)jiqrQFhaSlV3oCzY)Ib5((<9y$ftP%UBL5#Shh?H{iIjKtiGH%NUKNHdX zZd^X)lr*sov}c7N@8yjelWY$+6uF1hX+L$@v z6&pXW>sRuy-i-s7{;bnj9av5}JfULCst?qN2uBo>_L+8>Mb$a^5`PVrW3vZ*l32RG zbJefJx-{)Wt(42>gU)aJuOl3Sn&EX=IE28KxV;&le{x=YoYMp8f+jo|C95-b7U0vP z1H!G!vW_2SYxg9Ci%}G+Y8jbJ`8aT!P|qlEs=k*6`UoGRMG2l|TKXOXSM@XJx}CX? z_^ib|63kjHp@k&SBB8cNt&6d2xa5ojc8bimE{iy&=ADU{HXX(<2#jk4jro6mUGoW zKt8W!4E_9O>ca<>)kwkbIkG7@i*x8n+&`JnV1Ce-F&W@-+tMfgww+Ce)-EOXYn-rQ zbr1mEq|JZ)aHxDPPjMHEs3EUY$Vd@-EJu&Mxauq!rSXHxIkf{1f1E-T$=SWKiT$E zI-V>>s!#d{mD@!wad6Jk$zwK-rv9|YHbx370T8J%Rq62Hy7UV3l6k&}o&WI>ISGr0gwaexu9PW%=vb z`L0lr7AfD7kel+Gic3B(?rHPvKv^h*=;f~2Q(l}{hMgXyDQh8)d6-UL2%>TKdzw;O zRRtj4exK0@9wLUg=y>o~Z_8A1cV%$89zz1RI^M}~-}vu9EI1D(X7QN^_fH)w&^2yK z{OKVAmBQXI0@R#9@1|M-Ch2#&C0))gS8=hn^4t<-x=($UT0hkzla{x(H9ZriZw#jl z4c`%%X*y`qCJwe&Uwy=UN!FNYd6_6w^2`SBZN)O@*gQ1A&H4BL>~Cn}iREl>Q4xgK zX!O9qA#2CY_Qv%qJ~Hs7Sv24;xsx{p^t~9*h3j~QJWrTfKd2`Oo9@B#Z00?1?)$Iw zxafrX@p+d2Wz#rj`5i^{b@!pIV*07$mu_&74{vdBs<4h@6rW`h=;LVqMLsRkxd`_r zBaK9TUd7J14<@7D6qpJvc7$cBoDE62y=PSK64BGjs#|^c z)N;H*S|Jf&BqvAX_D86@gnV4tH@>+iSKjWC-@v?75xmk${6WqA-sjxRVep^WmO3JE(j-L{*5ZGu2+IO1R2y$bptsE&?^G+D)<($SU&gaTb<6*p9s+^b8;s z7e#(jmF9nhft4{ma*l^_>NbaUZw_8WOj~De+7QErnXy!NHkpkx0O#><0s1bey*n|b zUOHXSk@p~#NsOFh!WOoZi62c<96ECc1oaSUrWw#Lx|fxrrC+Ct*D(;jZ{l-~*si$2 z+~+UXHjPYzC}A&`G(X8WK4?;I8@F}wE6dGVC_1j!MUdRkK>uR<_y%0t1p5R1Cy<2X ze_z=Y@$uhWpTFR(W`m}O8b3oa$h_5+i{^0+GMVRIkaj3JcXpH>Rk<;@+4ttFL1O&< z2rVbgH`KWJf&&fZa+X0S)Ju*ZKRo`aiu6r2+z6ocq9Nh_)W_ATDp`$g0InEB7{0BX z&h{ATpr38tuirUv@+rZNO%E`y&Hh#=tGX%6rs$4Mb!>uXuu3nP`KKX-zHtfb=2x&} z4~J?qQShi3PdP-Wk8dPA{ob;tX2&Cj^5h=boi43RR=bCzNGvubc(bVMA(PwLq;-~P zDg8w#Kk+AXE~&d=zxAvDXijr$y0=xM{E|(l-Uw03iNuumEPu_Y`RJe?>7=eyI`Z4` zwtLG#VeKrX4oixtr00K=AW?ZINnYumM8xj+7D(fSHhR-(q}H}#|1XuPRn*FLwDi@UOKL#wW8mGWP@>wRc^zm$wz)rAT6sLgqAdjnhYv?X8-DH*CFl8X zC*wpGB~!0 zC>$1bbJBP5I?H(a(k9zF|3QAap8q}8G+mIwRHmRJqdb{1o?T`+Q;T^=7r&p03a-(Z z+@ifZbLsUHibZvSu;Kiz3~&P7E7N!$!@c;g+iLey1|H1^6M-3ynWW(x9T+_Gagq?9 zholr_mH*3^NUSHMT5kgscMpetUJNZr=ud6DNY5?qwu2JC4SY&H27|QO4Dh)uYSgdr zy0VPqe(}eqRgrk4V^8BhDR}-BeNdUPJCQlG9~{CF2yTO9C`d^ricml;Ch{d_M~6v> z8R)J>CDBf#x)}!CXe2XXV<9Hajm3YdzRG@)yt;5e?(7>u#R8_$io}gONXlz78w(X< zm)zYI38l_0K2R4bY010A5DRB#wOLERy7bxJhvO0Cs?Ci@<<4RWb-BomVGw~PO^sR8 z(=A&HzXp}~N&LztKu8Il`Xg;khrW|IvwKeEhAtc)UB2VvJVu?j%*tG7f}jj$8ByNq zvqH5$L{gCf86caaxOCAn7_&H62Xo}A@ z+OqCHeHU|jIWXlmw3=ax2wLq!CMK-wLCjXF7MbO0Ne7S5lGId)R%zfU@GjR5?5Ys! z^pCIpYW<-?UsWwVfP;<)Qa)dPF53i6&7$$Sn zhH!8{Rfk9uhi=!{X=j&5gbbfyJ8kA^fOlHFq?MRa18FjR_dOkUF4#38s5odXl*sM( zGh!Y)N}^agq{PlzQP|X{hpmHva%2}#tlnYnoAA8w{~2JtJ2-gY+@)FpA@cV)@Yza& zqO%zU=Lp_CI@~M8A^TDdso|yD!CzclNc3!Ig1&3ZWfd%~L`^P9FW$u52dpOg)PfEM6yFsR%ojv`7;IKNw@&i z+8e!Z5e+TddoWqKUa10!WqzdupS}=;?xEzY+R7Tc?*6#<$(rNaLd5*+5W%JEzLENP6VEiIpR5b2O>#xmWq@A5*pDIJ_MFk>B(r@rHZYfIBBDk5Y zsgep8KxZ>1g3lgl2;=#wlGN<&#{#2cC#K?1{gXDYGvR_{NI_)Rz}b;)hwdkEoA<6Z z=@!kwPIHQvA_IGf-7kOtuEZ5!xx4F4m(1ZMba-biU;}djQoC& zHX6`tDClmf?$ML7<2<^QLl_$jj8+6CxwH%bZW@q76X`|J4qxAQB8|QS9Xu8pOeu3G zr$N1&G+F7VNsUo%CH7UlN{^JP@iILuBL@0*#sr#Li}(aoHt8#EJo6_^g~BxbOT-S26);?-#T~w^QQK!g8ALUXaD(=H^K~XpDV87W?ukUGS3O0R3{6e~uTL}uc+mvA)hBuejh5zAr6HVdglNCA z7QPi-S=yQvPyH6(d^`=c%JSrc>)(e~bbBn}WvV0PE2jN~H2LN{RnLDK4U?rYvaX%cZyQI z!`p2y4>@sdJcf%8u|J;rUPF-3355) z0*~_dK6nsn6@KwqoJ&%6i=)r@V|+JY;`B9HaI;NQJS5|*V(R&r?S*G4-!(!@>4DTQ zEB>if-$<+36mOv4a@s&b76a-%Jr+1;%*;b898N`5jePK<7s^6RZqzfp#5Gt~V3{>Z zp>(VW0MvXaP#Y7^m!X=$5EK6_?0(UJOjb6ZAK259W}RK9t~hQ4u?WC|mue)+78)HP zBl*V}Hm=mTp*i?`}3P78R zDj)F3|K8u-rS8uB6DG4a#LA|MCV7QHGh6hSrF?V#xZiqVOGH(pDlN|5{7e~@=F<>j ztws70LD_I9yfrl7FZh%7HQ1a~QElxJZOq^qc(^j;qT;3^uv5p0m&sCwUwPn?yxNUR zsCvfR)rtcxum&qED+%gBGA%?mz&U*`N(>gi@u@iU;zG=|kai~rHQI1R?r8F!C*PhA z-d^tl-mdd3aAZdDx-BPHly5qPA?S2<*uuE`gFCvrV6I=833_S#+4xaD!9BJLnAy6QJsVnflL zxsfycw7kee&bT#mefU&wN;Xsg_w(J|T|Muu0lfS>kHL+FLP)B(TsPmik9Z|VOCLCU zcCVbSjT>)-BVcc!=wrhbRC7%KE0|3y20E$J?ZF+ZEKm3>@Ni#YFPAKTNU^i&;U-El zR}K9(e!cFx5v!_RB{Bd*<}T-s2hL;^rX|yY#LK0EzbBQ!5&adLE@j?LxARv<&~}HK zbPL#MQDy&~pzO{{$kMC-`W2r^a+I-TRounKbGY}Xc)iP`+}Gm$3}rVL8wJVuWVVTm zPG7H5%@z9lz23){<-FJm0}zZh;@cejQ0#J|BIuR!>vSQ&P@gNVn) zS=29O#sc#LwWb--YjMQ>W}gz>(ZsT`S+NyA35Xy1lrFq=?Ye!md>qblKX&D#a<`}> zw>EmTj#tKk+KH)hhG!7|_y<+b%|m=U0G%3o#fJG>j25Tk13qLrpVbhq2mwq93@Npq zX`nT~>(IoOMH@sLxP#fyuQ8jo)V&^V=<9XH_&>uPUJr!3LFuq>xdD$>0qyQdcS?#x z_A%wB1l!aoU2CJy$<;;FMS^*0UfIjrN;Tn|oCl|3WVSv7I-7NqzkWOh&@~ZPS&foW z1m*hNsC_W(wp<9guz6(eFQsQFB3`0_NsGH(L{89YDH5HNmD!r|C914?$m)`We}R72 zYIoH;s1i12y&iqTgMX>LKJvI(`getJVC_!)+d7LK59YHTJMa}~W(ffW0bLRcE9j)v z=z<4Z2Pf4f*qzkwNySB3%x97Jxmb|-*h@KZZvb{rR&s+qJ5vs5b`X4^FD%Hz}`o?4H8Xh;j_sqMq{ItUyIA&5m!+r|t=@mH|A7cIz&Nsvz>wfY zsKPz7j(#?r{ywizs1daxfbL3;9UCny@(E()jQs;=_T{d=T3??O2}q~kKqBzVWs09J zEY+|c7dNx8(62!Ok_d$1DAOfPSdPiBcRHF^{A$|txx&8Jd*yM{;S(KS*gI*Zsa*|} zQZ3vua7^dqtSlrkr>P3yB51=O{wy}R`x1bZuVBjcnLvLv23u3)%5_@520rUkdzfx(Tsk5i?As*bJH^<}hF@bKL zt3?=f0rghO!cA}sPgFN(w2u-S;}0)rrR?%^IC@zrXlKjTGe9s=ZG>_&iF+ZN?^i>} z4fd5!DPcn+tIpZQ{l~1Im^Ok)MLz4P*(!8No9o@csX~?4TuJFK+B||573rj*P%4Xo za~fW6@t|TVmaVe-6kB{!iGJ@&_*4<6`{S%CSGde{;~2ZN^mEmkPH6N-z7f|%_ntA~ z%PZrD?`PgUzV~|@3v`jm{1`eGIRv9e?ywC8HuCe<_Bk_xJK+B!=VGBSJ@A1em%_rM z&)eTrgO$i)kZ5#U{Xr9IGmlN#<6c6GjwhWg({4Jc?G01qRr-f}+Z*v*64&k}Uu^MJ zakMfxD=@byEktR81_8D3LnoRvu4@ZIRAdCi)4Q0Oee?EuwslU`2Kw@68|&osL!c`8 zgdwof>p5h=^1ZEid_2P+(K?YSwm5i?7VcSc1OYAH$R^~#W{FbLE^5%nZojFt5-%-| zp8?UdtA9l;1JViw47siMvi)ZlxdG{RZ)43G=2`69JMVpO;a@A_unez#yS$nX6xouG z@x<6wU*(1sw!9F>V{a)*cB@cDT5W}jO^5iU#9fS>7GNIqix0Q~d~T&Fs+lY1nx)hHnr{ykse2oOh6L%98(`(9?Ht?q#m3?l_Nlcf0jcl_FdJK3 zc;&rBd#N`y;@s)0>9G9ByaHd5SLr+ZP+NMg#$aL$`b2>Gtn)RE@#y!B(bmbvv8?kD zFm$+xig0CvhTur``DoFaKmX@V9f@it7JpNhc;T=Wa`kOYNJ!olO6jF6V$f zjW56GnDBOTBU94j(FQnx;@|5PCT`QcY*xtT(GhxC_{+r8GE)MPDSXOYh)f1rw>x7T z@ss>=J9wnm+VVlAQZc+gQu+K==YtcEvK+|oPmP{mz}r5(Deru5rC$Bdhzc0~!&BfL zs8QCal!l08lGS6~&~?jCDRNb-z3&0q^eYF7ek{USLDj3;q{^5%q+;y+{HbV)KzU{C z0rMRD-VxP`aC6p>nGbihjguWVqf_siZa?R1gK+y#s$I926zX5mS&Jbd=`xvCwc1uI z<0ATP3J@$Z{T?4hFbjfL;0_90KK@ztkLvWSv+4&u25O&I@ul=gM9#D zrk`&<%{~+Elg;_^&zDBPw`*rI^IkcQ??ct9VADvm)gS6qIxMUO9d1Qs$E%A_9CDZC z?b(@3*vEW2_PpkL;jhLT)M!BXoYK)9bwGVy`a#73#*Nh8#M?jt>j5Y_{?z%mifj}n zCl`W|2|wU4r|foPs8!uC-W9&=(T55$=QU4TR=a<^ZK@NgT3ENyVXk6~``YCY|Nl|V z(7KLZ4%}$}VT@n?Nh0>{k`TS%m37z{!=dl6YD`uu22! z-*vi-D@OjAn-SBs=+;l7`_jCG++*1$Bh0>scK~&a+*1|kFKn>Gi!dCuIWf} zZncaZf%2WtM4bi`6tkw4AgGsC`@G3-&}sP7tA>Mf7}B-z^XuV086$aqWS6?dz*E|x zXES|Sc>TWR;2Ge9i@z~#S9XoA`b;xT?%x7r_ zpHOODzA`edWoq}ZSt6*Zbg>>{wN7K*l*%JbjXlt2Y`a)2Y~Hh`H4`b{iukzSo~@Mi zi1*q5LYtrY@N{l39d2;5gs8B~Bx!2)CO=X-b_#Q^bEyn5-?mE~VZ@~L9HDqVy^ve; z6=y%Vb_HRqDIK1th=NBl1&xJIn?+|&G~$sSbkg`hL9MVjE5JA=rN|v(z$hs33M2AY z!Af%h8^jq!4al!iPDvL>g!vkcDlIrDbzBW{q7Un|M|QNuBOc zO`%I6gow;~`4!I2O+QH1R_@`tx(FLqP#+Xk zI~_YWQUpx~s65wy>gUIW`E>F_eh4ZWcklX--ZyKOBK)^+IU9P@mB6;@Ky2I9v;m_F z`L)M)Lvxu0IS3TCVURwo|?o80PfvsWStTGV|hkXfOfhhcAsD;sVa273Kw^ng6w4m;ulb@aika zlgSnW1{d#SVuGraqSbk9`Om~L5be5Py(IKpih_e<$OhLAud#?si-||C2e?H=&;h6p z`!ZNP0~{;Sd1DzGJtH=F8 zq>jO=-m(&Ui_iITsjp4^4LXXH6v6SO+g%wO&zIjCsjhciO@c>Cu-jZ=>v6pf8Pu@y zveVwuy1MiP4Bt57yoyD^o6ZE-S$*C}C&nGmp6iSRC(K)1#6nl;O2h%+hT)wrv$}uK zlAl%S?&Y6NGNH`MkT3#6C`xQ8hTZ+GyvB{fbz`Hi)SPH}Ds8!a`iRA6;bqMi! z<*TC%|2pR|U9El?^$a^;xYW3K%qv{pLFzT*4z-#=7T%iSe3TW&dRVDtaxG=!i2P$L z&^3{AojLM9j?OZm$@gu;QtChfnUn~K(vs3GQqtWG(kb0BI{hKi-7!*XpITke0tAj=%inLIR zX7Gye8jxilCH zR#z$QFDU=)Br5^lkUZ)Wm8Z#P5j}aK!L<7YY_nT8ex1>bss>!1Pq$p=&Jvw8r87vr zyPO;HBol6vip|U;Zdz!i>FDo(+qkUw)PF>eW=%$FY%ALR2L;6(m=e!%*&yWg--67U zxRBy%PZ2g@n!j$bPf(|+L-I!a(?cA(r2Zc-;qU^6vKKAJ5D6x^a$UJdiFIY_E(;4D zAN%!)0@QOi=BYR~BN;?em@j^oOniaaqyvQ}jxSIp-jhz5rt+n<0)SMCuV2DWQhoIY zg=c%!&G>6Ja6wsiTbinkv(g}#IbAfus!MrJ(}3UtW$75Brp`*I5#=X3mV~UIg4Hw z3-j;|getq#g5ZYF*Bg2bS>)7zO9)&g1$~uabQtt-sA@duvu(_4IzP9(#dycoqn9b1 zMZ#HB|1DXu{`3L5e{2=#JqvTV4cF(@kAE2EKp3X4 zt0x3LxDLad3zSH1s4>E~&cB^uR(ij`{L~*|f6@(h)o|!Twg=koPSSv0V3w;Nq-gGv zRN{OU>;Ah^yC~rJZQi>{r1X0@L z;O|4U!E%^cqhKT*x&WrRo4?Hkj6o1Q z_vFXg&(eu2FL(Gmc1s?r@E;n@dOwv2W$? z`woT$z*kKR+M6L&=eSU}pt_%ud`8%PuF)<^|id!aI^_#76g;X zJ;iNaLgHw~K6O=(4Fcz0jTdM7tCH+*?|$a~T||uJT_&nlXxgh@V9=N9^_aPw+3CbC zfOo14N{60xJWG0;+fT)XuL;)Wd@k4=^b}!-thbF|XD2&3U)9axJ~jerYR_u=mYjUG zk{SjWYg#-n+VOBQhg&bWgPH>OWySDs}W3X{an4&8lzqadgr?? zxkK({T;~-AloC!I^Sdub+xhV^C1!B+6~E2dp?CEfI05YnYyOvi7}QtncB;l6KSrrO zi>7()k-JP0hNB48<^5ZwGuLBT6;z!fy_BwrXT?4PQ>S%L#b|?S%&Xq$B zE8uyFwIX_WL*rYfe4WOp#>^(aGR-16{n*2I>Je<0C7b>gRSZ8-A^s6jy-6wrvhIGd zj$Dx>nQkcN;L9S9ye(^tds(z@vWk^S0ILNh1O0PW>~&y?t9B54m` z@Sqt$*-6B;MD`uGn^i=!y0u7h6n6<|nPYRs+$N~k(IC*Z&V2K1Z?oQ;C-`8lbIj~- z`NQ;kZ)ysH^5wqgnJIRp7JU5>5}71r6SHRFj2WnVMt4OGgME?*tZ4wRuRV2I7aDO3 zJjswzqxXR;5xuRNQ&mq^^6d79Wt+<&^Xgb-v5Tzxw#;ART;M0cMRZm61MMg0l>jf6 z?Y@?vnMTzDY7xiQ#O{IS6-*ZrB9BNkM7kR+NN+{orhnD;_w4|yb(E%Z9L%EbJv}Gg zWE!y7XkuLWhLtn_*Aw~EPbo>E?CJ&K5@npcyjR5W&LjXZo}utNqKyB%Z!!i>+tbbS zP+kK9?w=tSJTl^Mh6VO{vv-oUQ)vDP-9t{F((!iOK#mvKOvgBsfLD;a7_Fr#1`_@S zpx>A$n->%Pv#)iUA|VVQ4RL~X660Gy>=pmEa&!{o%FEYDCG%1?Fo5+Wj8;hTWC2_` z9#?FP@8%EA9e?*;DEMvv_S=k`H+q!G$6WR+D+jU8pQ``)D_E$DlWu z{dYQFdw06FBk6yhxn`Y92@G!4)JhRjlks+FQJ;0A56dFL@}_VWzN5D8DH=lLNa!Bo z-Dj9pS4*$-~peLtEqop*gO_!Fp z$?}=B)H5bW5?c)&r`685LgnjqAY_bqQBeR)&dAu)@-HK*s!>Xo8Ni_cOz^@DE7Gc? z=E|pl%AE93)fxt59kj`Jq*|v5rC|s>qI_9l)%o&&9SGW&>6s36;gT5+m4&~Q2$QQC zf9gtIK^LbyKL+2vyRrt^OgkZVW#xapUpfv1$BdBEiK2~TA>|K3|6Tz^Zg^Mp2*8rpVR9}Q5bMCnL5vG|C#f` z9FS9OL}fK*w@Y}jPv1)HxQIG21eQOG5-fB7_ZhnUYWLqkJ!)9*e^#_8120IG{y@TwM;*2&bCoc1*wk&#g(_7n>K*=NNR^O!Z+y+UZ7nSt!2ZP+mhh{tB zBb7SlvJn^q1e*chZb0O636w);;MCIXv)TtH`A2N(T1lokKN90x%3L)*Xhg^fa@r5$ z`%4$4Bq0bAPdU7w>8T#YaR`Eaz%0btQ`&!`*s=v^gm``!*7?Y#>n2pszfiC;Jbct_ zvPv>lqcZZjxAv5xeg<9G{ok#G31o6x3Gseeh0jR-@w5{?}<=c*u6goRi#szu!-aXrH;;(r3;E`lkq`QVI zyFF8ya7i}!d?IJ>ho#t-0V>|i<)~t>-JtpN$90XA9NC*U_aqGX`8}n1mjJ>RP#)K6 zf?-`<7w3k%duCcF(-G!IDUn$-1U{L+FXxw>?r|+>N(;#E4%iI*T3}^R2Mv9gU!Wl} z-FCTW)~Cr{squ|)C}q8#w{I-^=O%f;fSP%pYMxoAii~P*F!9WG1ATCQsl&UKDYMnklW(H9mkmggFaSDaDO)7I%K5g>ssF1=Wga1_Q8~y#_h?$c?)!w|j~j zQ?2+fd?bc9$7Gj8^uW*gKnAP!Y~)e{-9qxR33UuVtv@x#AF5w?G99vZ^AQWYYIT+# z2jCk5++VBTyG!2V4{6^DoIBd9WS6Wk^LAeDMdRhNR)hN|!K3gy$h@d@H2VUd&*jtX zWR7P$^atLLFC?5Em@|M;Ifn7}Q!kVtr^atvzmmMXjbdHDWeKVQ#(L@T?s^d^!~T+| zG*IB((dcSY1MMG?La86Y>y8ZdE`g5qU1~lLiwK=}n(- zc|LXgqaWzp9N7N`pjq^WDQ_DgDyEf7QUU=5~D@=C`v7(0ovLc;cOo?I#Mj+w! zI2O8Fa>(Wyr*nB?gL?DM(BA3ss`9(yLVxXNU%Jn%&;`JhBjRD1(8dq$Q>gd?`7%7f zEB_e5Oxhm(X6hloa*b_UDmKmAQ@&LwjO56preh1h8&V`~gQ+xGtO3#4FirI3=ZVZA z(WvNZ{|Ok;w1T|-R1otXaqKGpjN-&^5{-^QU<5(gKN@N;ljKJmA(d8+cmlSw0;ng* zH&_cvdmbM0b-;lJ>v*F^@3*4&?CxWtZH3OD>D!F#9ju-Q-L_%(_5!f7$vO*z?jhxl zfRcomi3F*$oZqckoJ9wJG;e;rKCfwT1Hh&Np2yqGE>G05GE`1u>)pqUf|maT*JcY_ zOC|$%tX`xLO*YRcKU@7r2S`C@4lOBG&JuP2!=;Ke)t-yQn((L$_x02WbPMmfC$nh2 zz?pR{Fl#BDM4R{(qWYV$ZdduOb(VIcd+9m;)F9&>ini0+2H3ud1vqz}@uc;Rz z8-EvGKjwBho3;P5Rd{aSuFdOwYkM3$z0CX)-*xNNOX!z&QwY6n7HQWZ35tW;t+WRB zeROVgolRce=2NuYLF&7w=OxG8c<2!Zxu-?jYMvT$@#5GK2xeiO#4z^NN%Rk|GS=-P z(KoZGacrprCD6d;WlKr+t{EHQyjwsi5oW>;%B->dV1K}k^PWEd5+62((y4x|0C=8^ z_vIy9lW{Op?!OQC?AMw+$ln%_w5|6ySfliqBza6PR6yd zT4^?nL(AByilrB7zbk&g^GqG{og@lp5&gs=|0TD#KgBg(aD`y#qNO6q+^$nN)m#=WLa`Me#bt`iNR zOk>>a(G5*w!-=Ljc!C!TJ;YfH5HU|kaD4KeD{`KpCB%C2r>0nk1`l{OH5;yj=-l^VaXbqoqe4gw6yBRRSD}rJIHj$Gq=4H94by9bI(~e;3#sS9uNX2g5Qv!8 zEe(|5>4f}zNz&X@wVPI4WZ(LU?Kxas27ETedv|F@lS>8Q+9@04rSShGWtYTYBDP@{yeo7)a62Zmx=oBM3ky3p?G-^F6tAqSh zD?xk9^=vMlmU*BcWz^Ka(8%r~>KI4^H=jL`9AR2JQRX^~A12972#>2CBU>ljO`qQ# zYs-V*A<}Pk`gK2C)Te4X)GUfiXB^*MRKmmR6|i*toc<86@L z-=p0*!B57OMfj$d!*y}7-oVyT#wl5 z4fEWqXZVx(c2>)C$a)5v9laPWqDl+U{F(^A*AVa~>aYYIvSpBR5v<+;RORL~gVu1a$J z%uc_Pz@1z<=kr6yU>|1KqpQkP`e&GN{g3bvF`FTUJDYxm zOWmg=j|YTz8-BSz4f^%4(4eyD;7G&dt3sweZ@U8o*kkgyy*YSjLnC;id}P6n0oEiC z%Gbq;sqt2wmQ_vK$-Q6tx5FoUh)Mh-lW@_Fg$GX@;%U$?F!K zLE;=QagAoSHu*dOCOLhLDW=w|AkEGM;P6-0(4_JEP!^t|_^^Na^dDc&RD@oMBnEHN zG^I^Xb;-T%bM8#vUBT5b;H<*d>0ff!ZMQAqD{K@%<0=(f8aTj*ci|l&2~QDA z4dfg&Sgg81!l8~KPW^9$gqcy9U;q<$D&UQEArL;Ro(cWeZkXiVvAL%-iG0*cehIR^oQ{|ROS``0RjUrJrkx`K>S8XQpRZ zSJ3na4AW=*SgWq;)8YY!W{VAumWCc=^WINYg4*76a&4h>=kw`LOT~?+jFNgBy(iJ; z&$LPUkCSRtbg${E$(A=yT)=wkuLe@rnR1_=5}O$!D{aOWDB3?(F-a8vDSK#!d{lZ7 zct!#YxSg1DlJ6;q8Az=LMV`JSnbY!Qte*K))*Qg{L<`7pt?rd;E)wpBCBEO@h3xE1 z4n#@-TDEMUUX-&B7*)wJn&6V7tJb0z4;D01$O|6V1jB7fkFU5T=mh}Y%Qrb=)@F@Ao&ABRuFzuT^DP?Cy!3!N_;1FBS=YO0B30_vA;f!OH>xFi zqS@mk4{W&{Pq$-nj-;-0ihd32vSXA&647oMB5!7qdC4hGnm8K%yIf8r3Xp+_2R(i1G!p@KV~T$BTiIxTB%UZWhEQ>O^L??Ya^~|G&)(y zSdPdL`1!vOzFXJ#v8Km@Kw*x&#M%L{4+(l3TI=M`l|5beg?yxA4ICTzKo=0ARo$ZC zh^?ZPPUTr_r#B60^8ByqjocYAErg;Qccnr0GYhbVi#6~ zobtV9@w+uDVwMcF?JYX5y;Agbn&UFltAAR3T$|b(^KeN^arQv$Gx|M1*dz_ayhwZM!9+dBrjzk54S6se04> z$zcnAYmM|SO(na%VwtzM)8W+}#QAvQ_@-Fam_0r0`Q?q&Wv53E9xI&Gv;KFtd9bw+ zBZ-d1@s@ZM8quoqffa8eFc+6<*=?2jI{Wx$O%F8uRo)XTb6I-a@|+h|xD&3edGLe6 z;4X{}t4lBQ-#vo2-k6spe0T!A+8uHV(vWs^oANe~`D}y3oF$_9q2wV6k4q8nLCsei z*}R<;{amLSUZWkC_fOyKxfK$XW>~k6l+)yBK?ChL=r2@OE57#DRuo-Ofa8=VjDg_^blXiUE1Hpi1L#TAZ5-P3Xk?o2iv^YsD+?dfuGONUCZfHlSg zqtqD|M?*$<-qmnX4(V*akB4hKai}ZeWGmO?^?wxfK#hw-n<==gGW=y=izWk8W-z{jY;2o+4a$D{ut}$Vi`+ zbV~DyO9#RCj_A;9dcZ!!Vk`J_rR6$|EkLF*gAZ-u{XAcFD%yo|%seG0Pq=|Y&L1l9XI-00%|itpbr zjKFlKj`zoTc>4rYP|>*Y~en-QJp)o(5gQ$x6!)&g3+AL zXh&79^`Bw;a^srOIzlSEAa)fRtm*i^#e$Dv;hTqVRi;1dBay2n!uc5zn1M@}zU?lT z$N@C`$h`OcZI8CyHfi*?OFzVgJF_`!vptbxn*qQ1x|&&%Z_UZ8QQA*pBY3<5noQ}4CCNWCMv z&!MWQ&Q&O45K656E7@Qmc!B!JJxq1M!o<+&iSU%bvM+aNR{wnBmRv8jA{Nk6_r^H> z{!sYfOipcow|=EePZFP~ijK(>O@HJc;WRV!VNv|;9m7GMzQlYs0D#CR&p z@3W7N|JENba4)iuaxqXRwH)Y0u9LM9+<jJE{_DG zlsUQKc|q;>7D;I=5m0E+y$vr@$4T@yY0WLl{(ia3gUT+@WS!`HS3?k0Z}q34>8$uB zo2N^(|GknEQxmv@33+j=piMwq)}eprcEw|fB+H2G>65bPsw_ZgV92r)Fo0YKAY{z8 zPL*eW?|SoM=6YH2l4Y2m#vV8`A>aB;BSNO6b2$Uq(9=K!1RW0C!dZR59fn|Z=?oGN zYaL@4R%581P1e0vbo|W6++f|V%~E;_M_(!{D+WB}fusE;0;}eDES120zqu;9c?uP= z>vfxvZ!INxQsnRhaUIMeX$QSF%kxzn$e?j_$1btYkI2%)7WzW9(>Fy3<-=|b{0%Dq z$==Hyzbe)pU}cleljv@gt#Yzsg|*zddQCMFn4MzaHI$VUTph8+^wTEGLNu0kRfX!O z8dcClu?OWhyvqrY)BNMWvATKW84|>nmJE6!E%=>EA zc8#~aZoZ;p%`?#5=ezLZZr1Q7(!?|*(Vu=HRFD3aWA-kaS_g0C;a{!A@$^lqQ&Lij zK|~FCcS~Fvrp(`UD(gTaQ8q8n+^KTnvZuJ)-*dLU1HNHGADriZ-|bD{gGk_uk>%no zUXohhVs?{u5?+yi9W?mn5BN6l`6NXH+W+KS9s15MIii5h=4Trzw)zn)h%ih$J-!co zBq}P*T?_;!F$w$~U7#PBfzXk3ce3Q>e&?O;a_vaqt96!Fv}?i^%+YVBri?%c@E2%Lcel35d5ug*?xhs!+<~{Jo^q|cCjaKyp0nsz0`p$K zfKE#~oC$VtwOqE-#Ur=yeJ?ghOvsoM6r8=QzA3ql2XXdF;G8KD{6}=wHVNEb+@8TB zp+$h)obv$oSh9W@@Egnz5_YeHp5z383_Aw9uOyo>?;UxF6CI&Y-Q=e{!L;Xn3qEXk zLm!B(3@pCVA;k`ziZ*D$-)cc4Bk+&C`bw7yU@Gd2r1jVk%3}J(t4Unr5R>^?=+^Z# zT4at&@u5|BL4WDnZbr~4Q~IC!ot}$tJRi4L;1^-*%~~&4wX_%)Tp4?pm$3e_Vhmm$ zGRW346;X9X5$@&D5;hu7jMYDddM56YZkh*Rf{X+Rl*27 z#Gk3eR{xf06>}?gC<%y5Mz`!2X?m2j?zVA~OS5j53scKotE9 zqEIX@hhH_`r5GLPFeqrO&Q!}kIui}$y`vVo^yh8bm1vguUQM*YIYVw;%CZ+143{gn zUxHW{y?DH^n6^=?x%Z^kx9~npocCFzj`qtvO-d~fEGu;%8|{098&$S^S? zTeXk7zx@Odl>{u`8?wwJY|8+C<4!0WmH(Z%kJPZxVO1`>j20=_+D*`hRk6Uz;E-A9 zKh~@!jK8u>qq)URPvzxnF{ujeFB?uWGSbLjYs87m-f<7ZC#ataT#?rrEuX~SB#C8} z(k}#7n`)Lcrw3|k=)C#eb-x|=!-mt|E@Q*bf+D4ijjKgMWe$4!!%J+4DK`}AudmzN zt35`p2@C=6GMahZU~>3|pB8x!V1IPcX}ORpMKs-{`@ow(Yv&v0PaMTL%le_Yj=~%; zzm9GwN0>I_*9f=G+;aYj8QMLbPi2%Tk6xGL(m!(ZpE+UxZ)lJ^?wg#h;7#kb|K~fd zC|%MO$ksl>280i7Ue^7CXcId^#*K>=MHG*zCF|AJL-lP`A>(z$GRgYz?zP_)HHXu3 zfK9x9=n*zbtb4U8=3#hIPScc$F2-dg2-}=fS(~MR9?p<$Qn$bVl0}KwtifB?1$Q(^ zv1-#Y@3yp}UHX#7zD=Xvk*T#L^_8YCei6M=`vUpJEBfQ^57I9zbv=EbUY9Z;npvKv z?<;JPVCV=9dC$9pkUnjMwFy!`$U&=;cueoA3W;1O20R5o^dlMzxe+&sTfxtP3UHpX z=*_@DrhEVF?{FQ%==7Z}2n}SaZEGBRq$8W!PK&Xno&L&S(#4-C9h3d+q1k;h)4g}k z-gq9e-zi9mnW;It?;O+A3i=AiV{(@R=i;rKq`)w#jZY6}e@198Cg9mXfR8h9dg7RX zc|R{ixW{&vi7e>Nd$f4F?$v9{Fty4$@h}Qtzr72;d96H;I2~o!w1%G=q>rJ0WZmsF zUMke5Qw(@HD5)Di{6|a>Yg**vjNKurHEsFB{ofWF;HocI5+I#d@eQaZ2f#MAvg2o0 zez&RE_H5nB0X6)mx)QQPZfDhmxp~UsYt$7$lI>4&rQoB*8K1DK2PL%2 zid>oqbAoL5X<<>}{|G!H=$C)1>pBn=!ve#e>#up>`zOZaT+f1^X0z|9O5M2#Rpsk! zDY)NF7#&T};)>hnd!m?3KKKwUsfl5wpP_{!tY)vCQEer)kVL-8Ebbj!{2NrsV5f#%g;L6CGzpUpI*4PUo3r~d;fb} zkbRtj)sjfNw@&`r=AqzaRCYA)^XEj1JnjD257a?D{nI1OPBjN|!qs{LY1X;PdIwg7 ztHZYZ!Dw=?4FKgd<{h5mrmZ|K+XkZ#s>73)BphMTnBY|s> z+045~&V=VKkjZnWyV@Y4?UJCTJ|wdM23SvY#IvW_p8^#-^l>!}&H-bqFVc5jWxGozjGS8qv%>s1dusk{7|DYPfHKo~7X6;;x*)vWeDYEwrY9W_ zazBc^_$e&R%WFhd_I;I&0Y_=yAEARz%nhOP9a+m(%W-Zee|%-bw)7{sB}GPQaDRqQ zcujWd)YQGk{`qid{y&Mx$qtWy`HNR{j_!VTX8OFkR?>BvYv)p&XeNsGRgvN6n&^;0 zKj>PvF$wKI$XSyn&a0;TEdQe9IA{z1w+}xmU-Ktwnxnb&-vl3u!+A*lZ`5El~$rk9?AaS z!%2H?7>H8t?qnDb6m~+?FLYx3wNFVl9f+rb>8Cuf$370y6jwK2fcelYN$?SMNJ5Uv z%@)O5h9DW!L0BV{XVHDTc$Vv@G^qtbDs#?`?H}$@fIe8yV?XlJ?j}@w%+kPLbMIVg z!vmYp=p#X~5Q+@epR|^-MlDKH`|2A0ispS?vF_{zS|T96y6L&-FtMi+b;7&3HMc+n z+>H0WDn^%GQ~PoW+2XQ1F$X@un!>9jC{dzw=c4t+)SQ|sMx`okl)vw#mMZfFg;F2j z`ok=M`|<^EbZpV&@N<%@&@tP!(EM=p&yyw0e{sXijr=VF*j>F+(lRo&q-12lx* zHLMpD9q?)w*SJr0@w?wvt8KMoE?YKK8OA((kG#Y?USE7YyTqF6H0gh=qfW-T|2uDc zfQ=|)I|`oqR|;s@Z<`^!GcOFWzwYt>^K`w|9q zdZf7|uXVE9057t7l;dEyh&R_P`vVDy3Z8=$^wl46w$8@1#)FI1QMC&%o72PYP#SBzWYGb^KJvBKwT6jP zG?=#@JI{gq9(j9tTUwXaK!B*E`cg>dDOA{X0$O8}OnI@Yy9+>_|6e60)Hk0DW<#s~^ zTWCI;)C-n(V?~Mcq7mJk;QQ4b)hN}Qxhf8W!!lOXbD#uf)GOLx2I5C=+vt2%_@hCW zuTd(&-Ffo_qvp6+SDBYv%wg^kVS2ovPv6Z=`UspEy!P}a>knkrFhq9@HkuP*W#Mw_ zabWb;lZ)4A5j}@p%n>>>8_V&Ounnm>G?jY)NeyU0(VUn)Byl%p6!A^2cgZy5tvKBC zHUw*nf>(TbeRU^sf8Q@H#;=?AYMe2@6+?;0*ka77yI~ z32(ZhI%~I*2I6#^+>ICgeLrO=s>8%i(TFOqGVlua=yWdzQ|MR_=!xyhS7BZr35E z3}fo_xO*nq#O`gfmI$_=46=bVniFwdO}2!aP!SI<>vr9O2v0ZuOHFutUH_pQudWpubqMuoxq>zTfFm2Vh$ zPP%nM#}bwwz6B(8niRwo?#K6mGWOZ8cI>$Uvmu8k{!Pyv9+3|ZMwlYEsC^}nW3m6m z2SSNIB9{nRQ3i({xCI;<>`pr0;o_ewRS0NRM${T6@~B{!*fm6U%0}|zWI&69(>~GJ zULi(>cz7(XNm&e1qy7eYe2=EAyj2MTS}S%Cn1F%$vKjr*(b)+Nwgo45{8*D@{AlR&{DlZ~1J?N;|y;DsYAkEbnjk9@s?R>*YVR~Z|^rSANSbaV`c9QtHinAb6Z zSwW;B3iuco&Q4r(ZN6122b}hd3VAFZfBO{eH_Z*NsSztVA@qcn_JuwE_1Mo>skC9_ zh$bUafg^omp<=Tk^`s7NWma}=D0E2|<3fPR9S<-(FUg?bLw%!SR8u>Z`02F%+rXQ7 zq8j9L-ox?mC7Gt@;r(Dk5$-C&o80pN!!3CsOHZk&&Kg^Wy#Gmv3pa|#$Y1Mj?7;kV z$5h`S2CCCA&7VGm)tXL#?1e&ybe(ANq4uR+G1AW#5msbZNuY42Wf2j|&_pTU zgM!Lf^V|wRlfyyC+8A!AmGqQE(Lrr9vE$;@Twc%^kHxwA%eY`$r%faR_v^(|tp9bn z=Z1oDRPq1dU)PMm%l-GB+PhErS3Iy_!S8fzguWfC{~)39TtIEd@cpwaJ>vq_2HIei ziBgBbdg?XI3ctZ;W9o>*v~J-jJJoCv!^`dCtcv=-l6rFku);?+lCnM z#1RpqW%(6@CsLKTS}<#@3n#6RcR%T>@hz8mp~Yzb`Mki2M$N%;W2e@;TX<9i$Y{2U zIc=sS)|9iszgS4A7nyw{a{Ozlt$*G8F6LIY@S&>~GX6u+*7RiFDP%}E) zssg_02|?4XFNL0m%U*Ed+redRho{~eFXwU!A`W0;p=(GV@&b@Z$8X*PqqNGP^{+Y} z&dJrgB)Ng`*P;1N0h~sQk`;MfX=i?{j|8fom>xd&AlS-W3^p87P4TAnJk0mk)g?uH9=bvso(%?7BOglmzJC&6mpEYDUXxn7!+h%Z7pSftUzB+%b z*Am5KXTuv-aM8H&;t-M7WbjVJp-XVznk=IU&?s0UocUi>-^BzuKPPl=+w}LXBImQt zolgj=b=Xqz`~#L(zg_PK9&5gc@@b1hUK%o&ct%524J~7Uc%SXg`8ABE-TP&4#CpcZ z=_-x4~eG?ss1i7 zx*474e~Z!J%;6Inz$iP%LT94gUWT>(_S*_3K^@$BoTGpJC}eqp$AG5;e#RQ~Kmt#4I_m^Z@gOKD25+dInAm|M#EJPMDx{;~ptfIE zb8nG&p}T8tpV!*bd#2%hx=C4~XTyNav-_;nWtj36RhbOdr~auNeUaKh z+|-@C)aZ3k?gi8`bAefEFVI{_yV;5F`6qfN_4KN?YvC-THo3Dv+d17t48nN98* z(~E7aw<$3m4;+u{U&?5p86vDT^mPgfoZewZ2b2x}NP!EFf8D4VJ*d?sc4%cKR*BkL zN^YgjPA6MgOm2XGO%C=w5)lfkgyfk{Q2Sno24bAxt>ZSa9mI}1(U0bK1TANe3iQOi z&!bIa*3IcX9(2#uxC4+}XaeD6A6o`&C#|W`*Z17TgLn}@ph@JI?`jD}s?_mCeWHkV zhG?haa(U!OR$HkZZGF8tIBUV{wZ zow}8XOLF;PtuFAw^U&W%*M+R@>yRq{E=rJJEVgS+EjKEc-%s0GR&!Rtx3NYaG$-EP zl`~ws!$+&j|L8>h9aqOZA zf3-*d?~LVveuV#R8|h8$XvFQ84pC``9iP&TjNktz@AO=LR6n1z zi=6Z&RW|xmS;XQmEMMIJT23ZLe#28O*JzC9@O@61+t@r+|U;%ZM|x+{wO zFD!&1b~~E^j?MjLTkqeBO(#YvCD$7b&hMSxZf_9`?a9ufz~wTcRkHnkSZKEeoseoF zA=u%S1}#CTDm&TfbzE-kb?)yk^?yIECl#)P+=~=H-lN?Ei7ck&i7&k2sv;CQuUHM; zDoRm}fb@=_&)L8JB=-R~zIz(Ho6*!r`KLo-t|maUnpL3di3pP@Gwd=Sjo{D!*u(AV zV8PEjVwphA_uGf+#&778f~;+;;V$^xRM@0w+WqrzwLfuRVoTUdY3#cK!+# zK=Qz$vgoFBJ;eoN=wN|#YbO$wmGx{e#lQggVVudh5ag7J`k$P-(zl$AH(gKC`nj^* zHGhbT?){ao@-L->UQCKgP@saQQAmE$7B;+Gy5-m(!~gS+&^AvM=jZ?uGFDnQ&!8{A zeSZ8aSzEGXh9wepZP)9^xto+kdef9URbsr(?fe5#?S zaWkot;0Dw0nOR*o)yu_)DUUq^RVb{*m|(t)FlaOK2m}p}sAs$mg`add_;5Hs zxsMo<-=w22Kn8+DQwZ>=7NPBFR)zwCv@D(GO{gjL>)3zgtxn#Wo-<4NfY4_0SLS>+ zW-ka59jZ2hUx`e&KY@f{Ji`omGg3h)WAmYq|9~dk`m$^Fl=Qc1>G;%#6*Xe@Ohnj) zv7O4)v}`r=fob!j%2XEd(+>Q?kGN-Qp5@CNW zO3qC|16ETkmdC06A4g~5&{W@sVJTrWOhQsg5hSF$!Ju0@1nGuJ!$3kxq(_6&43O^b z5QNc<)F_EjBfj%~|H0yH=lq`MzOM`A?PbcRLPpL2@>VFkp`&7!^+|)Q{Qjuz(?afi zw*wrUGPh^LjD)+QX*}SG2i9Fe>_RH1wO;Si3cu25ZaoJ4r>%c5%IkuN&4c1YpMMS5 zttS8KV`Zi4{*vEtLbXjNau+2G4lj{U)fJFpau4yoCZ)r6JxJzNP4P2S9C0HOx2?fX z!~=u6d;!AXjb`c}vwuml4|)C1KCww4zIGgcq!YOUy$N?isvS>ZL&6!PV@iF%DS!ux z1sr;YO(iN$&@aWuiWjJC1V9-&gYM5d7?JPJDydY;W?X#OZZAr1^KFKdMs@z7)BQCE zs4CmDp!b~U!xrBk%!r{7Rbl&uud`IQ-sKO4Z06oSj%Q9i9g|V(0^<%)*ns4ArZ_)9RL2|UwRTob~{ddE zELLn5jmPfV9lou(glBrD!lH<*1`X{<=o^Os^mLO@3-& zt+iLei#l(mqIz(d-c-3-7F^Hpo%$Q%aNM|Wt>kwwHY{y>(XjNM!yP)mq_1M0_n$Gj z8B%8`R}~v17F@-Lz^H z5@q4#e*hfxV(wfUnc3UfYa=SxHy3`*si#fdrO>OVCG$4>;rcyw!+n57HSS18^=E-P z#FYZH!S^Z3&;I=j>_DL3?9iI0A($E3w3D}F_&p5E&3G~FK^=aX0lHQq*U(emmUcuy zN$Pj=rkw!y;n+)sV8`>!o2@!r$4mV<%eoYtE@6@pBqB_Um?G?*+WZ3@48mu$9`%V4|Rl9-v!l0vI@cR_8Kox5QMN z+5|mn>x+T0(3#aD;8%HFhN|L ze?7L|c!JASWA&rnQ z*#d^7s+q8pstX^#8KUs4?1?3bRT~w yX18jVpECnN=-KguNz4!qf2pPF8wF#L8 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
(e3(<&@5p*VFUDm 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

n`eU+cc*Fg93-?Az>%nQoZg~%4im{Oh)wTs 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 z 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

r@BAiD# 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

-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 literal 0 HcmV?d00001 From cf25ad9030c0ea0c742b7e21592dc668e6c90734 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Mon, 21 Oct 2024 19:03:11 -0400 Subject: [PATCH 09/13] draft callpeakunit.py --- MACS3/Signal/CallPeakUnit.py | 2242 ++++++++++++++++++++++++++++++++++ 1 file changed, 2242 insertions(+) create mode 100644 MACS3/Signal/CallPeakUnit.py diff --git a/MACS3/Signal/CallPeakUnit.py b/MACS3/Signal/CallPeakUnit.py new file mode 100644 index 00000000..166ae34e --- /dev/null +++ b/MACS3/Signal/CallPeakUnit.py @@ -0,0 +1,2242 @@ +# cython: language_level=3 +# cython: profile=True +# cython: linetrace=True +# Time-stamp: <2024-10-21 17:49:36 Tao Liu> + +"""Module for Calculate Scores. + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file LICENSE included with +the distribution). +""" + +# ------------------------------------ +# python modules +# ------------------------------------ + +import _pickle as cPickle +from tempfile import mkstemp +import os + +# ------------------------------------ +# Other modules +# ------------------------------------ +import numpy as np +import cython +import cython.cimports.numpy as cnp +# from numpy cimport int32_t, int64_t, float32_t, float64_t +from cython.cimports.cpython import bool +from cykhash import PyObjectMap, Float32to32Map + +# ------------------------------------ +# C lib +# ------------------------------------ +from cython.cimports.libc.stdio import FILE, fopen, fprintf, fclose +from cython.cimports.libc.math import exp, log10, log1p, erf, sqrt + +# ------------------------------------ +# MACS3 modules +# ------------------------------------ +from MACS3.Signal.SignalProcessing import maxima, enforce_peakyness +from MACS3.IO.PeakIO import PeakIO, BroadPeakIO +from MACS3.Signal.FixWidthTrack import FWTrack +from MACS3.Signal.PairedEndTrack import PETrackI +from MACS3.Signal.Prob import poisson_cdf +from MACS3.Utilities.Logger import logging + +logger = logging.getLogger(__name__) +debug = logger.debug +info = logger.info +# -------------------------------------------- +# cached pscore function and LR_asym functions +# -------------------------------------------- +pscore_dict = PyObjectMap() +logLR_dict = PyObjectMap() + + +@cython.cfunc +def get_pscore(t: tuple) -> cython.float: + """t: tuple of (lambda, observation) + """ + val: cython.float + + if t in pscore_dict: + return pscore_dict[t] + else: + # calculate and cache + val = -1.0 * poisson_cdf(t[0], t[1], False, True) + pscore_dict[t] = val + return val + + +@cython.cfunc +def get_logLR_asym(t: tuple) -> cython.float: + """Calculate log10 Likelihood between H1 (enriched) and H0 ( + chromatin bias). Set minus sign for depletion. + """ + val: cython.float + x: cython.float + y: cython.float + + if t in logLR_dict: + return logLR_dict[t] + else: + x = t[0] + y = t[1] + # calculate and cache + if x > y: + val = (x*(log10(x)-log10(y))+y-x) + elif x < y: + val = (x*(-log10(x)+log10(y))-y+x) + else: + val = 0 + logLR_dict[t] = val + return val + +# ------------------------------------ +# constants +# ------------------------------------ + + +LOG10_E: cython.float = 0.43429448190325176 + +# ------------------------------------ +# Misc functions +# ------------------------------------ + + +@cython.cfunc +def clean_up_ndarray(x: cnp.ndarray): + # clean numpy ndarray in two steps + i: cython.long + + i = x.shape[0] // 2 + x.resize(100000 if i > 100000 else i, refcheck=False) + x.resize(0, refcheck=False) + return + + +@cython.cfunc +@cython.inline +def chi2_k1_cdf(x: cython.float) -> cython.float: + return erf(sqrt(x/2)) + + +@cython.cfunc +@cython.inline +def log10_chi2_k1_cdf(x: cython.float) -> cython.float: + return log10(erf(sqrt(x/2))) + + +@cython.cfunc +@cython.inline +def chi2_k2_cdf(x: cython.float) -> cython.float: + return 1 - exp(-x/2) + + +@cython.cfunc +@cython.inline +def log10_chi2_k2_cdf(x: cython.float) -> cython.float: + return log1p(- exp(-x/2)) * LOG10_E + + +@cython.cfunc +@cython.inline +def chi2_k4_cdf(x: cython.float) -> cython.float: + return 1 - exp(-x/2) * (1 + x/2) + + +@cython.cfunc +@cython.inline +def log10_chi2_k4_CDF(x: cython.float) -> cython.float: + return log1p(- exp(-x/2) * (1 + x/2)) * LOG10_E + + +@cython.cfunc +@cython.inline +def apply_multiple_cutoffs(multiple_score_arrays: list, + multiple_cutoffs: list) -> cnp.ndarray: + i: cython.int + ret: cnp.ndarray + + ret = multiple_score_arrays[0] > multiple_cutoffs[0] + + for i in range(1, len(multiple_score_arrays)): + ret += multiple_score_arrays[i] > multiple_cutoffs[i] + + return ret + + +@cython.cfunc +@cython.inline +def get_from_multiple_scores(multiple_score_arrays: list, + index: cython.int) -> list: + ret: list = [] + i: cython.int + + for i in range(len(multiple_score_arrays)): + ret.append(multiple_score_arrays[i][index]) + return ret + + +@cython.cfunc +@cython.inline +def get_logFE(x: cython.float, + y: cython.float) -> cython.float: + """ return 100* log10 fold enrichment with +1 pseudocount. + """ + return log10(x/y) + + +@cython.cfunc +@cython.inline +def get_subtraction(x: cython.float, + y: cython.float) -> cython.float: + """ return subtraction. + """ + return x - y + + +@cython.cfunc +@cython.inline +def getitem_then_subtract(peakset: list, + start: cython.int) -> list: + a: list + + a = [x["start"] for x in peakset] + for i in range(len(a)): + a[i] = a[i] - start + return a + + +@cython.cfunc +@cython.inline +def left_sum(data, pos: cython.int, + width: cython.int) -> cython.int: + """ + """ + return sum([data[x] for x in data if x <= pos and x >= pos - width]) + + +@cython.cfunc +@cython.inline +def right_sum(data, + pos: cython.int, + width: cython.int) -> cython.int: + """ + """ + return sum([data[x] for x in data if x >= pos and x <= pos + width]) + + +@cython.cfunc +@cython.inline +def left_forward(data, + pos: cython.int, + window_size: cython.int) -> cython.int: + return data.get(pos, 0) - data.get(pos-window_size, 0) + + +@cython.cfunc +@cython.inline +def right_forward(data, + pos: cython.int, + window_size: cython.int) -> cython.int: + return data.get(pos + window_size, 0) - data.get(pos, 0) + + +@cython.cfunc +def median_from_value_length(value: cnp.ndarray(cython.float, ndim=1), + length: list) -> cython.float: + """ + """ + tmp: list + c: cython.int + tmp_l: cython.int + tmp_v: cython.float + mid_l: cython.float + + c = 0 + tmp = sorted(list(zip(value, length))) + mid_l = sum(length)/2 + for (tmp_v, tmp_l) in tmp: + c += tmp_l + if c > mid_l: + return tmp_v + + +@cython.cfunc +def mean_from_value_length(value: cnp.ndarray(cython.float, ndim=1), + length: list) -> cython.float: + """take of: list values and of: list corresponding lengths, + calculate the mean. An important function for bedGraph type of + data. + + """ + i: cython.int + tmp_l: cython.int + ln: cython.int + tmp_v: cython.double + sum_v: cython.double + tmp_sum: cython.double + ret: cython.float + + sum_v = 0 + ln = 0 + + for i in range(len(length)): + tmp_l = length[i] + tmp_v = cython.cast(cython.double, value[i]) + tmp_sum = tmp_v * tmp_l + sum_v = tmp_sum + sum_v + ln += tmp_l + + ret = cython.cast(cython.float, (sum_v/ln)) + + return ret + + +@cython.cfunc +def find_optimal_cutoff(x: list, y: list) -> tuple: + """Return the best cutoff x and y. + + We assume that total peak length increase exponentially while + decreasing cutoff value. But while cutoff decreases to a point + that background noises are captured, total length increases much + faster. So we fit a linear model by taking the first 10 points, + then look for the largest cutoff that + + + *Currently, it is coded as a useless function. + """ + npx: cnp.ndarray + npy: cnp.ndarray + npA: cnp.ndarray + ln: cython.long + i: cython.long + m: cython.float + c: cython.float # slop and intercept + sst: cython.float # sum of squared total + sse: cython.float # sum of squared error + rsq: cython.float # R-squared + + ln = len(x) + assert ln == len(y) + npx = np.array(x) + npy = np.log10(np.array(y)) + npA = np.vstack([npx, np.ones(len(npx))]).T + + for i in range(10, ln): + # at least the largest 10 points + m, c = np.linalg.lstsq(npA[:i], npy[:i], rcond=None)[0] + sst = sum((npy[:i] - np.mean(npy[:i])) ** 2) + sse = sum((npy[:i] - m*npx[:i] - c) ** 2) + rsq = 1 - sse/sst + # print i, x[i], y[i], m, c, rsq + return (1.0, 1.0) + + +# ------------------------------------ +# Classes +# ------------------------------------ +@cython.cclass +class CallerFromAlignments: + """A unit to calculate scores and call peaks from alignments -- + FWTrack or PETrack objects. + + It will compute for each chromosome separately in order to save + memory usage. + """ + treat: object # FWTrack or PETrackI object for ChIP + ctrl: object # FWTrack or PETrackI object for Control + + d: cython.int # extension size for ChIP + # extension sizes for Control. Can be multiple values + ctrl_d_s: list + treat_scaling_factor: cython.float # scaling factor for ChIP + # scaling factor for Control, corresponding to each extension size. + ctrl_scaling_factor_s: list + # minimum local bias to fill missing values + lambda_bg: cython.float + # name of common chromosomes in ChIP and Control data + chromosomes: list + # the pseudocount used to calcuate logLR, FE or logFE + pseudocount: cython.double + # prefix will be added to _pileup.bdg for treatment and + # _lambda.bdg for control + bedGraph_filename_prefix: bytes + # shift of cutting ends before extension + end_shift: cython.int + # whether trackline should be saved in bedGraph + trackline: bool + # whether to save pileup and local bias in bedGraph files + save_bedGraph: bool + # whether to save pileup normalized by sequencing depth in million reads + save_SPMR: bool + # whether ignore local bias, and to use global bias instead + no_lambda_flag: bool + # whether it's in PE mode, will be detected during initiation + PE_mode: bool + + # temporary data buffer + # temporary [position, treat_pileup, ctrl_pileup] for a given chromosome + chr_pos_treat_ctrl: list + bedGraph_treat_filename: bytes + bedGraph_control_filename: bytes + bedGraph_treat_f: cython.pointer(FILE) + bedGraph_ctrl_f: cython.pointer(FILE) + + # data needed to be pre-computed before peak calling + # remember pvalue->qvalue convertion; saved in cykhash Float32to32Map + pqtable: Float32to32Map + # whether the pvalue of whole genome is all calculated. If yes, it's OK to calculate q-value. + pvalue_all_done: bool + # record for each pvalue cutoff, how many peaks can be called + pvalue_npeaks: dict + # record for each pvalue cutoff, the total length of called peaks + pvalue_length: dict + # automatically decide the p-value cutoff (can be translated into + # qvalue cutoff) based on p-value to total peak length analysis. + optimal_p_cutoff: cython.float + # file to save the pvalue-npeaks-totallength table + cutoff_analysis_filename: bytes + # Record the names of temporary files for storing pileup values of each chromosome + pileup_data_files: dict + + def __init__(self, + treat, + ctrl, + d: cython.int = 200, + ctrl_d_s: list = [200, 1000, 10000], + treat_scaling_factor: cython.float = 1.0, + ctrl_scaling_factor_s: list = [1.0, 0.2, 0.02], + stderr_on: bool = False, + pseudocount: cython.float = 1, + end_shift: cython.int = 0, + lambda_bg: cython.float = 0, + save_bedGraph: bool = False, + bedGraph_filename_prefix: str = "PREFIX", + bedGraph_treat_filename: str = "TREAT.bdg", + bedGraph_control_filename: str = "CTRL.bdg", + cutoff_analysis_filename: str = "TMP.txt", + save_SPMR: bool = False): + """Initialize. + + A calculator is unique to each comparison of treat and + control. Treat_depth and ctrl_depth should not be changed + during calculation. + + treat and ctrl are either FWTrack or PETrackI objects. + + treat_depth and ctrl_depth are effective depth in million: + sequencing depth in million after + duplicates being filtered. If + treatment is scaled down to + control sample size, then this + should be control sample size in + million. And vice versa. + + d, sregion, lregion: d is the fragment size, sregion is the + small region size, lregion is the large + region size + + pseudocount: a pseudocount used to calculate logLR, FE or + logFE. Please note this value will not be changed + with normalization method. So if you really want + to pseudocount: set 1 per million reads, it: set + after you normalize treat and control by million + reads by `change_normalizetion_method(ord('M'))`. + + """ + chr1: set + chr2: set + p: cython.float + + # decide PE mode + if isinstance(treat, FWTrack): + self.PE_mode = False + elif isinstance(treat, PETrackI): + self.PE_mode = True + else: + raise Exception("Should be FWTrack or PETrackI object!") + # decide if there is control + self.treat = treat + if ctrl: + self.ctrl = ctrl + else: # while there is no control + self.ctrl = treat + self.trackline = False + self.d = d # note, self.d doesn't make sense in PE mode + self.ctrl_d_s = ctrl_d_s # note, self.d doesn't make sense in PE mode + self.treat_scaling_factor = treat_scaling_factor + self.ctrl_scaling_factor_s = ctrl_scaling_factor_s + self.end_shift = end_shift + self.lambda_bg = lambda_bg + self.pqtable = Float32to32Map(for_int=False) # Float32 -> Float32 map + self.save_bedGraph = save_bedGraph + self.save_SPMR = save_SPMR + self.bedGraph_filename_prefix = bedGraph_filename_prefix.encode() + self.bedGraph_treat_filename = bedGraph_treat_filename.encode() + self.bedGraph_control_filename = bedGraph_control_filename.encode() + if not self.ctrl_d_s or not self.ctrl_scaling_factor_s: + self.no_lambda_flag = True + else: + self.no_lambda_flag = False + self.pseudocount = pseudocount + # get the common chromosome names from both treatment and control + chr1 = set(self.treat.get_chr_names()) + chr2 = set(self.ctrl.get_chr_names()) + self.chromosomes = sorted(list(chr1.intersection(chr2))) + + self.pileup_data_files = {} + self.pvalue_length = {} + self.pvalue_npeaks = {} + for p in np.arange(0.3, 10, 0.3): # step for optimal cutoff is 0.3 in -log10pvalue, we try from pvalue 1E-10 (-10logp=10) to 0.5 (-10logp=0.3) + self.pvalue_length[p] = 0 + self.pvalue_npeaks[p] = 0 + self.optimal_p_cutoff = 0 + self.cutoff_analysis_filename = cutoff_analysis_filename.encode() + + @cython.ccall + def destroy(self): + """Remove temporary files for pileup values of each chromosome. + + Note: This function MUST be called if the class won: object't + be used anymore. + + """ + f: bytes + + for f in self.pileup_data_files.values(): + if os.path.isfile(f): + os.unlink(f) + return + + @cython.ccall + def set_pseudocount(self, pseudocount: cython.float): + self.pseudocount = pseudocount + + @cython.ccall + def enable_trackline(self): + """Turn on trackline with bedgraph output + """ + self.trackline = True + + @cython.cfunc + def pileup_treat_ctrl_a_chromosome(self, chrom: bytes): + """After this function is called, self.chr_pos_treat_ctrl will + be reand: set assigned to the pileup values of the given + chromosome. + + """ + treat_pv: list + ctrl_pv: list + f: object + temp_filename: str + + assert chrom in self.chromosomes, "chromosome %s is not valid." % chrom + + # check backup file of pileup values. If not exists, create + # it. Otherwise, load them instead of calculating new pileup + # values. + if chrom in self.pileup_data_files: + try: + f = open(self.pileup_data_files[chrom], "rb") + self.chr_pos_treat_ctrl = cPickle.load(f) + f.close() + return + except Exception: + temp_fd, temp_filename = mkstemp() + os.close(temp_fd) + self.pileup_data_files[chrom] = temp_filename + else: + temp_fd, temp_filename = mkstemp() + os.close(temp_fd) + self.pileup_data_files[chrom] = temp_filename.encode() + + # reor: set clean existing self.chr_pos_treat_ctrl + if self.chr_pos_treat_ctrl: # not a beautiful way to clean + clean_up_ndarray(self.chr_pos_treat_ctrl[0]) + clean_up_ndarray(self.chr_pos_treat_ctrl[1]) + clean_up_ndarray(self.chr_pos_treat_ctrl[2]) + + if self.PE_mode: + treat_pv = self.treat.pileup_a_chromosome(chrom, + [self.treat_scaling_factor,], + baseline_value=0.0) + else: + treat_pv = self.treat.pileup_a_chromosome(chrom, + [self.d,], + [self.treat_scaling_factor,], + baseline_value=0.0, + directional=True, + end_shift=self.end_shift) + + if not self.no_lambda_flag: + if self.PE_mode: + # note, we pileup up PE control as SE control because + # we assume the bias only can be captured at the + # surrounding regions of cutting sites from control experiments. + ctrl_pv = self.ctrl.pileup_a_chromosome_c(chrom, + self.ctrl_d_s, + self.ctrl_scaling_factor_s, + baseline_value=self.lambda_bg) + else: + ctrl_pv = self.ctrl.pileup_a_chromosome(chrom, + self.ctrl_d_s, + self.ctrl_scaling_factor_s, + baseline_value=self.lambda_bg, + directional=False) + else: + ctrl_pv = [treat_pv[0][-1:], np.array([self.lambda_bg,], + dtype="f4")] # a: set global lambda + + self.chr_pos_treat_ctrl = self.__chrom_pair_treat_ctrl(treat_pv, ctrl_pv) + + # clean treat_pv and ctrl_pv + treat_pv = [] + ctrl_pv = [] + + # save data to temporary file + try: + f = open(self.pileup_data_files[chrom], "wb") + cPickle.dump(self.chr_pos_treat_ctrl, f, protocol=2) + f.close() + except Exception: + # fail to write then remove the key in pileup_data_files + self.pileup_data_files.pop(chrom) + return + + @cython.cfunc + def __chrom_pair_treat_ctrl(self, treat_pv, ctrl_pv) -> list: + """*private* Pair treat and ctrl pileup for each region. + + treat_pv and ctrl_pv are [np.ndarray, np.ndarray]. + + return [p, t, c] list, each element is a numpy array. + """ + index_ret: cython.long + it: cython.long + ic: cython.long + lt: cython.long + lc: cython.long + t_p: cnp.ndarray(cython.int, ndim=1) + c_p: cnp.ndarray(cython.int, ndim=1) + ret_p: cnp.ndarray(cython.int, ndim=1) + t_v: cnp.ndarray(cython.float, ndim=1) + c_v: cnp.ndarray(cython.float, ndim=1) + ret_t: cnp.ndarray(cython.float, ndim=1) + ret_c: cnp.ndarray(cython.float, ndim=1) + t_p_ptr: cython.pointer[cython.int] + c_p_ptr: cython.pointer[cython.int] + ret_p_ptr: cython.pointer[cython.int] + t_v_ptr: cython.pointer[cython.float] + c_v_ptr: cython.pointer[cython.float] + ret_t_ptr: cython.pointer[cython.float] + ret_c_ptr: cython.pointer[cython.float] + + [t_p, t_v] = treat_pv + [c_p, c_v] = ctrl_pv + + lt = t_p.shape[0] + lc = c_p.shape[0] + + chrom_max_len = lt + lc + + ret_p = np.zeros(chrom_max_len, dtype="i4") # position + ret_t = np.zeros(chrom_max_len, dtype="f4") # value from treatment + ret_c = np.zeros(chrom_max_len, dtype="f4") # value from control + + t_p_ptr = cython.cast(cython.pointer[cython.int], t_p.data) + t_v_ptr = cython.cast(cython.pointer[cython.float], t_v.data) + c_p_ptr = cython.cast(cython.pointer[cython.int], c_p.data) + c_v_ptr = cython.cast(cython.pointer[cython.float], c_v.data) + ret_p_ptr = cython.cast(cython.pointer[cython.int], ret_p.data) + ret_t_ptr = cython.cast(cython.pointer[cython.float], ret_t.data) + ret_c_ptr = cython.cast(cython.pointer[cython.float], ret_c.data) + + index_ret = 0 + it = 0 + ic = 0 + + while it < lt and ic < lc: + if t_p_ptr[0] < c_p_ptr[0]: + # clip a region from pre_p to p1, then pre_p: set as p1. + ret_p_ptr[0] = t_p_ptr[0] + ret_t_ptr[0] = t_v_ptr[0] + ret_c_ptr[0] = c_v_ptr[0] + ret_p_ptr += 1 + ret_t_ptr += 1 + ret_c_ptr += 1 + index_ret += 1 + # call for the next p1 and v1 + it += 1 + t_p_ptr += 1 + t_v_ptr += 1 + elif t_p_ptr[0] > c_p_ptr[0]: + # clip a region from pre_p to p2, then pre_p: set as p2. + ret_p_ptr[0] = c_p_ptr[0] + ret_t_ptr[0] = t_v_ptr[0] + ret_c_ptr[0] = c_v_ptr[0] + ret_p_ptr += 1 + ret_t_ptr += 1 + ret_c_ptr += 1 + index_ret += 1 + # call for the next p2 and v2 + ic += 1 + c_p_ptr += 1 + c_v_ptr += 1 + else: + # from pre_p to p1 or p2, then pre_p: set as p1 or p2. + ret_p_ptr[0] = t_p_ptr[0] + ret_t_ptr[0] = t_v_ptr[0] + ret_c_ptr[0] = c_v_ptr[0] + ret_p_ptr += 1 + ret_t_ptr += 1 + ret_c_ptr += 1 + index_ret += 1 + # call for the next p1, v1, p2, v2. + it += 1 + ic += 1 + t_p_ptr += 1 + t_v_ptr += 1 + c_p_ptr += 1 + c_v_ptr += 1 + + ret_p.resize(index_ret, refcheck=False) + ret_t.resize(index_ret, refcheck=False) + ret_c.resize(index_ret, refcheck=False) + return [ret_p, ret_t, ret_c] + + @cython.cfunc + def __cal_score(self, + array1: cnp.ndarray(cython.float, ndim=1), + array2: cnp.ndarray(cython.float, ndim=1), + cal_func) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray(cython.float, ndim=1) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + for i in range(array1.shape[0]): + s[i] = cal_func(array1[i], array2[i]) + return s + + @cython.cfunc + def __cal_pvalue_qvalue_table(self): + """After this function is called, self.pqtable is built. All + chromosomes will be iterated. So it will take some time. + + """ + chrom: bytes + pos_array: cnp.ndarray + treat_array: cnp.ndarray + ctrl_array: cnp.ndarray + pscore_stat: dict + pre_p: cython.long + # pre_l: cython.long + l: cython.long + i: cython.long + j: cython.long + this_v: cython.float + # pre_v: cython.float + v: cython.float + q: cython.float + pre_q: cython.float + N: cython.long + k: cython.long + this_l: cython.long + f: cython.float + unique_values: list + pos_ptr: cython.pointer[cython.int] + treat_value_ptr: cython.pointer[cython.float] + ctrl_value_ptr: cython.pointer[cython.float] + + debug("Start to calculate pvalue stat...") + + pscore_stat = {} # dict() + for i in range(len(self.chromosomes)): + chrom = self.chromosomes[i] + pre_p = 0 + + self.pileup_treat_ctrl_a_chromosome(chrom) + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + + pos_ptr = cython.cast(cython.pointer(cython.int), + pos_array.data) + treat_value_ptr = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_value_ptr = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + for j in range(pos_array.shape[0]): + this_v = get_pscore((cython.cast(cython.int, + treat_value_ptr[0]), + ctrl_value_ptr[0])) + this_l = pos_ptr[0] - pre_p + if this_v in pscore_stat: + pscore_stat[this_v] += this_l + else: + pscore_stat[this_v] = this_l + pre_p = pos_ptr[0] + pos_ptr += 1 + treat_value_ptr += 1 + ctrl_value_ptr += 1 + + N = sum(pscore_stat.values()) # total length + k = 1 # rank + f = -log10(N) + # pre_v = -2147483647 + # pre_l = 0 + pre_q = 2147483647 # save the previous q-value + + self.pqtable = Float32to32Map(for_int=False) + unique_values = sorted(list(pscore_stat.keys()), reverse=True) + for i in range(len(unique_values)): + v = unique_values[i] + l = pscore_stat[v] + q = v + (log10(k) + f) + if q > pre_q: + q = pre_q + if q <= 0: + q = 0 + break + #q = max(0,min(pre_q,q)) # make q-score monotonic + self.pqtable[v] = q + pre_q = q + k += l + # bottom rank pscores all have qscores 0 + for j in range(i, len(unique_values)): + v = unique_values[j] + self.pqtable[v] = 0 + return + + @cython.cfunc + def __pre_computes(self, + max_gap: cython.int = 50, + min_length: cython.int = 200): + """After this function is called, self.pqtable and self.pvalue_length is built. All + chromosomes will be iterated. So it will take some time. + + """ + chrom: bytes + pos_array: cnp.ndarray + treat_array: cnp.ndarray + ctrl_array: cnp.ndarray + score_array: cnp.ndarray + pscore_stat: dict + n: cython.long + pre_p: cython.long + this_p: cython.long + j: cython.long + l: cython.long + i: cython.long + q: cython.float + pre_q: cython.float + this_v: cython.float + v: cython.float + cutoff: cython.float + N: cython.long + k: cython.long + this_l: cython.long + f: cython.float + unique_values: list + above_cutoff: cnp.ndarray + above_cutoff_endpos: cnp.ndarray + above_cutoff_startpos: cnp.ndarray + peak_content: list + peak_length: cython.long + total_l: cython.long + total_p: cython.long + tmplist: list + + # above cutoff start position pointer + acs_ptr: cython.pointer(cython.int) + # above cutoff end position pointer + ace_ptr: cython.pointer(cython.int) + # position array pointer + pos_array_ptr: cython.pointer(cython.int) + # score array pointer + score_array_ptr: cython.pointer(cython.float) + + debug("Start to calculate pvalue stat...") + + # tmpcontains: list a of: list log pvalue cutoffs from 0.3 to 10 + tmplist = [round(x, 5) + for x in sorted(list(np.arange(0.3, 10.0, 0.3)), + reverse=True)] + + pscore_stat = {} # dict() + # print (list(pscore_stat.keys())) + # print (list(self.pvalue_length.keys())) + # print (list(self.pvalue_npeaks.keys())) + for i in range(len(self.chromosomes)): + chrom = self.chromosomes[i] + self.pileup_treat_ctrl_a_chromosome(chrom) + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + + score_array = self.__cal_pscore(treat_array, ctrl_array) + + for n in range(len(tmplist)): + cutoff = tmplist[n] + total_l = 0 # total length in potential peak + total_p = 0 + + # get the regions with scores above cutoffs this is + # not an optimized method. It would be better to store + # score array in a 2-D ndarray? + above_cutoff = np.nonzero(score_array > cutoff)[0] + # end positions of regions where score is above cutoff + above_cutoff_endpos = pos_array[above_cutoff] + # start positions of regions where score is above cutoff + above_cutoff_startpos = pos_array[above_cutoff-1] + + if above_cutoff_endpos.size == 0: + continue + + # first bit of region above cutoff + acs_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_startpos.data) + ace_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_endpos.data) + + peak_content = [(acs_ptr[0], ace_ptr[0]),] + lastp = ace_ptr[0] + acs_ptr += 1 + ace_ptr += 1 + + for i in range(1, above_cutoff_startpos.size): + tl = acs_ptr[0] - lastp + if tl <= max_gap: + peak_content.append((acs_ptr[0], ace_ptr[0])) + else: + peak_length = peak_content[-1][1] - peak_content[0][0] + # if the peak is too small, reject it + if peak_length >= min_length: + total_l += peak_length + total_p += 1 + peak_content = [(acs_ptr[0], ace_ptr[0]),] + lastp = ace_ptr[0] + acs_ptr += 1 + ace_ptr += 1 + + if peak_content: + peak_length = peak_content[-1][1] - peak_content[0][0] + # if the peak is too small, reject it + if peak_length >= min_length: + total_l += peak_length + total_p += 1 + self.pvalue_length[cutoff] = self.pvalue_length.get(cutoff, 0) + total_l + self.pvalue_npeaks[cutoff] = self.pvalue_npeaks.get(cutoff, 0) + total_p + + pos_array_ptr = cython.cast(cython.pointer(cython.int), + pos_array.data) + score_array_ptr = cython.cast(cython.pointer(cython.float), + score_array.data) + + pre_p = 0 + for i in range(pos_array.shape[0]): + this_p = pos_array_ptr[0] + this_l = this_p - pre_p + this_v = score_array_ptr[0] + if this_v in pscore_stat: + pscore_stat[this_v] += this_l + else: + pscore_stat[this_v] = this_l + pre_p = this_p # pos_array[i] + pos_array_ptr += 1 + score_array_ptr += 1 + + # debug ("make pscore_stat cost %.5f seconds" % t) + + # add all pvalue cutoffs from cutoff-analysis part. So that we + # can get the corresponding qvalues for them. + for cutoff in tmplist: + if cutoff not in pscore_stat: + pscore_stat[cutoff] = 0 + + N = sum(pscore_stat.values()) # total length + k = 1 # rank + f = -log10(N) + pre_q = 2147483647 # save the previous q-value + + self.pqtable = Float32to32Map(for_int=False) # {} + # sorted(unique_values,reverse=True) + unique_values = sorted(list(pscore_stat.keys()), reverse=True) + for i in range(len(unique_values)): + v = unique_values[i] + l = pscore_stat[v] + q = v + (log10(k) + f) + if q > pre_q: + q = pre_q + if q <= 0: + q = 0 + break + # q = max(0,min(pre_q,q)) # make q-score monotonic + self.pqtable[v] = q + pre_q = q + k += l + for j in range(i, len(unique_values)): + v = unique_values[j] + self.pqtable[v] = 0 + + # write pvalue and total length of predicted peaks + # this is the output from cutoff-analysis + fhd = open(self.cutoff_analysis_filename, "w") + fhd.write("pscore\tqscore\tnpeaks\tlpeaks\tavelpeak\n") + x = [] + y = [] + for cutoff in tmplist: + if self.pvalue_npeaks[cutoff] > 0: + fhd.write("%.2f\t%.2f\t%d\t%d\t%.2f\n" % + (cutoff, self.pqtable[cutoff], + self.pvalue_npeaks[cutoff], + self.pvalue_length[cutoff], + self.pvalue_length[cutoff]/self.pvalue_npeaks[cutoff])) + x.append(cutoff) + y.append(self.pvalue_length[cutoff]) + fhd.close() + info("#3 Analysis of cutoff vs num of peaks or total length has been saved in %s" % self.cutoff_analysis_filename) + # info("#3 Suggest a cutoff...") + # optimal_cutoff, optimal_length = find_optimal_cutoff(x, y) + # info("#3 -10log10pvalue cutoff %.2f will call approximately %.0f bps regions as significant regions" % (optimal_cutoff, optimal_length)) + # print (list(pqtable.keys())) + # print (list(self.pvalue_length.keys())) + # print (list(self.pvalue_npeaks.keys())) + return + + @cython.ccall + def call_peaks(self, + scoring_function_symbols: list, + score_cutoff_s: list, + min_length: cython.int = 200, + max_gap: cython.int = 50, + call_summits: bool = False, + cutoff_analysis: bool = False): + """Call peaks for all chromosomes. Return a PeakIO object. + + scoring_function_s: symbols of functions to calculate score. 'p' for pscore, 'q' for qscore, 'f' for fold change, 's' for subtraction. for example: ['p', 'q'] + score_cutoff_s : cutoff values corresponding to scoring functions + min_length : minimum length of peak + max_gap : maximum gap of 'insignificant' regions within a peak. Note, for PE_mode, max_gap and max_length are both as: set fragment length. + call_summits : boolean. Whether or not call sub-peaks. + save_bedGraph : whether or not to save pileup and control into a bedGraph file + """ + chrom: bytes + tmp_bytes: bytes + + peaks = PeakIO() + + # prepare p-q table + if len(self.pqtable) == 0: + info("#3 Pre-compute pvalue-qvalue table...") + if cutoff_analysis: + info("#3 Cutoff vs peaks called will be analyzed!") + self.__pre_computes(max_gap=max_gap, min_length=min_length) + else: + self.__cal_pvalue_qvalue_table() + + + # prepare bedGraph file + if self.save_bedGraph: + self.bedGraph_treat_f = fopen(self.bedGraph_treat_filename, "w") + self.bedGraph_ctrl_f = fopen(self.bedGraph_control_filename, "w") + + info("#3 In the peak calling step, the following will be performed simultaneously:") + info("#3 Write bedGraph files for treatment pileup (after scaling if necessary)... %s" % + self.bedGraph_filename_prefix.decode() + "_treat_pileup.bdg") + info("#3 Write bedGraph files for control lambda (after scaling if necessary)... %s" % + self.bedGraph_filename_prefix.decode() + "_control_lambda.bdg") + + if self.save_SPMR: + info("#3 --SPMR is requested, so pileup will be normalized by sequencing depth in million reads.") + elif self.treat_scaling_factor == 1: + info("#3 Pileup will be based on sequencing depth in treatment.") + else: + info("#3 Pileup will be based on sequencing depth in control.") + + if self.trackline: + # this line is REQUIRED by the wiggle format for UCSC browser + tmp_bytes = ("track type=bedGraph name=\"treatment pileup\" description=\"treatment pileup after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() + fprintf(self.bedGraph_treat_f, tmp_bytes) + tmp_bytes = ("track type=bedGraph name=\"control lambda\" description=\"control lambda after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() + fprintf(self.bedGraph_ctrl_f, tmp_bytes) + + info("#3 Call peaks for each chromosome...") + for chrom in self.chromosomes: + # treat/control bedGraph will be saved if requested by user. + self.__chrom_call_peak_using_certain_criteria(peaks, + chrom, + scoring_function_symbols, + score_cutoff_s, + min_length, + max_gap, + call_summits, + self.save_bedGraph) + + # close bedGraph file + if self.save_bedGraph: + fclose(self.bedGraph_treat_f) + fclose(self.bedGraph_ctrl_f) + self.save_bedGraph = False + + return peaks + + @cython.cfunc + def __chrom_call_peak_using_certain_criteria(self, + peaks, + chrom: bytes, + scoring_function_s: list, + score_cutoff_s: list, + min_length: cython.int, + max_gap: cython.int, + call_summits: bool, + save_bedGraph: bool): + """ Call peaks for a chromosome. + + Combination of criteria is allowed here. + + peaks: a PeakIO object, the return value of this function + scoring_function_s: symbols of functions to calculate score as score=f(x, y) where x is treatment pileup, and y is control pileup + save_bedGraph : whether or not to save pileup and control into a bedGraph file + """ + i: cython.int + s: str + above_cutoff: cnp.ndarray + above_cutoff_endpos: cnp.ndarray(cython.int, ndim=1) + above_cutoff_startpos: cnp.ndarray(cython.int, ndim=1) + pos_array: cnp.ndarray(cython.int, ndim=1) + above_cutoff_index_array: cnp.ndarray(cython.int, ndim=1) + treat_array: cnp.ndarray(cython.float, ndim=1) + ctrl_array: cnp.ndarray(cython.float, ndim=1) + score_array_s: list # to: list keep different types of scores + peak_content: list # to store information for a + # chunk in a peak region, it + # contains lists of: 1. left + # position; 2. right + # position; 3. treatment + # value; 4. control value; + # 5. of: list scores at this + # chunk + tl: cython.long + lastp: cython.long + ts: cython.long + te: cython.long + ti: cython.long + tp: cython.float + cp: cython.float + acs_ptr: cython.pointer(cython.int) + ace_ptr: cython.pointer(cython.int) + acia_ptr: cython.pointer(cython.int) + treat_array_ptr: cython.pointer(cython.float) + ctrl_array_ptr: cython.pointer(cython.float) + + assert len(scoring_function_s) == len(score_cutoff_s), "number of functions and cutoffs should be the same!" + + peak_content = [] # to store points above cutoff + + # first, build pileup, self.chr_pos_treat_ctrl + # this step will be speeped up if pqtable is pre-computed. + self.pileup_treat_ctrl_a_chromosome(chrom) + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + + # while save_bedGraph is true, invoke __write_bedGraph_for_a_chromosome + if save_bedGraph: + self.__write_bedGraph_for_a_chromosome(chrom) + + # keep all types of scores needed + # t0 = ttime() + score_array_s = [] + for i in range(len(scoring_function_s)): + s = scoring_function_s[i] + if s == 'p': + score_array_s.append(self.__cal_pscore(treat_array, + ctrl_array)) + elif s == 'q': + score_array_s.append(self.__cal_qscore(treat_array, + ctrl_array)) + elif s == 'f': + score_array_s.append(self.__cal_FE(treat_array, + ctrl_array)) + elif s == 's': + score_array_s.append(self.__cal_subtraction(treat_array, + ctrl_array)) + + # get the regions with scores above cutoffs. this is not an + # optimized method. It would be better to store score array in + # a 2-D ndarray? + above_cutoff = np.nonzero(apply_multiple_cutoffs(score_array_s, + score_cutoff_s))[0] + # indices + above_cutoff_index_array = np.arange(pos_array.shape[0], + dtype="i4")[above_cutoff] + # end positions of regions where score is above cutoff + above_cutoff_endpos = pos_array[above_cutoff] + # start positions of regions where score is above cutoff + above_cutoff_startpos = pos_array[above_cutoff-1] + + if above_cutoff.size == 0: + # nothing above cutoff + return + + if above_cutoff[0] == 0: + # first element > cutoff, fix the first point as + # 0. otherwise it would be the last item in + # data[chrom]['pos'] + above_cutoff_startpos[0] = 0 + + #print "apply cutoff -- chrom:",chrom," time:", ttime() - t0 + # start to build peak regions + #t0 = ttime() + + # first bit of region above cutoff + acs_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_startpos.data) + ace_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_endpos.data) + acia_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_index_array.data) + treat_array_ptr = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_array_ptr = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + + peak_content.append((ts, te, tp, cp, ti)) + lastp = te + acs_ptr += 1 + ace_ptr += 1 + acia_ptr += 1 + + for i in range(1, above_cutoff_startpos.shape[0]): + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + acs_ptr += 1 + ace_ptr += 1 + acia_ptr += 1 + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + tl = ts - lastp + if tl <= max_gap: + # append. + peak_content.append((ts, te, tp, cp, ti)) + lastp = te # above_cutoff_endpos[i] + else: + # close + if call_summits: + # smooth length is min_length, i.e. fragment size 'd' + self.__close_peak_with_subpeaks(peak_content, + peaks, + min_length, + chrom, + min_length, + score_array_s, + score_cutoff_s=score_cutoff_s) + else: + # smooth length is min_length, i.e. fragment size 'd' + self.__close_peak_wo_subpeaks(peak_content, + peaks, + min_length, + chrom, + min_length, + score_array_s, + score_cutoff_s=score_cutoff_s) + peak_content = [(ts, te, tp, cp, ti),] + lastp = te # above_cutoff_endpos[i] + # save the last peak + if not peak_content: + return + else: + if call_summits: + # smooth length is min_length, i.e. fragment size 'd' + self.__close_peak_with_subpeaks(peak_content, + peaks, + min_length, + chrom, + min_length, + score_array_s, + score_cutoff_s=score_cutoff_s) + else: + # smooth length is min_length, i.e. fragment size 'd' + self.__close_peak_wo_subpeaks(peak_content, + peaks, + min_length, + chrom, + min_length, + score_array_s, + score_cutoff_s=score_cutoff_s) + + # print "close peaks -- chrom:",chrom," time:", ttime() - t0 + return + + @cython.cfunc + def __close_peak_wo_subpeaks(self, + peak_content: list, + peaks, + min_length: cython.int, + chrom: bytes, + smoothlen: cython.int, + score_array_s: list, + score_cutoff_s: list = []) -> bool: + """Close the peak region, output peak boundaries, peak summit + and scores, then add the peak to peakIO object. + + peak_content contains [start, end, treat_p, ctrl_p, index_in_score_array] + + peaks: a PeakIO object + + """ + summit_pos: cython.int + tstart: cython.int + tend: cython.int + summit_index: cython.int + i: cython.int + midindex: cython.int + ttreat_p: cython.double + tctrl_p: cython.double + tscore: cython.double + summit_treat: cython.double + summit_ctrl: cython.double + summit_p_score: cython.double + summit_q_score: cython.double + tlist_scores_p: cython.int + + peak_length = peak_content[-1][1] - peak_content[0][0] + if peak_length >= min_length: # if the peak is too small, reject it + tsummit = [] + summit_pos = 0 + summit_value = 0 + for i in range(len(peak_content)): + (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] + tscore = ttreat_p # use pscore as general score to find summit + if not summit_value or summit_value < tscore: + tsummit = [(tend + tstart) // 2,] + tsummit_index = [i,] + summit_value = tscore + elif summit_value == tscore: + # remember continuous summit values + tsummit.append((tend + tstart) // 2) + tsummit_index.append(i) + # the middle of all highest points in peak region is defined as summit + midindex = (len(tsummit) + 1) // 2 - 1 + summit_pos = tsummit[midindex] + summit_index = tsummit_index[midindex] + + summit_treat = peak_content[summit_index][2] + summit_ctrl = peak_content[summit_index][3] + + # this is a double-check to see if the summit can pass cutoff values. + for i in range(len(score_cutoff_s)): + if score_cutoff_s[i] > score_array_s[i][peak_content[summit_index][4]]: + return False # not passed, then disgard this peak. + + summit_p_score = pscore_dict[(cython.cast(cython.int, + summit_treat), + summit_ctrl)] + summit_q_score = self.pqtable[summit_p_score] + + peaks.add(chrom, # chromosome + peak_content[0][0], # start + peak_content[-1][1], # end + summit=summit_pos, # summit position + peak_score=summit_q_score, # score at summit + pileup=summit_treat, # pileup + pscore=summit_p_score, # pvalue + fold_change=(summit_treat + self.pseudocount) / (summit_ctrl + self.pseudocount), # fold change + qscore=summit_q_score # qvalue + ) + # start a new peak + return True + + @cython.cfunc + def __close_peak_with_subpeaks(self, + peak_content: list, + peaks, + min_length: cython.int, + chrom: bytes, + smoothlen: cython.int, + score_array_s: list, + score_cutoff_s: list = [], + min_valley: cython.float = 0.9) -> bool: + """Algorithm implemented by Ben, to profile the pileup signals + within a peak region then find subpeak summits. This method is + highly recommended for TFBS or DNAase I sites. + + """ + tstart: cython.int + tend: cython.int + summit_index: cython.int + summit_offset: cython.int + start: cython.int + end: cython.int + i: cython.int + start_boundary: cython.int + m: cython.int + n: cython.int + ttreat_p: cython.double + tctrl_p: cython.double + tscore: cython.double + summit_treat: cython.double + summit_ctrl: cython.double + summit_p_score: cython.double + summit_q_score: cython.double + peakdata: cnp.ndarray(cython.float, ndim=1) + peakindices: cnp.ndarray(cython.int, ndim=1) + summit_offsets: cnp.ndarray(cython.int, ndim=1) + tlist_scores_p: cython.int + + peak_length = peak_content[-1][1] - peak_content[0][0] + + if peak_length < min_length: + return # if the region is too small, reject it + + # Add 10 bp padding to peak region so that we can get true minima + end = peak_content[-1][1] + 10 + start = peak_content[0][0] - 10 + if start < 0: + # this is the offof: set original peak boundary in peakdata list. + start_boundary = 10 + start + start = 0 + else: + # this is the offof: set original peak boundary in peakdata list. + start_boundary = 10 + + # save the scores (qscore) for each position in this region + peakdata = np.zeros(end - start, dtype='f4') + # save the indices for each position in this region + peakindices = np.zeros(end - start, dtype='i4') + for i in range(len(peak_content)): + (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] + tscore = ttreat_p # use pileup as general score to find summit + m = tstart - start + start_boundary + n = tend - start + start_boundary + peakdata[m:n] = tscore + peakindices[m:n] = i + + # offsets are the indices for summits in peakdata/peakindices array. + summit_offsets = maxima(peakdata, smoothlen) + + if summit_offsets.shape[0] == 0: + # **failsafe** if no summits, fall back on old approach # + return self.__close_peak_wo_subpeaks(peak_content, + peaks, + min_length, + chrom, + smoothlen, + score_array_s, + score_cutoff_s) + else: + # remove maxima that occurred in padding + m = np.searchsorted(summit_offsets, + start_boundary) + n = np.searchsorted(summit_offsets, + peak_length + start_boundary, + 'right') + summit_offsets = summit_offsets[m:n] + + summit_offsets = enforce_peakyness(peakdata, summit_offsets) + + # print "enforced:",summit_offsets + if summit_offsets.shape[0] == 0: + # **failsafe** if no summits, fall back on old approach # + return self.__close_peak_wo_subpeaks(peak_content, + peaks, + min_length, + chrom, + smoothlen, + score_array_s, + score_cutoff_s) + + # indices are those point to peak_content + summit_indices = peakindices[summit_offsets] + + summit_offsets -= start_boundary + + for summit_offset, summit_index in list(zip(summit_offsets, + summit_indices)): + + summit_treat = peak_content[summit_index][2] + summit_ctrl = peak_content[summit_index][3] + + summit_p_score = pscore_dict[(cython.cast(cython.int, + summit_treat), + summit_ctrl)] + summit_q_score = self.pqtable[summit_p_score] + + for i in range(len(score_cutoff_s)): + if score_cutoff_s[i] > score_array_s[i][peak_content[summit_index][4]]: + return False # not passed, then disgard this summit. + + peaks.add(chrom, + peak_content[0][0], + peak_content[-1][1], + summit=start + summit_offset, + peak_score=summit_q_score, + pileup=summit_treat, + pscore=summit_p_score, + fold_change=(summit_treat + self.pseudocount) / (summit_ctrl + self.pseudocount), # fold change + qscore=summit_q_score + ) + # start a new peak + return True + + @cython.cfunc + def __cal_pscore(self, + array1: cnp.ndarray(cython.float, ndim=1), + array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: + + i: cython.long + array1_size: cython.long + s: cnp.ndarray(cython.float, ndim=1) + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + array1_size = array1.shape[0] + + for i in range(array1_size): + s_ptr[0] = get_pscore((cython.cast(cython.int, + a1_ptr[0]), + a2_ptr[0])) + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_qscore(self, + array1: cnp.ndarray(cython.float, ndim=1), + array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray(cython.float, ndim=1) + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = self.pqtable[get_pscore((cython.cast(cython.int, + a1_ptr[0]), + a2_ptr[0]))] + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_logLR(self, + array1: cnp.ndarray(cython.float, ndim=1), + array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray(cython.float, ndim=1) + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = get_logLR_asym((a1_ptr[0] + self.pseudocount, + a2_ptr[0] + self.pseudocount)) + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_logFE(self, + array1: cnp.ndarray(cython.float, ndim=1), + array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray(cython.float, ndim=1) + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = get_logFE(a1_ptr[0] + self.pseudocount, + a2_ptr[0] + self.pseudocount) + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_FE(self, + array1: cnp.ndarray(cython.float, ndim=1), + array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray(cython.float, ndim=1) + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = (a1_ptr[0] + self.pseudocount) / (a2_ptr[0] + self.pseudocount) + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_subtraction(self, + array1: cnp.ndarray(cython.float, ndim=1), + array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray(cython.float, ndim=1) + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = a1_ptr[0] - a2_ptr[0] + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __write_bedGraph_for_a_chromosome(self, chrom: bytes) -> bool: + """Write treat/control values for a certain chromosome into a + specified file handler. + + """ + pos_array: cnp.ndarray(cython.int, ndim=1) + treat_array: cnp.ndarray(cython.int, ndim=1) + ctrl_array: cnp.ndarray(cython.int, ndim=1) + pos_array_ptr: cython.pointer(cython.int) + treat_array_ptr: cython.pointer(cython.float) + ctrl_array_ptr: cython.pointer(cython.float) + l: cython.int + i: cython.int + p: cython.int + pre_p_t: cython.int + # current position, previous position for treat, previous position for control + pre_p_c: cython.int + pre_v_t: cython.float + pre_v_c: cython.float + v_t: cython.float + # previous value for treat, for control, current value for treat, for control + v_c: cython.float + # 1 if save_SPMR is false, or depth in million if save_SPMR is + # true. Note, while piling up and calling peaks, treatment and + # control have been scaled to the same depth, so we need to + # find what this 'depth' is. + denominator: cython.float + ft: cython.pointer(FILE) + fc: cython.pointer(FILE) + + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + pos_array_ptr = cython.cast(cython.pointer(cython.int), + pos_array.data) + treat_array_ptr = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_array_ptr = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + if self.save_SPMR: + if self.treat_scaling_factor == 1: + # in this case, control has been asked to be scaled to depth of treatment + denominator = self.treat.total/1e6 + else: + # in this case, treatment has been asked to be scaled to depth of control + denominator = self.ctrl.total/1e6 + else: + denominator = 1.0 + + l = pos_array.shape[0] + + if l == 0: # if there is no data, return + return False + + ft = self.bedGraph_treat_f + fc = self.bedGraph_ctrl_f + # t_write_func = self.bedGraph_treat.write + # c_write_func = self.bedGraph_ctrl.write + + pre_p_t = 0 + pre_p_c = 0 + pre_v_t = treat_array_ptr[0]/denominator + pre_v_c = ctrl_array_ptr[0]/denominator + treat_array_ptr += 1 + ctrl_array_ptr += 1 + + for i in range(1, l): + v_t = treat_array_ptr[0]/denominator + v_c = ctrl_array_ptr[0]/denominator + p = pos_array_ptr[0] + pos_array_ptr += 1 + treat_array_ptr += 1 + ctrl_array_ptr += 1 + + if abs(pre_v_t - v_t) > 1e-5: # precision is 5 digits + fprintf(ft, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_t, p, pre_v_t) + pre_v_t = v_t + pre_p_t = p + + if abs(pre_v_c - v_c) > 1e-5: # precision is 5 digits + fprintf(fc, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_c, p, pre_v_c) + pre_v_c = v_c + pre_p_c = p + + p = pos_array_ptr[0] + # last one + fprintf(ft, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_t, p, pre_v_t) + fprintf(fc, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_c, p, pre_v_c) + + return True + + @cython.ccall + def call_broadpeaks(self, + scoring_function_symbols: list, + lvl1_cutoff_s: list, + lvl2_cutoff_s: list, + min_length: cython.int = 200, + lvl1_max_gap: cython.int = 50, + lvl2_max_gap: cython.int = 400, + cutoff_analysis: bool = False): + """This function try to find enriched regions within which, + scores are continuously higher than a given cutoff for level + 1, and link them using the gap above level 2 cutoff with a + maximum length of lvl2_max_gap. + + scoring_function_s: symbols of functions to calculate + score. 'p' for pscore, 'q' for qscore, 'f' for fold change, + 's' for subtraction. for example: ['p', 'q'] + + lvl1_cutoff_s: of: list cutoffs at highly enriched regions, + corresponding to scoring functions. + + lvl2_cutoff_s: of: list cutoffs at less enriched regions, + corresponding to scoring functions. + + min_length : minimum peak length, default 200. + + lvl1_max_gap : maximum gap to merge nearby enriched peaks, + default 50. + + lvl2_max_gap : maximum length of linkage regions, default 400. + + Return both general PeakIO for: object highly enriched regions + and gapped broad regions in BroadPeakIO. + + """ + i: cython.int + j: cython.int + chrom: bytes + lvl1peaks: object + lvl1peakschrom: object + lvl1: object + lvl2peaks: object + lvl2peakschrom: object + lvl2: object + broadpeaks: object + chrs: set + tmppeakset: list + + lvl1peaks = PeakIO() + lvl2peaks = PeakIO() + + # prepare p-q table + if len(self.pqtable) == 0: + info("#3 Pre-compute pvalue-qvalue table...") + if cutoff_analysis: + info("#3 Cutoff value vs broad region calls will be analyzed!") + self.__pre_computes(max_gap=lvl2_max_gap, min_length=min_length) + else: + self.__cal_pvalue_qvalue_table() + + # prepare bedGraph file + if self.save_bedGraph: + + self.bedGraph_treat_f = fopen(self.bedGraph_treat_filename, "w") + self.bedGraph_ctrl_f = fopen(self.bedGraph_control_filename, "w") + info("#3 In the peak calling step, the following will be performed simultaneously:") + info("#3 Write bedGraph files for treatment pileup (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_treat_pileup.bdg") + info("#3 Write bedGraph files for control lambda (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_control_lambda.bdg") + + if self.trackline: + # this line is REQUIRED by the wiggle format for UCSC browser + tmp_bytes = ("track type=bedGraph name=\"treatment pileup\" description=\"treatment pileup after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() + fprintf(self.bedGraph_treat_f, tmp_bytes) + tmp_bytes = ("track type=bedGraph name=\"control lambda\" description=\"control lambda after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() + fprintf(self.bedGraph_ctrl_f, tmp_bytes) + + info("#3 Call peaks for each chromosome...") + for chrom in self.chromosomes: + self.__chrom_call_broadpeak_using_certain_criteria(lvl1peaks, + lvl2peaks, + chrom, + scoring_function_symbols, + lvl1_cutoff_s, + lvl2_cutoff_s, + min_length, + lvl1_max_gap, + lvl2_max_gap, + self.save_bedGraph) + + # close bedGraph file + if self.save_bedGraph: + fclose(self.bedGraph_treat_f) + fclose(self.bedGraph_ctrl_f) + # self.bedGraph_ctrl.close() + self.save_bedGraph = False + + # now combine lvl1 and lvl2 peaks + chrs = lvl1peaks.get_chr_names() + broadpeaks = BroadPeakIO() + # use lvl2_peaks as linking regions between lvl1_peaks + for chrom in sorted(chrs): + lvl1peakschrom = lvl1peaks.get_data_from_chrom(chrom) + lvl2peakschrom = lvl2peaks.get_data_from_chrom(chrom) + lvl1peakschrom_next = iter(lvl1peakschrom).__next__ + tmppeakset = [] # to temporarily store lvl1 region inside a lvl2 region + # our assumption is lvl1 regions should be included in lvl2 regions + try: + lvl1 = lvl1peakschrom_next() + for i in range(len(lvl2peakschrom)): + # for each lvl2 peak, find all lvl1 peaks inside + # I assume lvl1 peaks can be ALL covered by lvl2 peaks. + lvl2 = lvl2peakschrom[i] + + while True: + if lvl2["start"] <= lvl1["start"] and lvl1["end"] <= lvl2["end"]: + tmppeakset.append(lvl1) + lvl1 = lvl1peakschrom_next() + else: + # make a hierarchical broad peak + #print lvl2["start"], lvl2["end"], lvl2["score"] + self.__add_broadpeak(broadpeaks, + chrom, + lvl2, + tmppeakset) + tmppeakset = [] + break + except StopIteration: + # no more strong (aka lvl1) peaks left + self.__add_broadpeak(broadpeaks, + chrom, + lvl2, + tmppeakset) + tmppeakset = [] + # add the rest lvl2 peaks + for j in range(i+1, len(lvl2peakschrom)): + self.__add_broadpeak(broadpeaks, + chrom, + lvl2peakschrom[j], + tmppeakset) + + return broadpeaks + + @cython.cfunc + def __chrom_call_broadpeak_using_certain_criteria(self, + lvl1peaks, + lvl2peaks, + chrom: bytes, + scoring_function_s: list, + lvl1_cutoff_s: list, + lvl2_cutoff_s: list, + min_length: cython.int, + lvl1_max_gap: cython.int, + lvl2_max_gap: cython.int, + save_bedGraph: bool): + """Call peaks for a chromosome. + + Combination of criteria is allowed here. + + peaks: a PeakIO object + + scoring_function_s: symbols of functions to calculate score as + score=f(x, y) where x is treatment pileup, and y is control + pileup + + save_bedGraph : whether or not to save pileup and control into + a bedGraph file + + """ + i: cython.int + s: str + above_cutoff: cnp.ndarray + above_cutoff_endpos: cnp.ndarray + above_cutoff_startpos: cnp.ndarray + pos_array: cnp.ndarray + treat_array: cnp.ndarray + ctrl_array: cnp.ndarray + above_cutoff_index_array: cnp.ndarray + score_array_s: list # to: list keep different types of scores + peak_content: list + acs_ptr: cython.pointer(cython.int) + ace_ptr: cython.pointer(cython.int) + acia_ptr: cython.pointer(cython.int) + treat_array_ptr: cython.pointer(cython.float) + ctrl_array_ptr: cython.pointer(cython.float) + + assert len(scoring_function_s) == len(lvl1_cutoff_s), "number of functions and cutoffs should be the same!" + assert len(scoring_function_s) == len(lvl2_cutoff_s), "number of functions and cutoffs should be the same!" + + # first, build pileup, self.chr_pos_treat_ctrl + self.pileup_treat_ctrl_a_chromosome(chrom) + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + + # while save_bedGraph is true, invoke __write_bedGraph_for_a_chromosome + if save_bedGraph: + self.__write_bedGraph_for_a_chromosome(chrom) + + # keep all types of scores needed + score_array_s = [] + for i in range(len(scoring_function_s)): + s = scoring_function_s[i] + if s == 'p': + score_array_s.append(self.__cal_pscore(treat_array, + ctrl_array)) + elif s == 'q': + score_array_s.append(self.__cal_qscore(treat_array, + ctrl_array)) + elif s == 'f': + score_array_s.append(self.__cal_FE(treat_array, + ctrl_array)) + elif s == 's': + score_array_s.append(self.__cal_subtraction(treat_array, + ctrl_array)) + + # lvl1 : strong peaks + peak_content = [] # to store points above cutoff + + # get the regions with scores above cutoffs + above_cutoff = np.nonzero(apply_multiple_cutoffs(score_array_s, + lvl1_cutoff_s))[0] # this is not an optimized method. It would be better to store score array in a 2-D ndarray? + above_cutoff_index_array = np.arange(pos_array.shape[0], + dtype="int32")[above_cutoff] # indices + above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff + above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff + + if above_cutoff.size == 0: + # nothing above cutoff + return + + if above_cutoff[0] == 0: + # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] + above_cutoff_startpos[0] = 0 + + # first bit of region above cutoff + acs_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_startpos.data) + ace_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_endpos.data) + acia_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_index_array.data) + treat_array_ptr = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_array_ptr = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + + peak_content.append((ts, te, tp, cp, ti)) + acs_ptr += 1 # move ptr + ace_ptr += 1 + acia_ptr += 1 + lastp = te + + # peak_content.append((above_cutoff_startpos[0], above_cutoff_endpos[0], treat_array[above_cutoff_index_array[0]], ctrl_array[above_cutoff_index_array[0]], score_array_s, above_cutoff_index_array[0])) + for i in range(1, above_cutoff_startpos.size): + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + acs_ptr += 1 + ace_ptr += 1 + acia_ptr += 1 + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + tl = ts - lastp + if tl <= lvl1_max_gap: + # append + peak_content.append((ts, te, tp, cp, ti)) + lastp = te + else: + # close + self.__close_peak_for_broad_region(peak_content, + lvl1peaks, + min_length, + chrom, + lvl1_max_gap//2, + score_array_s) + peak_content = [(ts, te, tp, cp, ti),] + lastp = te # above_cutoff_endpos[i] + + # save the last peak + if peak_content: + self.__close_peak_for_broad_region(peak_content, + lvl1peaks, + min_length, + chrom, + lvl1_max_gap//2, + score_array_s) + + # lvl2 : weak peaks + peak_content = [] # to store points above cutoff + + # get the regions with scores above cutoffs + + # this is not an optimized method. It would be better to store score array in a 2-D ndarray? + above_cutoff = np.nonzero(apply_multiple_cutoffs(score_array_s, + lvl2_cutoff_s))[0] + + above_cutoff_index_array = np.arange(pos_array.shape[0], + dtype="i4")[above_cutoff] # indices + above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff + above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff + + if above_cutoff.size == 0: + # nothing above cutoff + return + + if above_cutoff[0] == 0: + # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] + above_cutoff_startpos[0] = 0 + + # first bit of region above cutoff + acs_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_startpos.data) + ace_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_endpos.data) + acia_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_index_array.data) + treat_array_ptr = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_array_ptr = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + peak_content.append((ts, te, tp, cp, ti)) + acs_ptr += 1 # move ptr + ace_ptr += 1 + acia_ptr += 1 + + lastp = te + for i in range(1, above_cutoff_startpos.size): + # for everything above cutoff + ts = acs_ptr[0] # get the start + te = ace_ptr[0] # get the end + ti = acia_ptr[0] # get the index + + acs_ptr += 1 # move ptr + ace_ptr += 1 + acia_ptr += 1 + tp = treat_array_ptr[ti] # get the treatment pileup + cp = ctrl_array_ptr[ti] # get the control pileup + tl = ts - lastp # get the distance from the current point to last position of existing peak_content + + if tl <= lvl2_max_gap: + # append + peak_content.append((ts, te, tp, cp, ti)) + lastp = te + else: + # close + self.__close_peak_for_broad_region(peak_content, + lvl2peaks, + min_length, + chrom, + lvl2_max_gap//2, + score_array_s) + + peak_content = [(ts, te, tp, cp, ti),] + lastp = te + + # save the last peak + if peak_content: + self.__close_peak_for_broad_region(peak_content, + lvl2peaks, + min_length, + chrom, + lvl2_max_gap//2, + score_array_s) + + return + + @cython.cfunc + def __close_peak_for_broad_region(self, + peak_content: list, + peaks, + min_length: cython.int, + chrom: bytes, + smoothlen: cython.int, + score_array_s: list, + score_cutoff_s: list = []) -> bool: + """Close the broad peak region, output peak boundaries, peak summit + and scores, then add the peak to peakIO object. + + peak_content contains [start, end, treat_p, ctrl_p, list_scores] + + peaks: a BroadPeakIO object + + """ + tstart: cython.int + tend: cython.int + i: cython.int + ttreat_p: cython.double + tctrl_p: cython.double + tlist_pileup: list + tlist_control: list + tlist_length: list + tlist_scores_p: cython.int + tarray_pileup: cnp.ndarray + tarray_control: cnp.ndarray + tarray_pscore: cnp.ndarray + tarray_qscore: cnp.ndarray + tarray_fc: cnp.ndarray + + peak_length = peak_content[-1][1] - peak_content[0][0] + if peak_length >= min_length: # if the peak is too small, reject it + tlist_pileup = [] + tlist_control = [] + tlist_length = [] + for i in range(len(peak_content)): # each position in broad peak + (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] + tlist_pileup.append(ttreat_p) + tlist_control.append(tctrl_p) + tlist_length.append(tend - tstart) + + tarray_pileup = np.array(tlist_pileup, dtype="f4") + tarray_control = np.array(tlist_control, dtype="f4") + tarray_pscore = self.__cal_pscore(tarray_pileup, tarray_control) + tarray_qscore = self.__cal_qscore(tarray_pileup, tarray_control) + tarray_fc = self.__cal_FE(tarray_pileup, tarray_control) + + peaks.add(chrom, # chromosome + peak_content[0][0], # start + peak_content[-1][1], # end + summit=0, + peak_score=mean_from_value_length(tarray_qscore, tlist_length), + pileup=mean_from_value_length(tarray_pileup, tlist_length), + pscore=mean_from_value_length(tarray_pscore, tlist_length), + fold_change=mean_from_value_length(tarray_fc, tlist_length), + qscore=mean_from_value_length(tarray_qscore, tlist_length), + ) + # if chrom == "chr1" and peak_content[0][0] == 237643 and peak_content[-1][1] == 237935: + # print tarray_qscore, tlist_length + # start a new peak + return True + + @cython.cfunc + def __add_broadpeak(self, + bpeaks, + chrom: bytes, + lvl2peak: object, + lvl1peakset: list): + """Internal function to create broad peak. + + *Note* lvl1peakset/strong_regions might be empty + """ + + blockNum: cython.int + start: cython.int + end: cython.int + blockSizes: bytes + blockStarts: bytes + thickStart: bytes + thickEnd: bytes + + start = lvl2peak["start"] + end = lvl2peak["end"] + + if not lvl1peakset: + # will complement by adding 1bps start and end to this region + # may change in the future if gappedPeak format was improved. + bpeaks.add(chrom, start, end, + score=lvl2peak["score"], + thickStart=(b"%d" % start), + thickEnd=(b"%d" % end), + blockNum=2, + blockSizes=b"1,1", + blockStarts=(b"0,%d" % (end-start-1)), + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) + return bpeaks + + thickStart = b"%d" % (lvl1peakset[0]["start"]) + thickEnd = b"%d" % (lvl1peakset[-1]["end"]) + blockNum = len(lvl1peakset) + blockSizes = b",".join([b"%d" % y for y in [x["length"] for x in lvl1peakset]]) + blockStarts = b",".join([b"%d" % x for x in getitem_then_subtract(lvl1peakset, start)]) + + # add 1bp left and/or right block if necessary + if int(thickStart) != start: + # add 1bp left block + thickStart = b"%d" % start + blockNum += 1 + blockSizes = b"1,"+blockSizes + blockStarts = b"0,"+blockStarts + if int(thickEnd) != end: + # add 1bp right block + thickEnd = b"%d" % end + blockNum += 1 + blockSizes = blockSizes + b",1" + blockStarts = blockStarts + b"," + (b"%d" % (end-start-1)) + + bpeaks.add(chrom, start, end, + score=lvl2peak["score"], + thickStart=thickStart, + thickEnd=thickEnd, + blockNum=blockNum, + blockSizes=blockSizes, + blockStarts=blockStarts, + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) + return bpeaks From f48272b9cc5955291cb129f758516ff22a01b3d5 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Tue, 22 Oct 2024 10:02:48 -0400 Subject: [PATCH 10/13] changelog --- ChangeLog | 11 ++- MACS3/Signal/CallPeakUnit.py | 132 ++++++++++++++++++----------------- pyproject.toml | 6 +- 3 files changed, 81 insertions(+), 68 deletions(-) diff --git a/ChangeLog b/ChangeLog index 3b8c97df..7d00be76 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,7 +3,16 @@ * Features added - 1) We extensively rewrote the `pyx` codes into `py` codes. In + 1) We implemented the IO module for reading the fragment files + usually used in single-cell ATAC-seq experiment + `Parser.FragParser`. And we implemented a new + `PairedEndTrack.PETrackII` to store the data in fragment file, + including the barcodes and counts information. In the `PETrackII` + class, we are able to extract a subset using a list of barcodes, + which enables us to call peaks only on a pool (pseudo-bulk) of + cells. + + 2) We extensively rewrote the `pyx` codes into `py` codes. In another words, we now apply the 'pure python style' with PEP-484 type annotations to our previous Cython style codes. So that, the source codes can be more compatible to Python programming tools diff --git a/MACS3/Signal/CallPeakUnit.py b/MACS3/Signal/CallPeakUnit.py index 166ae34e..cf325d2b 100644 --- a/MACS3/Signal/CallPeakUnit.py +++ b/MACS3/Signal/CallPeakUnit.py @@ -1,7 +1,7 @@ # cython: language_level=3 # cython: profile=True # cython: linetrace=True -# Time-stamp: <2024-10-21 17:49:36 Tao Liu> +# Time-stamp: <2024-10-21 22:36:05 Tao Liu> """Module for Calculate Scores. @@ -388,7 +388,8 @@ class CallerFromAlignments: # data needed to be pre-computed before peak calling # remember pvalue->qvalue convertion; saved in cykhash Float32to32Map pqtable: Float32to32Map - # whether the pvalue of whole genome is all calculated. If yes, it's OK to calculate q-value. + # whether the pvalue of whole genome is all calculated. If yes, + # it's OK to calculate q-value. pvalue_all_done: bool # record for each pvalue cutoff, how many peaks can be called pvalue_npeaks: dict @@ -399,7 +400,8 @@ class CallerFromAlignments: optimal_p_cutoff: cython.float # file to save the pvalue-npeaks-totallength table cutoff_analysis_filename: bytes - # Record the names of temporary files for storing pileup values of each chromosome + # Record the names of temporary files for storing pileup values of + # each chromosome pileup_data_files: dict def __init__(self, @@ -490,7 +492,9 @@ def __init__(self, self.pileup_data_files = {} self.pvalue_length = {} self.pvalue_npeaks = {} - for p in np.arange(0.3, 10, 0.3): # step for optimal cutoff is 0.3 in -log10pvalue, we try from pvalue 1E-10 (-10logp=10) to 0.5 (-10logp=0.3) + # step for optimal cutoff is 0.3 in -log10pvalue, we try from + # pvalue 1E-10 (-10logp=10) to 0.5 (-10logp=0.3) + for p in np.arange(0.3, 10, 0.3): self.pvalue_length[p] = 0 self.pvalue_npeaks[p] = 0 self.optimal_p_cutoff = 0 @@ -587,10 +591,12 @@ def pileup_treat_ctrl_a_chromosome(self, chrom: bytes): baseline_value=self.lambda_bg, directional=False) else: + # a: set global lambda ctrl_pv = [treat_pv[0][-1:], np.array([self.lambda_bg,], - dtype="f4")] # a: set global lambda + dtype="f4")] - self.chr_pos_treat_ctrl = self.__chrom_pair_treat_ctrl(treat_pv, ctrl_pv) + self.chr_pos_treat_ctrl = self.__chrom_pair_treat_ctrl(treat_pv, + ctrl_pv) # clean treat_pv and ctrl_pv treat_pv = [] @@ -626,13 +632,14 @@ def __chrom_pair_treat_ctrl(self, treat_pv, ctrl_pv) -> list: c_v: cnp.ndarray(cython.float, ndim=1) ret_t: cnp.ndarray(cython.float, ndim=1) ret_c: cnp.ndarray(cython.float, ndim=1) - t_p_ptr: cython.pointer[cython.int] - c_p_ptr: cython.pointer[cython.int] - ret_p_ptr: cython.pointer[cython.int] - t_v_ptr: cython.pointer[cython.float] - c_v_ptr: cython.pointer[cython.float] - ret_t_ptr: cython.pointer[cython.float] - ret_c_ptr: cython.pointer[cython.float] + t_p_view: cython.int [:] + # cython.pointer[cython.int] + c_p_view: cython.int [:] + ret_p_view: cython.int [:] + t_v_view: cython.float [:] # cython.pointer[cython.float] + c_v_view: cython.float [:] # cython.pointer[cython.float] + ret_t_view: cython.float [:] # cython.pointer[cython.float] + ret_c_view: cython.float [:] # cython.pointer[cython.float] [t_p, t_v] = treat_pv [c_p, c_v] = ctrl_pv @@ -646,61 +653,61 @@ def __chrom_pair_treat_ctrl(self, treat_pv, ctrl_pv) -> list: ret_t = np.zeros(chrom_max_len, dtype="f4") # value from treatment ret_c = np.zeros(chrom_max_len, dtype="f4") # value from control - t_p_ptr = cython.cast(cython.pointer[cython.int], t_p.data) - t_v_ptr = cython.cast(cython.pointer[cython.float], t_v.data) - c_p_ptr = cython.cast(cython.pointer[cython.int], c_p.data) - c_v_ptr = cython.cast(cython.pointer[cython.float], c_v.data) - ret_p_ptr = cython.cast(cython.pointer[cython.int], ret_p.data) - ret_t_ptr = cython.cast(cython.pointer[cython.float], ret_t.data) - ret_c_ptr = cython.cast(cython.pointer[cython.float], ret_c.data) + t_p_view = t_p #cython.cast(cython.pointer[cython.int], t_p.data) + t_v_view = t_v #cython.cast(cython.pointer[cython.float], t_v.data) + c_p_view = c_p #cython.cast(cython.pointer[cython.int], c_p.data) + c_v_view = c_v #cython.cast(cython.pointer[cython.float], c_v.data) + ret_p_view = ret_p #cython.cast(cython.pointer[cython.int], ret_p.data) + ret_t_view = ret_t #cython.cast(cython.pointer[cython.float], ret_t.data) + ret_c_view = ret_c #cython.cast(cython.pointer[cython.float], ret_c.data) index_ret = 0 it = 0 ic = 0 while it < lt and ic < lc: - if t_p_ptr[0] < c_p_ptr[0]: + if t_p_view[0] < c_p_view[0]: # clip a region from pre_p to p1, then pre_p: set as p1. - ret_p_ptr[0] = t_p_ptr[0] - ret_t_ptr[0] = t_v_ptr[0] - ret_c_ptr[0] = c_v_ptr[0] - ret_p_ptr += 1 - ret_t_ptr += 1 - ret_c_ptr += 1 + ret_p_view[0] = t_p_view[0] + ret_t_view[0] = t_v_view[0] + ret_c_view[0] = c_v_view[0] + ret_p_view += 1 + ret_t_view += 1 + ret_c_view += 1 index_ret += 1 # call for the next p1 and v1 it += 1 - t_p_ptr += 1 - t_v_ptr += 1 - elif t_p_ptr[0] > c_p_ptr[0]: + t_p_view += 1 + t_v_view += 1 + elif t_p_view[0] > c_p_view[0]: # clip a region from pre_p to p2, then pre_p: set as p2. - ret_p_ptr[0] = c_p_ptr[0] - ret_t_ptr[0] = t_v_ptr[0] - ret_c_ptr[0] = c_v_ptr[0] - ret_p_ptr += 1 - ret_t_ptr += 1 - ret_c_ptr += 1 + ret_p_view[0] = c_p_view[0] + ret_t_view[0] = t_v_view[0] + ret_c_view[0] = c_v_view[0] + ret_p_view += 1 + ret_t_view += 1 + ret_c_view += 1 index_ret += 1 # call for the next p2 and v2 ic += 1 - c_p_ptr += 1 - c_v_ptr += 1 + c_p_view += 1 + c_v_view += 1 else: # from pre_p to p1 or p2, then pre_p: set as p1 or p2. - ret_p_ptr[0] = t_p_ptr[0] - ret_t_ptr[0] = t_v_ptr[0] - ret_c_ptr[0] = c_v_ptr[0] - ret_p_ptr += 1 - ret_t_ptr += 1 - ret_c_ptr += 1 + ret_p_view[0] = t_p_view[0] + ret_t_view[0] = t_v_view[0] + ret_c_view[0] = c_v_view[0] + ret_p_view += 1 + ret_t_view += 1 + ret_c_view += 1 index_ret += 1 # call for the next p1, v1, p2, v2. it += 1 ic += 1 - t_p_ptr += 1 - t_v_ptr += 1 - c_p_ptr += 1 - c_v_ptr += 1 + t_p_view += 1 + t_v_view += 1 + c_p_view += 1 + c_v_view += 1 ret_p.resize(index_ret, refcheck=False) ret_t.resize(index_ret, refcheck=False) @@ -747,9 +754,9 @@ def __cal_pvalue_qvalue_table(self): this_l: cython.long f: cython.float unique_values: list - pos_ptr: cython.pointer[cython.int] - treat_value_ptr: cython.pointer[cython.float] - ctrl_value_ptr: cython.pointer[cython.float] + pos_view: cython.int [:] # cython.pointer[cython.int] + treat_value_view: cython.float [:] # cython.pointer[cython.float] + ctrl_value_view: cython.float [:] # cython.pointer[cython.float] debug("Start to calculate pvalue stat...") @@ -761,26 +768,23 @@ def __cal_pvalue_qvalue_table(self): self.pileup_treat_ctrl_a_chromosome(chrom) [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - pos_ptr = cython.cast(cython.pointer(cython.int), - pos_array.data) - treat_value_ptr = cython.cast(cython.pointer(cython.float), - treat_array.data) - ctrl_value_ptr = cython.cast(cython.pointer(cython.float), - ctrl_array.data) + pos_view = pos_array + treat_value_view = treat_array + ctrl_value_view = ctr_array for j in range(pos_array.shape[0]): this_v = get_pscore((cython.cast(cython.int, - treat_value_ptr[0]), - ctrl_value_ptr[0])) - this_l = pos_ptr[0] - pre_p + treat_value_view[0]), + ctrl_value_view[0])) + this_l = pos_view[0] - pre_p if this_v in pscore_stat: pscore_stat[this_v] += this_l else: pscore_stat[this_v] = this_l - pre_p = pos_ptr[0] - pos_ptr += 1 - treat_value_ptr += 1 - ctrl_value_ptr += 1 + pre_p = pos_view[0] + pos_view += 1 + treat_value_view += 1 + ctrl_value_view += 1 N = sum(pscore_stat.values()) # total length k = 1 # rank diff --git a/pyproject.toml b/pyproject.toml index 1f4c6cad..9babe702 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires=['setuptools>=68.0', 'numpy>=1.25,<2.0.0', 'scipy>=1.12', 'cykhash>=2.0,<3.0', 'Cython>=3.0,<3.1', 'scikit-learn>=1.3', 'hmmlearn>=0.3.2'] +requires=['setuptools>=68.0', 'numpy>=1.25,<2', 'scipy>=1.12', 'cykhash>=2.0', 'Cython>=3.0', 'scikit-learn>=1.3', 'hmmlearn>=0.3.2'] build-backend = "setuptools.build_meta" [project] @@ -24,11 +24,11 @@ classifiers =['Development Status :: 5 - Production/Stable', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Cython'] -dependencies = ["numpy>=1.25,<2.0.0", +dependencies = ["numpy>=1.25,<2", "scipy>=1.12", "hmmlearn>=0.3.2", "scikit-learn>=1.3", - "cykhash>=2.0,<3.0"] + "cykhash>=2.0"] [project.urls] Homepage = "https://https://macs3-project.github.io/MACS/" From 48e5a23302eba3de5313fb316d5f770dd054673c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Liu=20=28=CF=84=CE=BD=29?= Date: Tue, 22 Oct 2024 10:09:19 -0400 Subject: [PATCH 11/13] Delete MACS3/Signal/CallPeakUnit.py Currently don't have a good way to implement pointer addition in pure python style. --- MACS3/Signal/CallPeakUnit.py | 2246 ---------------------------------- 1 file changed, 2246 deletions(-) delete mode 100644 MACS3/Signal/CallPeakUnit.py diff --git a/MACS3/Signal/CallPeakUnit.py b/MACS3/Signal/CallPeakUnit.py deleted file mode 100644 index cf325d2b..00000000 --- a/MACS3/Signal/CallPeakUnit.py +++ /dev/null @@ -1,2246 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# cython: linetrace=True -# Time-stamp: <2024-10-21 22:36:05 Tao Liu> - -"""Module for Calculate Scores. - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file LICENSE included with -the distribution). -""" - -# ------------------------------------ -# python modules -# ------------------------------------ - -import _pickle as cPickle -from tempfile import mkstemp -import os - -# ------------------------------------ -# Other modules -# ------------------------------------ -import numpy as np -import cython -import cython.cimports.numpy as cnp -# from numpy cimport int32_t, int64_t, float32_t, float64_t -from cython.cimports.cpython import bool -from cykhash import PyObjectMap, Float32to32Map - -# ------------------------------------ -# C lib -# ------------------------------------ -from cython.cimports.libc.stdio import FILE, fopen, fprintf, fclose -from cython.cimports.libc.math import exp, log10, log1p, erf, sqrt - -# ------------------------------------ -# MACS3 modules -# ------------------------------------ -from MACS3.Signal.SignalProcessing import maxima, enforce_peakyness -from MACS3.IO.PeakIO import PeakIO, BroadPeakIO -from MACS3.Signal.FixWidthTrack import FWTrack -from MACS3.Signal.PairedEndTrack import PETrackI -from MACS3.Signal.Prob import poisson_cdf -from MACS3.Utilities.Logger import logging - -logger = logging.getLogger(__name__) -debug = logger.debug -info = logger.info -# -------------------------------------------- -# cached pscore function and LR_asym functions -# -------------------------------------------- -pscore_dict = PyObjectMap() -logLR_dict = PyObjectMap() - - -@cython.cfunc -def get_pscore(t: tuple) -> cython.float: - """t: tuple of (lambda, observation) - """ - val: cython.float - - if t in pscore_dict: - return pscore_dict[t] - else: - # calculate and cache - val = -1.0 * poisson_cdf(t[0], t[1], False, True) - pscore_dict[t] = val - return val - - -@cython.cfunc -def get_logLR_asym(t: tuple) -> cython.float: - """Calculate log10 Likelihood between H1 (enriched) and H0 ( - chromatin bias). Set minus sign for depletion. - """ - val: cython.float - x: cython.float - y: cython.float - - if t in logLR_dict: - return logLR_dict[t] - else: - x = t[0] - y = t[1] - # calculate and cache - if x > y: - val = (x*(log10(x)-log10(y))+y-x) - elif x < y: - val = (x*(-log10(x)+log10(y))-y+x) - else: - val = 0 - logLR_dict[t] = val - return val - -# ------------------------------------ -# constants -# ------------------------------------ - - -LOG10_E: cython.float = 0.43429448190325176 - -# ------------------------------------ -# Misc functions -# ------------------------------------ - - -@cython.cfunc -def clean_up_ndarray(x: cnp.ndarray): - # clean numpy ndarray in two steps - i: cython.long - - i = x.shape[0] // 2 - x.resize(100000 if i > 100000 else i, refcheck=False) - x.resize(0, refcheck=False) - return - - -@cython.cfunc -@cython.inline -def chi2_k1_cdf(x: cython.float) -> cython.float: - return erf(sqrt(x/2)) - - -@cython.cfunc -@cython.inline -def log10_chi2_k1_cdf(x: cython.float) -> cython.float: - return log10(erf(sqrt(x/2))) - - -@cython.cfunc -@cython.inline -def chi2_k2_cdf(x: cython.float) -> cython.float: - return 1 - exp(-x/2) - - -@cython.cfunc -@cython.inline -def log10_chi2_k2_cdf(x: cython.float) -> cython.float: - return log1p(- exp(-x/2)) * LOG10_E - - -@cython.cfunc -@cython.inline -def chi2_k4_cdf(x: cython.float) -> cython.float: - return 1 - exp(-x/2) * (1 + x/2) - - -@cython.cfunc -@cython.inline -def log10_chi2_k4_CDF(x: cython.float) -> cython.float: - return log1p(- exp(-x/2) * (1 + x/2)) * LOG10_E - - -@cython.cfunc -@cython.inline -def apply_multiple_cutoffs(multiple_score_arrays: list, - multiple_cutoffs: list) -> cnp.ndarray: - i: cython.int - ret: cnp.ndarray - - ret = multiple_score_arrays[0] > multiple_cutoffs[0] - - for i in range(1, len(multiple_score_arrays)): - ret += multiple_score_arrays[i] > multiple_cutoffs[i] - - return ret - - -@cython.cfunc -@cython.inline -def get_from_multiple_scores(multiple_score_arrays: list, - index: cython.int) -> list: - ret: list = [] - i: cython.int - - for i in range(len(multiple_score_arrays)): - ret.append(multiple_score_arrays[i][index]) - return ret - - -@cython.cfunc -@cython.inline -def get_logFE(x: cython.float, - y: cython.float) -> cython.float: - """ return 100* log10 fold enrichment with +1 pseudocount. - """ - return log10(x/y) - - -@cython.cfunc -@cython.inline -def get_subtraction(x: cython.float, - y: cython.float) -> cython.float: - """ return subtraction. - """ - return x - y - - -@cython.cfunc -@cython.inline -def getitem_then_subtract(peakset: list, - start: cython.int) -> list: - a: list - - a = [x["start"] for x in peakset] - for i in range(len(a)): - a[i] = a[i] - start - return a - - -@cython.cfunc -@cython.inline -def left_sum(data, pos: cython.int, - width: cython.int) -> cython.int: - """ - """ - return sum([data[x] for x in data if x <= pos and x >= pos - width]) - - -@cython.cfunc -@cython.inline -def right_sum(data, - pos: cython.int, - width: cython.int) -> cython.int: - """ - """ - return sum([data[x] for x in data if x >= pos and x <= pos + width]) - - -@cython.cfunc -@cython.inline -def left_forward(data, - pos: cython.int, - window_size: cython.int) -> cython.int: - return data.get(pos, 0) - data.get(pos-window_size, 0) - - -@cython.cfunc -@cython.inline -def right_forward(data, - pos: cython.int, - window_size: cython.int) -> cython.int: - return data.get(pos + window_size, 0) - data.get(pos, 0) - - -@cython.cfunc -def median_from_value_length(value: cnp.ndarray(cython.float, ndim=1), - length: list) -> cython.float: - """ - """ - tmp: list - c: cython.int - tmp_l: cython.int - tmp_v: cython.float - mid_l: cython.float - - c = 0 - tmp = sorted(list(zip(value, length))) - mid_l = sum(length)/2 - for (tmp_v, tmp_l) in tmp: - c += tmp_l - if c > mid_l: - return tmp_v - - -@cython.cfunc -def mean_from_value_length(value: cnp.ndarray(cython.float, ndim=1), - length: list) -> cython.float: - """take of: list values and of: list corresponding lengths, - calculate the mean. An important function for bedGraph type of - data. - - """ - i: cython.int - tmp_l: cython.int - ln: cython.int - tmp_v: cython.double - sum_v: cython.double - tmp_sum: cython.double - ret: cython.float - - sum_v = 0 - ln = 0 - - for i in range(len(length)): - tmp_l = length[i] - tmp_v = cython.cast(cython.double, value[i]) - tmp_sum = tmp_v * tmp_l - sum_v = tmp_sum + sum_v - ln += tmp_l - - ret = cython.cast(cython.float, (sum_v/ln)) - - return ret - - -@cython.cfunc -def find_optimal_cutoff(x: list, y: list) -> tuple: - """Return the best cutoff x and y. - - We assume that total peak length increase exponentially while - decreasing cutoff value. But while cutoff decreases to a point - that background noises are captured, total length increases much - faster. So we fit a linear model by taking the first 10 points, - then look for the largest cutoff that - - - *Currently, it is coded as a useless function. - """ - npx: cnp.ndarray - npy: cnp.ndarray - npA: cnp.ndarray - ln: cython.long - i: cython.long - m: cython.float - c: cython.float # slop and intercept - sst: cython.float # sum of squared total - sse: cython.float # sum of squared error - rsq: cython.float # R-squared - - ln = len(x) - assert ln == len(y) - npx = np.array(x) - npy = np.log10(np.array(y)) - npA = np.vstack([npx, np.ones(len(npx))]).T - - for i in range(10, ln): - # at least the largest 10 points - m, c = np.linalg.lstsq(npA[:i], npy[:i], rcond=None)[0] - sst = sum((npy[:i] - np.mean(npy[:i])) ** 2) - sse = sum((npy[:i] - m*npx[:i] - c) ** 2) - rsq = 1 - sse/sst - # print i, x[i], y[i], m, c, rsq - return (1.0, 1.0) - - -# ------------------------------------ -# Classes -# ------------------------------------ -@cython.cclass -class CallerFromAlignments: - """A unit to calculate scores and call peaks from alignments -- - FWTrack or PETrack objects. - - It will compute for each chromosome separately in order to save - memory usage. - """ - treat: object # FWTrack or PETrackI object for ChIP - ctrl: object # FWTrack or PETrackI object for Control - - d: cython.int # extension size for ChIP - # extension sizes for Control. Can be multiple values - ctrl_d_s: list - treat_scaling_factor: cython.float # scaling factor for ChIP - # scaling factor for Control, corresponding to each extension size. - ctrl_scaling_factor_s: list - # minimum local bias to fill missing values - lambda_bg: cython.float - # name of common chromosomes in ChIP and Control data - chromosomes: list - # the pseudocount used to calcuate logLR, FE or logFE - pseudocount: cython.double - # prefix will be added to _pileup.bdg for treatment and - # _lambda.bdg for control - bedGraph_filename_prefix: bytes - # shift of cutting ends before extension - end_shift: cython.int - # whether trackline should be saved in bedGraph - trackline: bool - # whether to save pileup and local bias in bedGraph files - save_bedGraph: bool - # whether to save pileup normalized by sequencing depth in million reads - save_SPMR: bool - # whether ignore local bias, and to use global bias instead - no_lambda_flag: bool - # whether it's in PE mode, will be detected during initiation - PE_mode: bool - - # temporary data buffer - # temporary [position, treat_pileup, ctrl_pileup] for a given chromosome - chr_pos_treat_ctrl: list - bedGraph_treat_filename: bytes - bedGraph_control_filename: bytes - bedGraph_treat_f: cython.pointer(FILE) - bedGraph_ctrl_f: cython.pointer(FILE) - - # data needed to be pre-computed before peak calling - # remember pvalue->qvalue convertion; saved in cykhash Float32to32Map - pqtable: Float32to32Map - # whether the pvalue of whole genome is all calculated. If yes, - # it's OK to calculate q-value. - pvalue_all_done: bool - # record for each pvalue cutoff, how many peaks can be called - pvalue_npeaks: dict - # record for each pvalue cutoff, the total length of called peaks - pvalue_length: dict - # automatically decide the p-value cutoff (can be translated into - # qvalue cutoff) based on p-value to total peak length analysis. - optimal_p_cutoff: cython.float - # file to save the pvalue-npeaks-totallength table - cutoff_analysis_filename: bytes - # Record the names of temporary files for storing pileup values of - # each chromosome - pileup_data_files: dict - - def __init__(self, - treat, - ctrl, - d: cython.int = 200, - ctrl_d_s: list = [200, 1000, 10000], - treat_scaling_factor: cython.float = 1.0, - ctrl_scaling_factor_s: list = [1.0, 0.2, 0.02], - stderr_on: bool = False, - pseudocount: cython.float = 1, - end_shift: cython.int = 0, - lambda_bg: cython.float = 0, - save_bedGraph: bool = False, - bedGraph_filename_prefix: str = "PREFIX", - bedGraph_treat_filename: str = "TREAT.bdg", - bedGraph_control_filename: str = "CTRL.bdg", - cutoff_analysis_filename: str = "TMP.txt", - save_SPMR: bool = False): - """Initialize. - - A calculator is unique to each comparison of treat and - control. Treat_depth and ctrl_depth should not be changed - during calculation. - - treat and ctrl are either FWTrack or PETrackI objects. - - treat_depth and ctrl_depth are effective depth in million: - sequencing depth in million after - duplicates being filtered. If - treatment is scaled down to - control sample size, then this - should be control sample size in - million. And vice versa. - - d, sregion, lregion: d is the fragment size, sregion is the - small region size, lregion is the large - region size - - pseudocount: a pseudocount used to calculate logLR, FE or - logFE. Please note this value will not be changed - with normalization method. So if you really want - to pseudocount: set 1 per million reads, it: set - after you normalize treat and control by million - reads by `change_normalizetion_method(ord('M'))`. - - """ - chr1: set - chr2: set - p: cython.float - - # decide PE mode - if isinstance(treat, FWTrack): - self.PE_mode = False - elif isinstance(treat, PETrackI): - self.PE_mode = True - else: - raise Exception("Should be FWTrack or PETrackI object!") - # decide if there is control - self.treat = treat - if ctrl: - self.ctrl = ctrl - else: # while there is no control - self.ctrl = treat - self.trackline = False - self.d = d # note, self.d doesn't make sense in PE mode - self.ctrl_d_s = ctrl_d_s # note, self.d doesn't make sense in PE mode - self.treat_scaling_factor = treat_scaling_factor - self.ctrl_scaling_factor_s = ctrl_scaling_factor_s - self.end_shift = end_shift - self.lambda_bg = lambda_bg - self.pqtable = Float32to32Map(for_int=False) # Float32 -> Float32 map - self.save_bedGraph = save_bedGraph - self.save_SPMR = save_SPMR - self.bedGraph_filename_prefix = bedGraph_filename_prefix.encode() - self.bedGraph_treat_filename = bedGraph_treat_filename.encode() - self.bedGraph_control_filename = bedGraph_control_filename.encode() - if not self.ctrl_d_s or not self.ctrl_scaling_factor_s: - self.no_lambda_flag = True - else: - self.no_lambda_flag = False - self.pseudocount = pseudocount - # get the common chromosome names from both treatment and control - chr1 = set(self.treat.get_chr_names()) - chr2 = set(self.ctrl.get_chr_names()) - self.chromosomes = sorted(list(chr1.intersection(chr2))) - - self.pileup_data_files = {} - self.pvalue_length = {} - self.pvalue_npeaks = {} - # step for optimal cutoff is 0.3 in -log10pvalue, we try from - # pvalue 1E-10 (-10logp=10) to 0.5 (-10logp=0.3) - for p in np.arange(0.3, 10, 0.3): - self.pvalue_length[p] = 0 - self.pvalue_npeaks[p] = 0 - self.optimal_p_cutoff = 0 - self.cutoff_analysis_filename = cutoff_analysis_filename.encode() - - @cython.ccall - def destroy(self): - """Remove temporary files for pileup values of each chromosome. - - Note: This function MUST be called if the class won: object't - be used anymore. - - """ - f: bytes - - for f in self.pileup_data_files.values(): - if os.path.isfile(f): - os.unlink(f) - return - - @cython.ccall - def set_pseudocount(self, pseudocount: cython.float): - self.pseudocount = pseudocount - - @cython.ccall - def enable_trackline(self): - """Turn on trackline with bedgraph output - """ - self.trackline = True - - @cython.cfunc - def pileup_treat_ctrl_a_chromosome(self, chrom: bytes): - """After this function is called, self.chr_pos_treat_ctrl will - be reand: set assigned to the pileup values of the given - chromosome. - - """ - treat_pv: list - ctrl_pv: list - f: object - temp_filename: str - - assert chrom in self.chromosomes, "chromosome %s is not valid." % chrom - - # check backup file of pileup values. If not exists, create - # it. Otherwise, load them instead of calculating new pileup - # values. - if chrom in self.pileup_data_files: - try: - f = open(self.pileup_data_files[chrom], "rb") - self.chr_pos_treat_ctrl = cPickle.load(f) - f.close() - return - except Exception: - temp_fd, temp_filename = mkstemp() - os.close(temp_fd) - self.pileup_data_files[chrom] = temp_filename - else: - temp_fd, temp_filename = mkstemp() - os.close(temp_fd) - self.pileup_data_files[chrom] = temp_filename.encode() - - # reor: set clean existing self.chr_pos_treat_ctrl - if self.chr_pos_treat_ctrl: # not a beautiful way to clean - clean_up_ndarray(self.chr_pos_treat_ctrl[0]) - clean_up_ndarray(self.chr_pos_treat_ctrl[1]) - clean_up_ndarray(self.chr_pos_treat_ctrl[2]) - - if self.PE_mode: - treat_pv = self.treat.pileup_a_chromosome(chrom, - [self.treat_scaling_factor,], - baseline_value=0.0) - else: - treat_pv = self.treat.pileup_a_chromosome(chrom, - [self.d,], - [self.treat_scaling_factor,], - baseline_value=0.0, - directional=True, - end_shift=self.end_shift) - - if not self.no_lambda_flag: - if self.PE_mode: - # note, we pileup up PE control as SE control because - # we assume the bias only can be captured at the - # surrounding regions of cutting sites from control experiments. - ctrl_pv = self.ctrl.pileup_a_chromosome_c(chrom, - self.ctrl_d_s, - self.ctrl_scaling_factor_s, - baseline_value=self.lambda_bg) - else: - ctrl_pv = self.ctrl.pileup_a_chromosome(chrom, - self.ctrl_d_s, - self.ctrl_scaling_factor_s, - baseline_value=self.lambda_bg, - directional=False) - else: - # a: set global lambda - ctrl_pv = [treat_pv[0][-1:], np.array([self.lambda_bg,], - dtype="f4")] - - self.chr_pos_treat_ctrl = self.__chrom_pair_treat_ctrl(treat_pv, - ctrl_pv) - - # clean treat_pv and ctrl_pv - treat_pv = [] - ctrl_pv = [] - - # save data to temporary file - try: - f = open(self.pileup_data_files[chrom], "wb") - cPickle.dump(self.chr_pos_treat_ctrl, f, protocol=2) - f.close() - except Exception: - # fail to write then remove the key in pileup_data_files - self.pileup_data_files.pop(chrom) - return - - @cython.cfunc - def __chrom_pair_treat_ctrl(self, treat_pv, ctrl_pv) -> list: - """*private* Pair treat and ctrl pileup for each region. - - treat_pv and ctrl_pv are [np.ndarray, np.ndarray]. - - return [p, t, c] list, each element is a numpy array. - """ - index_ret: cython.long - it: cython.long - ic: cython.long - lt: cython.long - lc: cython.long - t_p: cnp.ndarray(cython.int, ndim=1) - c_p: cnp.ndarray(cython.int, ndim=1) - ret_p: cnp.ndarray(cython.int, ndim=1) - t_v: cnp.ndarray(cython.float, ndim=1) - c_v: cnp.ndarray(cython.float, ndim=1) - ret_t: cnp.ndarray(cython.float, ndim=1) - ret_c: cnp.ndarray(cython.float, ndim=1) - t_p_view: cython.int [:] - # cython.pointer[cython.int] - c_p_view: cython.int [:] - ret_p_view: cython.int [:] - t_v_view: cython.float [:] # cython.pointer[cython.float] - c_v_view: cython.float [:] # cython.pointer[cython.float] - ret_t_view: cython.float [:] # cython.pointer[cython.float] - ret_c_view: cython.float [:] # cython.pointer[cython.float] - - [t_p, t_v] = treat_pv - [c_p, c_v] = ctrl_pv - - lt = t_p.shape[0] - lc = c_p.shape[0] - - chrom_max_len = lt + lc - - ret_p = np.zeros(chrom_max_len, dtype="i4") # position - ret_t = np.zeros(chrom_max_len, dtype="f4") # value from treatment - ret_c = np.zeros(chrom_max_len, dtype="f4") # value from control - - t_p_view = t_p #cython.cast(cython.pointer[cython.int], t_p.data) - t_v_view = t_v #cython.cast(cython.pointer[cython.float], t_v.data) - c_p_view = c_p #cython.cast(cython.pointer[cython.int], c_p.data) - c_v_view = c_v #cython.cast(cython.pointer[cython.float], c_v.data) - ret_p_view = ret_p #cython.cast(cython.pointer[cython.int], ret_p.data) - ret_t_view = ret_t #cython.cast(cython.pointer[cython.float], ret_t.data) - ret_c_view = ret_c #cython.cast(cython.pointer[cython.float], ret_c.data) - - index_ret = 0 - it = 0 - ic = 0 - - while it < lt and ic < lc: - if t_p_view[0] < c_p_view[0]: - # clip a region from pre_p to p1, then pre_p: set as p1. - ret_p_view[0] = t_p_view[0] - ret_t_view[0] = t_v_view[0] - ret_c_view[0] = c_v_view[0] - ret_p_view += 1 - ret_t_view += 1 - ret_c_view += 1 - index_ret += 1 - # call for the next p1 and v1 - it += 1 - t_p_view += 1 - t_v_view += 1 - elif t_p_view[0] > c_p_view[0]: - # clip a region from pre_p to p2, then pre_p: set as p2. - ret_p_view[0] = c_p_view[0] - ret_t_view[0] = t_v_view[0] - ret_c_view[0] = c_v_view[0] - ret_p_view += 1 - ret_t_view += 1 - ret_c_view += 1 - index_ret += 1 - # call for the next p2 and v2 - ic += 1 - c_p_view += 1 - c_v_view += 1 - else: - # from pre_p to p1 or p2, then pre_p: set as p1 or p2. - ret_p_view[0] = t_p_view[0] - ret_t_view[0] = t_v_view[0] - ret_c_view[0] = c_v_view[0] - ret_p_view += 1 - ret_t_view += 1 - ret_c_view += 1 - index_ret += 1 - # call for the next p1, v1, p2, v2. - it += 1 - ic += 1 - t_p_view += 1 - t_v_view += 1 - c_p_view += 1 - c_v_view += 1 - - ret_p.resize(index_ret, refcheck=False) - ret_t.resize(index_ret, refcheck=False) - ret_c.resize(index_ret, refcheck=False) - return [ret_p, ret_t, ret_c] - - @cython.cfunc - def __cal_score(self, - array1: cnp.ndarray(cython.float, ndim=1), - array2: cnp.ndarray(cython.float, ndim=1), - cal_func) -> cnp.ndarray: - i: cython.long - s: cnp.ndarray(cython.float, ndim=1) - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="f4") - for i in range(array1.shape[0]): - s[i] = cal_func(array1[i], array2[i]) - return s - - @cython.cfunc - def __cal_pvalue_qvalue_table(self): - """After this function is called, self.pqtable is built. All - chromosomes will be iterated. So it will take some time. - - """ - chrom: bytes - pos_array: cnp.ndarray - treat_array: cnp.ndarray - ctrl_array: cnp.ndarray - pscore_stat: dict - pre_p: cython.long - # pre_l: cython.long - l: cython.long - i: cython.long - j: cython.long - this_v: cython.float - # pre_v: cython.float - v: cython.float - q: cython.float - pre_q: cython.float - N: cython.long - k: cython.long - this_l: cython.long - f: cython.float - unique_values: list - pos_view: cython.int [:] # cython.pointer[cython.int] - treat_value_view: cython.float [:] # cython.pointer[cython.float] - ctrl_value_view: cython.float [:] # cython.pointer[cython.float] - - debug("Start to calculate pvalue stat...") - - pscore_stat = {} # dict() - for i in range(len(self.chromosomes)): - chrom = self.chromosomes[i] - pre_p = 0 - - self.pileup_treat_ctrl_a_chromosome(chrom) - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - - pos_view = pos_array - treat_value_view = treat_array - ctrl_value_view = ctr_array - - for j in range(pos_array.shape[0]): - this_v = get_pscore((cython.cast(cython.int, - treat_value_view[0]), - ctrl_value_view[0])) - this_l = pos_view[0] - pre_p - if this_v in pscore_stat: - pscore_stat[this_v] += this_l - else: - pscore_stat[this_v] = this_l - pre_p = pos_view[0] - pos_view += 1 - treat_value_view += 1 - ctrl_value_view += 1 - - N = sum(pscore_stat.values()) # total length - k = 1 # rank - f = -log10(N) - # pre_v = -2147483647 - # pre_l = 0 - pre_q = 2147483647 # save the previous q-value - - self.pqtable = Float32to32Map(for_int=False) - unique_values = sorted(list(pscore_stat.keys()), reverse=True) - for i in range(len(unique_values)): - v = unique_values[i] - l = pscore_stat[v] - q = v + (log10(k) + f) - if q > pre_q: - q = pre_q - if q <= 0: - q = 0 - break - #q = max(0,min(pre_q,q)) # make q-score monotonic - self.pqtable[v] = q - pre_q = q - k += l - # bottom rank pscores all have qscores 0 - for j in range(i, len(unique_values)): - v = unique_values[j] - self.pqtable[v] = 0 - return - - @cython.cfunc - def __pre_computes(self, - max_gap: cython.int = 50, - min_length: cython.int = 200): - """After this function is called, self.pqtable and self.pvalue_length is built. All - chromosomes will be iterated. So it will take some time. - - """ - chrom: bytes - pos_array: cnp.ndarray - treat_array: cnp.ndarray - ctrl_array: cnp.ndarray - score_array: cnp.ndarray - pscore_stat: dict - n: cython.long - pre_p: cython.long - this_p: cython.long - j: cython.long - l: cython.long - i: cython.long - q: cython.float - pre_q: cython.float - this_v: cython.float - v: cython.float - cutoff: cython.float - N: cython.long - k: cython.long - this_l: cython.long - f: cython.float - unique_values: list - above_cutoff: cnp.ndarray - above_cutoff_endpos: cnp.ndarray - above_cutoff_startpos: cnp.ndarray - peak_content: list - peak_length: cython.long - total_l: cython.long - total_p: cython.long - tmplist: list - - # above cutoff start position pointer - acs_ptr: cython.pointer(cython.int) - # above cutoff end position pointer - ace_ptr: cython.pointer(cython.int) - # position array pointer - pos_array_ptr: cython.pointer(cython.int) - # score array pointer - score_array_ptr: cython.pointer(cython.float) - - debug("Start to calculate pvalue stat...") - - # tmpcontains: list a of: list log pvalue cutoffs from 0.3 to 10 - tmplist = [round(x, 5) - for x in sorted(list(np.arange(0.3, 10.0, 0.3)), - reverse=True)] - - pscore_stat = {} # dict() - # print (list(pscore_stat.keys())) - # print (list(self.pvalue_length.keys())) - # print (list(self.pvalue_npeaks.keys())) - for i in range(len(self.chromosomes)): - chrom = self.chromosomes[i] - self.pileup_treat_ctrl_a_chromosome(chrom) - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - - score_array = self.__cal_pscore(treat_array, ctrl_array) - - for n in range(len(tmplist)): - cutoff = tmplist[n] - total_l = 0 # total length in potential peak - total_p = 0 - - # get the regions with scores above cutoffs this is - # not an optimized method. It would be better to store - # score array in a 2-D ndarray? - above_cutoff = np.nonzero(score_array > cutoff)[0] - # end positions of regions where score is above cutoff - above_cutoff_endpos = pos_array[above_cutoff] - # start positions of regions where score is above cutoff - above_cutoff_startpos = pos_array[above_cutoff-1] - - if above_cutoff_endpos.size == 0: - continue - - # first bit of region above cutoff - acs_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_startpos.data) - ace_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_endpos.data) - - peak_content = [(acs_ptr[0], ace_ptr[0]),] - lastp = ace_ptr[0] - acs_ptr += 1 - ace_ptr += 1 - - for i in range(1, above_cutoff_startpos.size): - tl = acs_ptr[0] - lastp - if tl <= max_gap: - peak_content.append((acs_ptr[0], ace_ptr[0])) - else: - peak_length = peak_content[-1][1] - peak_content[0][0] - # if the peak is too small, reject it - if peak_length >= min_length: - total_l += peak_length - total_p += 1 - peak_content = [(acs_ptr[0], ace_ptr[0]),] - lastp = ace_ptr[0] - acs_ptr += 1 - ace_ptr += 1 - - if peak_content: - peak_length = peak_content[-1][1] - peak_content[0][0] - # if the peak is too small, reject it - if peak_length >= min_length: - total_l += peak_length - total_p += 1 - self.pvalue_length[cutoff] = self.pvalue_length.get(cutoff, 0) + total_l - self.pvalue_npeaks[cutoff] = self.pvalue_npeaks.get(cutoff, 0) + total_p - - pos_array_ptr = cython.cast(cython.pointer(cython.int), - pos_array.data) - score_array_ptr = cython.cast(cython.pointer(cython.float), - score_array.data) - - pre_p = 0 - for i in range(pos_array.shape[0]): - this_p = pos_array_ptr[0] - this_l = this_p - pre_p - this_v = score_array_ptr[0] - if this_v in pscore_stat: - pscore_stat[this_v] += this_l - else: - pscore_stat[this_v] = this_l - pre_p = this_p # pos_array[i] - pos_array_ptr += 1 - score_array_ptr += 1 - - # debug ("make pscore_stat cost %.5f seconds" % t) - - # add all pvalue cutoffs from cutoff-analysis part. So that we - # can get the corresponding qvalues for them. - for cutoff in tmplist: - if cutoff not in pscore_stat: - pscore_stat[cutoff] = 0 - - N = sum(pscore_stat.values()) # total length - k = 1 # rank - f = -log10(N) - pre_q = 2147483647 # save the previous q-value - - self.pqtable = Float32to32Map(for_int=False) # {} - # sorted(unique_values,reverse=True) - unique_values = sorted(list(pscore_stat.keys()), reverse=True) - for i in range(len(unique_values)): - v = unique_values[i] - l = pscore_stat[v] - q = v + (log10(k) + f) - if q > pre_q: - q = pre_q - if q <= 0: - q = 0 - break - # q = max(0,min(pre_q,q)) # make q-score monotonic - self.pqtable[v] = q - pre_q = q - k += l - for j in range(i, len(unique_values)): - v = unique_values[j] - self.pqtable[v] = 0 - - # write pvalue and total length of predicted peaks - # this is the output from cutoff-analysis - fhd = open(self.cutoff_analysis_filename, "w") - fhd.write("pscore\tqscore\tnpeaks\tlpeaks\tavelpeak\n") - x = [] - y = [] - for cutoff in tmplist: - if self.pvalue_npeaks[cutoff] > 0: - fhd.write("%.2f\t%.2f\t%d\t%d\t%.2f\n" % - (cutoff, self.pqtable[cutoff], - self.pvalue_npeaks[cutoff], - self.pvalue_length[cutoff], - self.pvalue_length[cutoff]/self.pvalue_npeaks[cutoff])) - x.append(cutoff) - y.append(self.pvalue_length[cutoff]) - fhd.close() - info("#3 Analysis of cutoff vs num of peaks or total length has been saved in %s" % self.cutoff_analysis_filename) - # info("#3 Suggest a cutoff...") - # optimal_cutoff, optimal_length = find_optimal_cutoff(x, y) - # info("#3 -10log10pvalue cutoff %.2f will call approximately %.0f bps regions as significant regions" % (optimal_cutoff, optimal_length)) - # print (list(pqtable.keys())) - # print (list(self.pvalue_length.keys())) - # print (list(self.pvalue_npeaks.keys())) - return - - @cython.ccall - def call_peaks(self, - scoring_function_symbols: list, - score_cutoff_s: list, - min_length: cython.int = 200, - max_gap: cython.int = 50, - call_summits: bool = False, - cutoff_analysis: bool = False): - """Call peaks for all chromosomes. Return a PeakIO object. - - scoring_function_s: symbols of functions to calculate score. 'p' for pscore, 'q' for qscore, 'f' for fold change, 's' for subtraction. for example: ['p', 'q'] - score_cutoff_s : cutoff values corresponding to scoring functions - min_length : minimum length of peak - max_gap : maximum gap of 'insignificant' regions within a peak. Note, for PE_mode, max_gap and max_length are both as: set fragment length. - call_summits : boolean. Whether or not call sub-peaks. - save_bedGraph : whether or not to save pileup and control into a bedGraph file - """ - chrom: bytes - tmp_bytes: bytes - - peaks = PeakIO() - - # prepare p-q table - if len(self.pqtable) == 0: - info("#3 Pre-compute pvalue-qvalue table...") - if cutoff_analysis: - info("#3 Cutoff vs peaks called will be analyzed!") - self.__pre_computes(max_gap=max_gap, min_length=min_length) - else: - self.__cal_pvalue_qvalue_table() - - - # prepare bedGraph file - if self.save_bedGraph: - self.bedGraph_treat_f = fopen(self.bedGraph_treat_filename, "w") - self.bedGraph_ctrl_f = fopen(self.bedGraph_control_filename, "w") - - info("#3 In the peak calling step, the following will be performed simultaneously:") - info("#3 Write bedGraph files for treatment pileup (after scaling if necessary)... %s" % - self.bedGraph_filename_prefix.decode() + "_treat_pileup.bdg") - info("#3 Write bedGraph files for control lambda (after scaling if necessary)... %s" % - self.bedGraph_filename_prefix.decode() + "_control_lambda.bdg") - - if self.save_SPMR: - info("#3 --SPMR is requested, so pileup will be normalized by sequencing depth in million reads.") - elif self.treat_scaling_factor == 1: - info("#3 Pileup will be based on sequencing depth in treatment.") - else: - info("#3 Pileup will be based on sequencing depth in control.") - - if self.trackline: - # this line is REQUIRED by the wiggle format for UCSC browser - tmp_bytes = ("track type=bedGraph name=\"treatment pileup\" description=\"treatment pileup after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() - fprintf(self.bedGraph_treat_f, tmp_bytes) - tmp_bytes = ("track type=bedGraph name=\"control lambda\" description=\"control lambda after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() - fprintf(self.bedGraph_ctrl_f, tmp_bytes) - - info("#3 Call peaks for each chromosome...") - for chrom in self.chromosomes: - # treat/control bedGraph will be saved if requested by user. - self.__chrom_call_peak_using_certain_criteria(peaks, - chrom, - scoring_function_symbols, - score_cutoff_s, - min_length, - max_gap, - call_summits, - self.save_bedGraph) - - # close bedGraph file - if self.save_bedGraph: - fclose(self.bedGraph_treat_f) - fclose(self.bedGraph_ctrl_f) - self.save_bedGraph = False - - return peaks - - @cython.cfunc - def __chrom_call_peak_using_certain_criteria(self, - peaks, - chrom: bytes, - scoring_function_s: list, - score_cutoff_s: list, - min_length: cython.int, - max_gap: cython.int, - call_summits: bool, - save_bedGraph: bool): - """ Call peaks for a chromosome. - - Combination of criteria is allowed here. - - peaks: a PeakIO object, the return value of this function - scoring_function_s: symbols of functions to calculate score as score=f(x, y) where x is treatment pileup, and y is control pileup - save_bedGraph : whether or not to save pileup and control into a bedGraph file - """ - i: cython.int - s: str - above_cutoff: cnp.ndarray - above_cutoff_endpos: cnp.ndarray(cython.int, ndim=1) - above_cutoff_startpos: cnp.ndarray(cython.int, ndim=1) - pos_array: cnp.ndarray(cython.int, ndim=1) - above_cutoff_index_array: cnp.ndarray(cython.int, ndim=1) - treat_array: cnp.ndarray(cython.float, ndim=1) - ctrl_array: cnp.ndarray(cython.float, ndim=1) - score_array_s: list # to: list keep different types of scores - peak_content: list # to store information for a - # chunk in a peak region, it - # contains lists of: 1. left - # position; 2. right - # position; 3. treatment - # value; 4. control value; - # 5. of: list scores at this - # chunk - tl: cython.long - lastp: cython.long - ts: cython.long - te: cython.long - ti: cython.long - tp: cython.float - cp: cython.float - acs_ptr: cython.pointer(cython.int) - ace_ptr: cython.pointer(cython.int) - acia_ptr: cython.pointer(cython.int) - treat_array_ptr: cython.pointer(cython.float) - ctrl_array_ptr: cython.pointer(cython.float) - - assert len(scoring_function_s) == len(score_cutoff_s), "number of functions and cutoffs should be the same!" - - peak_content = [] # to store points above cutoff - - # first, build pileup, self.chr_pos_treat_ctrl - # this step will be speeped up if pqtable is pre-computed. - self.pileup_treat_ctrl_a_chromosome(chrom) - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - - # while save_bedGraph is true, invoke __write_bedGraph_for_a_chromosome - if save_bedGraph: - self.__write_bedGraph_for_a_chromosome(chrom) - - # keep all types of scores needed - # t0 = ttime() - score_array_s = [] - for i in range(len(scoring_function_s)): - s = scoring_function_s[i] - if s == 'p': - score_array_s.append(self.__cal_pscore(treat_array, - ctrl_array)) - elif s == 'q': - score_array_s.append(self.__cal_qscore(treat_array, - ctrl_array)) - elif s == 'f': - score_array_s.append(self.__cal_FE(treat_array, - ctrl_array)) - elif s == 's': - score_array_s.append(self.__cal_subtraction(treat_array, - ctrl_array)) - - # get the regions with scores above cutoffs. this is not an - # optimized method. It would be better to store score array in - # a 2-D ndarray? - above_cutoff = np.nonzero(apply_multiple_cutoffs(score_array_s, - score_cutoff_s))[0] - # indices - above_cutoff_index_array = np.arange(pos_array.shape[0], - dtype="i4")[above_cutoff] - # end positions of regions where score is above cutoff - above_cutoff_endpos = pos_array[above_cutoff] - # start positions of regions where score is above cutoff - above_cutoff_startpos = pos_array[above_cutoff-1] - - if above_cutoff.size == 0: - # nothing above cutoff - return - - if above_cutoff[0] == 0: - # first element > cutoff, fix the first point as - # 0. otherwise it would be the last item in - # data[chrom]['pos'] - above_cutoff_startpos[0] = 0 - - #print "apply cutoff -- chrom:",chrom," time:", ttime() - t0 - # start to build peak regions - #t0 = ttime() - - # first bit of region above cutoff - acs_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_startpos.data) - ace_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_endpos.data) - acia_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_index_array.data) - treat_array_ptr = cython.cast(cython.pointer(cython.float), - treat_array.data) - ctrl_array_ptr = cython.cast(cython.pointer(cython.float), - ctrl_array.data) - - ts = acs_ptr[0] - te = ace_ptr[0] - ti = acia_ptr[0] - tp = treat_array_ptr[ti] - cp = ctrl_array_ptr[ti] - - peak_content.append((ts, te, tp, cp, ti)) - lastp = te - acs_ptr += 1 - ace_ptr += 1 - acia_ptr += 1 - - for i in range(1, above_cutoff_startpos.shape[0]): - ts = acs_ptr[0] - te = ace_ptr[0] - ti = acia_ptr[0] - acs_ptr += 1 - ace_ptr += 1 - acia_ptr += 1 - tp = treat_array_ptr[ti] - cp = ctrl_array_ptr[ti] - tl = ts - lastp - if tl <= max_gap: - # append. - peak_content.append((ts, te, tp, cp, ti)) - lastp = te # above_cutoff_endpos[i] - else: - # close - if call_summits: - # smooth length is min_length, i.e. fragment size 'd' - self.__close_peak_with_subpeaks(peak_content, - peaks, - min_length, - chrom, - min_length, - score_array_s, - score_cutoff_s=score_cutoff_s) - else: - # smooth length is min_length, i.e. fragment size 'd' - self.__close_peak_wo_subpeaks(peak_content, - peaks, - min_length, - chrom, - min_length, - score_array_s, - score_cutoff_s=score_cutoff_s) - peak_content = [(ts, te, tp, cp, ti),] - lastp = te # above_cutoff_endpos[i] - # save the last peak - if not peak_content: - return - else: - if call_summits: - # smooth length is min_length, i.e. fragment size 'd' - self.__close_peak_with_subpeaks(peak_content, - peaks, - min_length, - chrom, - min_length, - score_array_s, - score_cutoff_s=score_cutoff_s) - else: - # smooth length is min_length, i.e. fragment size 'd' - self.__close_peak_wo_subpeaks(peak_content, - peaks, - min_length, - chrom, - min_length, - score_array_s, - score_cutoff_s=score_cutoff_s) - - # print "close peaks -- chrom:",chrom," time:", ttime() - t0 - return - - @cython.cfunc - def __close_peak_wo_subpeaks(self, - peak_content: list, - peaks, - min_length: cython.int, - chrom: bytes, - smoothlen: cython.int, - score_array_s: list, - score_cutoff_s: list = []) -> bool: - """Close the peak region, output peak boundaries, peak summit - and scores, then add the peak to peakIO object. - - peak_content contains [start, end, treat_p, ctrl_p, index_in_score_array] - - peaks: a PeakIO object - - """ - summit_pos: cython.int - tstart: cython.int - tend: cython.int - summit_index: cython.int - i: cython.int - midindex: cython.int - ttreat_p: cython.double - tctrl_p: cython.double - tscore: cython.double - summit_treat: cython.double - summit_ctrl: cython.double - summit_p_score: cython.double - summit_q_score: cython.double - tlist_scores_p: cython.int - - peak_length = peak_content[-1][1] - peak_content[0][0] - if peak_length >= min_length: # if the peak is too small, reject it - tsummit = [] - summit_pos = 0 - summit_value = 0 - for i in range(len(peak_content)): - (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] - tscore = ttreat_p # use pscore as general score to find summit - if not summit_value or summit_value < tscore: - tsummit = [(tend + tstart) // 2,] - tsummit_index = [i,] - summit_value = tscore - elif summit_value == tscore: - # remember continuous summit values - tsummit.append((tend + tstart) // 2) - tsummit_index.append(i) - # the middle of all highest points in peak region is defined as summit - midindex = (len(tsummit) + 1) // 2 - 1 - summit_pos = tsummit[midindex] - summit_index = tsummit_index[midindex] - - summit_treat = peak_content[summit_index][2] - summit_ctrl = peak_content[summit_index][3] - - # this is a double-check to see if the summit can pass cutoff values. - for i in range(len(score_cutoff_s)): - if score_cutoff_s[i] > score_array_s[i][peak_content[summit_index][4]]: - return False # not passed, then disgard this peak. - - summit_p_score = pscore_dict[(cython.cast(cython.int, - summit_treat), - summit_ctrl)] - summit_q_score = self.pqtable[summit_p_score] - - peaks.add(chrom, # chromosome - peak_content[0][0], # start - peak_content[-1][1], # end - summit=summit_pos, # summit position - peak_score=summit_q_score, # score at summit - pileup=summit_treat, # pileup - pscore=summit_p_score, # pvalue - fold_change=(summit_treat + self.pseudocount) / (summit_ctrl + self.pseudocount), # fold change - qscore=summit_q_score # qvalue - ) - # start a new peak - return True - - @cython.cfunc - def __close_peak_with_subpeaks(self, - peak_content: list, - peaks, - min_length: cython.int, - chrom: bytes, - smoothlen: cython.int, - score_array_s: list, - score_cutoff_s: list = [], - min_valley: cython.float = 0.9) -> bool: - """Algorithm implemented by Ben, to profile the pileup signals - within a peak region then find subpeak summits. This method is - highly recommended for TFBS or DNAase I sites. - - """ - tstart: cython.int - tend: cython.int - summit_index: cython.int - summit_offset: cython.int - start: cython.int - end: cython.int - i: cython.int - start_boundary: cython.int - m: cython.int - n: cython.int - ttreat_p: cython.double - tctrl_p: cython.double - tscore: cython.double - summit_treat: cython.double - summit_ctrl: cython.double - summit_p_score: cython.double - summit_q_score: cython.double - peakdata: cnp.ndarray(cython.float, ndim=1) - peakindices: cnp.ndarray(cython.int, ndim=1) - summit_offsets: cnp.ndarray(cython.int, ndim=1) - tlist_scores_p: cython.int - - peak_length = peak_content[-1][1] - peak_content[0][0] - - if peak_length < min_length: - return # if the region is too small, reject it - - # Add 10 bp padding to peak region so that we can get true minima - end = peak_content[-1][1] + 10 - start = peak_content[0][0] - 10 - if start < 0: - # this is the offof: set original peak boundary in peakdata list. - start_boundary = 10 + start - start = 0 - else: - # this is the offof: set original peak boundary in peakdata list. - start_boundary = 10 - - # save the scores (qscore) for each position in this region - peakdata = np.zeros(end - start, dtype='f4') - # save the indices for each position in this region - peakindices = np.zeros(end - start, dtype='i4') - for i in range(len(peak_content)): - (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] - tscore = ttreat_p # use pileup as general score to find summit - m = tstart - start + start_boundary - n = tend - start + start_boundary - peakdata[m:n] = tscore - peakindices[m:n] = i - - # offsets are the indices for summits in peakdata/peakindices array. - summit_offsets = maxima(peakdata, smoothlen) - - if summit_offsets.shape[0] == 0: - # **failsafe** if no summits, fall back on old approach # - return self.__close_peak_wo_subpeaks(peak_content, - peaks, - min_length, - chrom, - smoothlen, - score_array_s, - score_cutoff_s) - else: - # remove maxima that occurred in padding - m = np.searchsorted(summit_offsets, - start_boundary) - n = np.searchsorted(summit_offsets, - peak_length + start_boundary, - 'right') - summit_offsets = summit_offsets[m:n] - - summit_offsets = enforce_peakyness(peakdata, summit_offsets) - - # print "enforced:",summit_offsets - if summit_offsets.shape[0] == 0: - # **failsafe** if no summits, fall back on old approach # - return self.__close_peak_wo_subpeaks(peak_content, - peaks, - min_length, - chrom, - smoothlen, - score_array_s, - score_cutoff_s) - - # indices are those point to peak_content - summit_indices = peakindices[summit_offsets] - - summit_offsets -= start_boundary - - for summit_offset, summit_index in list(zip(summit_offsets, - summit_indices)): - - summit_treat = peak_content[summit_index][2] - summit_ctrl = peak_content[summit_index][3] - - summit_p_score = pscore_dict[(cython.cast(cython.int, - summit_treat), - summit_ctrl)] - summit_q_score = self.pqtable[summit_p_score] - - for i in range(len(score_cutoff_s)): - if score_cutoff_s[i] > score_array_s[i][peak_content[summit_index][4]]: - return False # not passed, then disgard this summit. - - peaks.add(chrom, - peak_content[0][0], - peak_content[-1][1], - summit=start + summit_offset, - peak_score=summit_q_score, - pileup=summit_treat, - pscore=summit_p_score, - fold_change=(summit_treat + self.pseudocount) / (summit_ctrl + self.pseudocount), # fold change - qscore=summit_q_score - ) - # start a new peak - return True - - @cython.cfunc - def __cal_pscore(self, - array1: cnp.ndarray(cython.float, ndim=1), - array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: - - i: cython.long - array1_size: cython.long - s: cnp.ndarray(cython.float, ndim=1) - a1_ptr: cython.pointer(cython.float) - a2_ptr: cython.pointer(cython.float) - s_ptr: cython.pointer(cython.float) - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="f4") - - a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) - a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) - s_ptr = cython.cast(cython.pointer(cython.float), s.data) - - array1_size = array1.shape[0] - - for i in range(array1_size): - s_ptr[0] = get_pscore((cython.cast(cython.int, - a1_ptr[0]), - a2_ptr[0])) - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - @cython.cfunc - def __cal_qscore(self, - array1: cnp.ndarray(cython.float, ndim=1), - array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: - i: cython.long - s: cnp.ndarray(cython.float, ndim=1) - a1_ptr: cython.pointer(cython.float) - a2_ptr: cython.pointer(cython.float) - s_ptr: cython.pointer(cython.float) - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="f4") - - a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) - a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) - s_ptr = cython.cast(cython.pointer(cython.float), s.data) - - for i in range(array1.shape[0]): - s_ptr[0] = self.pqtable[get_pscore((cython.cast(cython.int, - a1_ptr[0]), - a2_ptr[0]))] - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - @cython.cfunc - def __cal_logLR(self, - array1: cnp.ndarray(cython.float, ndim=1), - array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: - i: cython.long - s: cnp.ndarray(cython.float, ndim=1) - a1_ptr: cython.pointer(cython.float) - a2_ptr: cython.pointer(cython.float) - s_ptr: cython.pointer(cython.float) - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="f4") - - a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) - a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) - s_ptr = cython.cast(cython.pointer(cython.float), s.data) - - for i in range(array1.shape[0]): - s_ptr[0] = get_logLR_asym((a1_ptr[0] + self.pseudocount, - a2_ptr[0] + self.pseudocount)) - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - @cython.cfunc - def __cal_logFE(self, - array1: cnp.ndarray(cython.float, ndim=1), - array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: - i: cython.long - s: cnp.ndarray(cython.float, ndim=1) - a1_ptr: cython.pointer(cython.float) - a2_ptr: cython.pointer(cython.float) - s_ptr: cython.pointer(cython.float) - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="f4") - - a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) - a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) - s_ptr = cython.cast(cython.pointer(cython.float), s.data) - - for i in range(array1.shape[0]): - s_ptr[0] = get_logFE(a1_ptr[0] + self.pseudocount, - a2_ptr[0] + self.pseudocount) - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - @cython.cfunc - def __cal_FE(self, - array1: cnp.ndarray(cython.float, ndim=1), - array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: - i: cython.long - s: cnp.ndarray(cython.float, ndim=1) - a1_ptr: cython.pointer(cython.float) - a2_ptr: cython.pointer(cython.float) - s_ptr: cython.pointer(cython.float) - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="f4") - - a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) - a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) - s_ptr = cython.cast(cython.pointer(cython.float), s.data) - - for i in range(array1.shape[0]): - s_ptr[0] = (a1_ptr[0] + self.pseudocount) / (a2_ptr[0] + self.pseudocount) - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - @cython.cfunc - def __cal_subtraction(self, - array1: cnp.ndarray(cython.float, ndim=1), - array2: cnp.ndarray(cython.float, ndim=1)) -> cnp.ndarray: - i: cython.long - s: cnp.ndarray(cython.float, ndim=1) - a1_ptr: cython.pointer(cython.float) - a2_ptr: cython.pointer(cython.float) - s_ptr: cython.pointer(cython.float) - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="f4") - - a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) - a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) - s_ptr = cython.cast(cython.pointer(cython.float), s.data) - - for i in range(array1.shape[0]): - s_ptr[0] = a1_ptr[0] - a2_ptr[0] - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - @cython.cfunc - def __write_bedGraph_for_a_chromosome(self, chrom: bytes) -> bool: - """Write treat/control values for a certain chromosome into a - specified file handler. - - """ - pos_array: cnp.ndarray(cython.int, ndim=1) - treat_array: cnp.ndarray(cython.int, ndim=1) - ctrl_array: cnp.ndarray(cython.int, ndim=1) - pos_array_ptr: cython.pointer(cython.int) - treat_array_ptr: cython.pointer(cython.float) - ctrl_array_ptr: cython.pointer(cython.float) - l: cython.int - i: cython.int - p: cython.int - pre_p_t: cython.int - # current position, previous position for treat, previous position for control - pre_p_c: cython.int - pre_v_t: cython.float - pre_v_c: cython.float - v_t: cython.float - # previous value for treat, for control, current value for treat, for control - v_c: cython.float - # 1 if save_SPMR is false, or depth in million if save_SPMR is - # true. Note, while piling up and calling peaks, treatment and - # control have been scaled to the same depth, so we need to - # find what this 'depth' is. - denominator: cython.float - ft: cython.pointer(FILE) - fc: cython.pointer(FILE) - - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - pos_array_ptr = cython.cast(cython.pointer(cython.int), - pos_array.data) - treat_array_ptr = cython.cast(cython.pointer(cython.float), - treat_array.data) - ctrl_array_ptr = cython.cast(cython.pointer(cython.float), - ctrl_array.data) - - if self.save_SPMR: - if self.treat_scaling_factor == 1: - # in this case, control has been asked to be scaled to depth of treatment - denominator = self.treat.total/1e6 - else: - # in this case, treatment has been asked to be scaled to depth of control - denominator = self.ctrl.total/1e6 - else: - denominator = 1.0 - - l = pos_array.shape[0] - - if l == 0: # if there is no data, return - return False - - ft = self.bedGraph_treat_f - fc = self.bedGraph_ctrl_f - # t_write_func = self.bedGraph_treat.write - # c_write_func = self.bedGraph_ctrl.write - - pre_p_t = 0 - pre_p_c = 0 - pre_v_t = treat_array_ptr[0]/denominator - pre_v_c = ctrl_array_ptr[0]/denominator - treat_array_ptr += 1 - ctrl_array_ptr += 1 - - for i in range(1, l): - v_t = treat_array_ptr[0]/denominator - v_c = ctrl_array_ptr[0]/denominator - p = pos_array_ptr[0] - pos_array_ptr += 1 - treat_array_ptr += 1 - ctrl_array_ptr += 1 - - if abs(pre_v_t - v_t) > 1e-5: # precision is 5 digits - fprintf(ft, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_t, p, pre_v_t) - pre_v_t = v_t - pre_p_t = p - - if abs(pre_v_c - v_c) > 1e-5: # precision is 5 digits - fprintf(fc, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_c, p, pre_v_c) - pre_v_c = v_c - pre_p_c = p - - p = pos_array_ptr[0] - # last one - fprintf(ft, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_t, p, pre_v_t) - fprintf(fc, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_c, p, pre_v_c) - - return True - - @cython.ccall - def call_broadpeaks(self, - scoring_function_symbols: list, - lvl1_cutoff_s: list, - lvl2_cutoff_s: list, - min_length: cython.int = 200, - lvl1_max_gap: cython.int = 50, - lvl2_max_gap: cython.int = 400, - cutoff_analysis: bool = False): - """This function try to find enriched regions within which, - scores are continuously higher than a given cutoff for level - 1, and link them using the gap above level 2 cutoff with a - maximum length of lvl2_max_gap. - - scoring_function_s: symbols of functions to calculate - score. 'p' for pscore, 'q' for qscore, 'f' for fold change, - 's' for subtraction. for example: ['p', 'q'] - - lvl1_cutoff_s: of: list cutoffs at highly enriched regions, - corresponding to scoring functions. - - lvl2_cutoff_s: of: list cutoffs at less enriched regions, - corresponding to scoring functions. - - min_length : minimum peak length, default 200. - - lvl1_max_gap : maximum gap to merge nearby enriched peaks, - default 50. - - lvl2_max_gap : maximum length of linkage regions, default 400. - - Return both general PeakIO for: object highly enriched regions - and gapped broad regions in BroadPeakIO. - - """ - i: cython.int - j: cython.int - chrom: bytes - lvl1peaks: object - lvl1peakschrom: object - lvl1: object - lvl2peaks: object - lvl2peakschrom: object - lvl2: object - broadpeaks: object - chrs: set - tmppeakset: list - - lvl1peaks = PeakIO() - lvl2peaks = PeakIO() - - # prepare p-q table - if len(self.pqtable) == 0: - info("#3 Pre-compute pvalue-qvalue table...") - if cutoff_analysis: - info("#3 Cutoff value vs broad region calls will be analyzed!") - self.__pre_computes(max_gap=lvl2_max_gap, min_length=min_length) - else: - self.__cal_pvalue_qvalue_table() - - # prepare bedGraph file - if self.save_bedGraph: - - self.bedGraph_treat_f = fopen(self.bedGraph_treat_filename, "w") - self.bedGraph_ctrl_f = fopen(self.bedGraph_control_filename, "w") - info("#3 In the peak calling step, the following will be performed simultaneously:") - info("#3 Write bedGraph files for treatment pileup (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_treat_pileup.bdg") - info("#3 Write bedGraph files for control lambda (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_control_lambda.bdg") - - if self.trackline: - # this line is REQUIRED by the wiggle format for UCSC browser - tmp_bytes = ("track type=bedGraph name=\"treatment pileup\" description=\"treatment pileup after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() - fprintf(self.bedGraph_treat_f, tmp_bytes) - tmp_bytes = ("track type=bedGraph name=\"control lambda\" description=\"control lambda after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() - fprintf(self.bedGraph_ctrl_f, tmp_bytes) - - info("#3 Call peaks for each chromosome...") - for chrom in self.chromosomes: - self.__chrom_call_broadpeak_using_certain_criteria(lvl1peaks, - lvl2peaks, - chrom, - scoring_function_symbols, - lvl1_cutoff_s, - lvl2_cutoff_s, - min_length, - lvl1_max_gap, - lvl2_max_gap, - self.save_bedGraph) - - # close bedGraph file - if self.save_bedGraph: - fclose(self.bedGraph_treat_f) - fclose(self.bedGraph_ctrl_f) - # self.bedGraph_ctrl.close() - self.save_bedGraph = False - - # now combine lvl1 and lvl2 peaks - chrs = lvl1peaks.get_chr_names() - broadpeaks = BroadPeakIO() - # use lvl2_peaks as linking regions between lvl1_peaks - for chrom in sorted(chrs): - lvl1peakschrom = lvl1peaks.get_data_from_chrom(chrom) - lvl2peakschrom = lvl2peaks.get_data_from_chrom(chrom) - lvl1peakschrom_next = iter(lvl1peakschrom).__next__ - tmppeakset = [] # to temporarily store lvl1 region inside a lvl2 region - # our assumption is lvl1 regions should be included in lvl2 regions - try: - lvl1 = lvl1peakschrom_next() - for i in range(len(lvl2peakschrom)): - # for each lvl2 peak, find all lvl1 peaks inside - # I assume lvl1 peaks can be ALL covered by lvl2 peaks. - lvl2 = lvl2peakschrom[i] - - while True: - if lvl2["start"] <= lvl1["start"] and lvl1["end"] <= lvl2["end"]: - tmppeakset.append(lvl1) - lvl1 = lvl1peakschrom_next() - else: - # make a hierarchical broad peak - #print lvl2["start"], lvl2["end"], lvl2["score"] - self.__add_broadpeak(broadpeaks, - chrom, - lvl2, - tmppeakset) - tmppeakset = [] - break - except StopIteration: - # no more strong (aka lvl1) peaks left - self.__add_broadpeak(broadpeaks, - chrom, - lvl2, - tmppeakset) - tmppeakset = [] - # add the rest lvl2 peaks - for j in range(i+1, len(lvl2peakschrom)): - self.__add_broadpeak(broadpeaks, - chrom, - lvl2peakschrom[j], - tmppeakset) - - return broadpeaks - - @cython.cfunc - def __chrom_call_broadpeak_using_certain_criteria(self, - lvl1peaks, - lvl2peaks, - chrom: bytes, - scoring_function_s: list, - lvl1_cutoff_s: list, - lvl2_cutoff_s: list, - min_length: cython.int, - lvl1_max_gap: cython.int, - lvl2_max_gap: cython.int, - save_bedGraph: bool): - """Call peaks for a chromosome. - - Combination of criteria is allowed here. - - peaks: a PeakIO object - - scoring_function_s: symbols of functions to calculate score as - score=f(x, y) where x is treatment pileup, and y is control - pileup - - save_bedGraph : whether or not to save pileup and control into - a bedGraph file - - """ - i: cython.int - s: str - above_cutoff: cnp.ndarray - above_cutoff_endpos: cnp.ndarray - above_cutoff_startpos: cnp.ndarray - pos_array: cnp.ndarray - treat_array: cnp.ndarray - ctrl_array: cnp.ndarray - above_cutoff_index_array: cnp.ndarray - score_array_s: list # to: list keep different types of scores - peak_content: list - acs_ptr: cython.pointer(cython.int) - ace_ptr: cython.pointer(cython.int) - acia_ptr: cython.pointer(cython.int) - treat_array_ptr: cython.pointer(cython.float) - ctrl_array_ptr: cython.pointer(cython.float) - - assert len(scoring_function_s) == len(lvl1_cutoff_s), "number of functions and cutoffs should be the same!" - assert len(scoring_function_s) == len(lvl2_cutoff_s), "number of functions and cutoffs should be the same!" - - # first, build pileup, self.chr_pos_treat_ctrl - self.pileup_treat_ctrl_a_chromosome(chrom) - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - - # while save_bedGraph is true, invoke __write_bedGraph_for_a_chromosome - if save_bedGraph: - self.__write_bedGraph_for_a_chromosome(chrom) - - # keep all types of scores needed - score_array_s = [] - for i in range(len(scoring_function_s)): - s = scoring_function_s[i] - if s == 'p': - score_array_s.append(self.__cal_pscore(treat_array, - ctrl_array)) - elif s == 'q': - score_array_s.append(self.__cal_qscore(treat_array, - ctrl_array)) - elif s == 'f': - score_array_s.append(self.__cal_FE(treat_array, - ctrl_array)) - elif s == 's': - score_array_s.append(self.__cal_subtraction(treat_array, - ctrl_array)) - - # lvl1 : strong peaks - peak_content = [] # to store points above cutoff - - # get the regions with scores above cutoffs - above_cutoff = np.nonzero(apply_multiple_cutoffs(score_array_s, - lvl1_cutoff_s))[0] # this is not an optimized method. It would be better to store score array in a 2-D ndarray? - above_cutoff_index_array = np.arange(pos_array.shape[0], - dtype="int32")[above_cutoff] # indices - above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff - above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff - - if above_cutoff.size == 0: - # nothing above cutoff - return - - if above_cutoff[0] == 0: - # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] - above_cutoff_startpos[0] = 0 - - # first bit of region above cutoff - acs_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_startpos.data) - ace_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_endpos.data) - acia_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_index_array.data) - treat_array_ptr = cython.cast(cython.pointer(cython.float), - treat_array.data) - ctrl_array_ptr = cython.cast(cython.pointer(cython.float), - ctrl_array.data) - - ts = acs_ptr[0] - te = ace_ptr[0] - ti = acia_ptr[0] - tp = treat_array_ptr[ti] - cp = ctrl_array_ptr[ti] - - peak_content.append((ts, te, tp, cp, ti)) - acs_ptr += 1 # move ptr - ace_ptr += 1 - acia_ptr += 1 - lastp = te - - # peak_content.append((above_cutoff_startpos[0], above_cutoff_endpos[0], treat_array[above_cutoff_index_array[0]], ctrl_array[above_cutoff_index_array[0]], score_array_s, above_cutoff_index_array[0])) - for i in range(1, above_cutoff_startpos.size): - ts = acs_ptr[0] - te = ace_ptr[0] - ti = acia_ptr[0] - acs_ptr += 1 - ace_ptr += 1 - acia_ptr += 1 - tp = treat_array_ptr[ti] - cp = ctrl_array_ptr[ti] - tl = ts - lastp - if tl <= lvl1_max_gap: - # append - peak_content.append((ts, te, tp, cp, ti)) - lastp = te - else: - # close - self.__close_peak_for_broad_region(peak_content, - lvl1peaks, - min_length, - chrom, - lvl1_max_gap//2, - score_array_s) - peak_content = [(ts, te, tp, cp, ti),] - lastp = te # above_cutoff_endpos[i] - - # save the last peak - if peak_content: - self.__close_peak_for_broad_region(peak_content, - lvl1peaks, - min_length, - chrom, - lvl1_max_gap//2, - score_array_s) - - # lvl2 : weak peaks - peak_content = [] # to store points above cutoff - - # get the regions with scores above cutoffs - - # this is not an optimized method. It would be better to store score array in a 2-D ndarray? - above_cutoff = np.nonzero(apply_multiple_cutoffs(score_array_s, - lvl2_cutoff_s))[0] - - above_cutoff_index_array = np.arange(pos_array.shape[0], - dtype="i4")[above_cutoff] # indices - above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff - above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff - - if above_cutoff.size == 0: - # nothing above cutoff - return - - if above_cutoff[0] == 0: - # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] - above_cutoff_startpos[0] = 0 - - # first bit of region above cutoff - acs_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_startpos.data) - ace_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_endpos.data) - acia_ptr = cython.cast(cython.pointer(cython.int), - above_cutoff_index_array.data) - treat_array_ptr = cython.cast(cython.pointer(cython.float), - treat_array.data) - ctrl_array_ptr = cython.cast(cython.pointer(cython.float), - ctrl_array.data) - - ts = acs_ptr[0] - te = ace_ptr[0] - ti = acia_ptr[0] - tp = treat_array_ptr[ti] - cp = ctrl_array_ptr[ti] - peak_content.append((ts, te, tp, cp, ti)) - acs_ptr += 1 # move ptr - ace_ptr += 1 - acia_ptr += 1 - - lastp = te - for i in range(1, above_cutoff_startpos.size): - # for everything above cutoff - ts = acs_ptr[0] # get the start - te = ace_ptr[0] # get the end - ti = acia_ptr[0] # get the index - - acs_ptr += 1 # move ptr - ace_ptr += 1 - acia_ptr += 1 - tp = treat_array_ptr[ti] # get the treatment pileup - cp = ctrl_array_ptr[ti] # get the control pileup - tl = ts - lastp # get the distance from the current point to last position of existing peak_content - - if tl <= lvl2_max_gap: - # append - peak_content.append((ts, te, tp, cp, ti)) - lastp = te - else: - # close - self.__close_peak_for_broad_region(peak_content, - lvl2peaks, - min_length, - chrom, - lvl2_max_gap//2, - score_array_s) - - peak_content = [(ts, te, tp, cp, ti),] - lastp = te - - # save the last peak - if peak_content: - self.__close_peak_for_broad_region(peak_content, - lvl2peaks, - min_length, - chrom, - lvl2_max_gap//2, - score_array_s) - - return - - @cython.cfunc - def __close_peak_for_broad_region(self, - peak_content: list, - peaks, - min_length: cython.int, - chrom: bytes, - smoothlen: cython.int, - score_array_s: list, - score_cutoff_s: list = []) -> bool: - """Close the broad peak region, output peak boundaries, peak summit - and scores, then add the peak to peakIO object. - - peak_content contains [start, end, treat_p, ctrl_p, list_scores] - - peaks: a BroadPeakIO object - - """ - tstart: cython.int - tend: cython.int - i: cython.int - ttreat_p: cython.double - tctrl_p: cython.double - tlist_pileup: list - tlist_control: list - tlist_length: list - tlist_scores_p: cython.int - tarray_pileup: cnp.ndarray - tarray_control: cnp.ndarray - tarray_pscore: cnp.ndarray - tarray_qscore: cnp.ndarray - tarray_fc: cnp.ndarray - - peak_length = peak_content[-1][1] - peak_content[0][0] - if peak_length >= min_length: # if the peak is too small, reject it - tlist_pileup = [] - tlist_control = [] - tlist_length = [] - for i in range(len(peak_content)): # each position in broad peak - (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] - tlist_pileup.append(ttreat_p) - tlist_control.append(tctrl_p) - tlist_length.append(tend - tstart) - - tarray_pileup = np.array(tlist_pileup, dtype="f4") - tarray_control = np.array(tlist_control, dtype="f4") - tarray_pscore = self.__cal_pscore(tarray_pileup, tarray_control) - tarray_qscore = self.__cal_qscore(tarray_pileup, tarray_control) - tarray_fc = self.__cal_FE(tarray_pileup, tarray_control) - - peaks.add(chrom, # chromosome - peak_content[0][0], # start - peak_content[-1][1], # end - summit=0, - peak_score=mean_from_value_length(tarray_qscore, tlist_length), - pileup=mean_from_value_length(tarray_pileup, tlist_length), - pscore=mean_from_value_length(tarray_pscore, tlist_length), - fold_change=mean_from_value_length(tarray_fc, tlist_length), - qscore=mean_from_value_length(tarray_qscore, tlist_length), - ) - # if chrom == "chr1" and peak_content[0][0] == 237643 and peak_content[-1][1] == 237935: - # print tarray_qscore, tlist_length - # start a new peak - return True - - @cython.cfunc - def __add_broadpeak(self, - bpeaks, - chrom: bytes, - lvl2peak: object, - lvl1peakset: list): - """Internal function to create broad peak. - - *Note* lvl1peakset/strong_regions might be empty - """ - - blockNum: cython.int - start: cython.int - end: cython.int - blockSizes: bytes - blockStarts: bytes - thickStart: bytes - thickEnd: bytes - - start = lvl2peak["start"] - end = lvl2peak["end"] - - if not lvl1peakset: - # will complement by adding 1bps start and end to this region - # may change in the future if gappedPeak format was improved. - bpeaks.add(chrom, start, end, - score=lvl2peak["score"], - thickStart=(b"%d" % start), - thickEnd=(b"%d" % end), - blockNum=2, - blockSizes=b"1,1", - blockStarts=(b"0,%d" % (end-start-1)), - pileup=lvl2peak["pileup"], - pscore=lvl2peak["pscore"], - fold_change=lvl2peak["fc"], - qscore=lvl2peak["qscore"]) - return bpeaks - - thickStart = b"%d" % (lvl1peakset[0]["start"]) - thickEnd = b"%d" % (lvl1peakset[-1]["end"]) - blockNum = len(lvl1peakset) - blockSizes = b",".join([b"%d" % y for y in [x["length"] for x in lvl1peakset]]) - blockStarts = b",".join([b"%d" % x for x in getitem_then_subtract(lvl1peakset, start)]) - - # add 1bp left and/or right block if necessary - if int(thickStart) != start: - # add 1bp left block - thickStart = b"%d" % start - blockNum += 1 - blockSizes = b"1,"+blockSizes - blockStarts = b"0,"+blockStarts - if int(thickEnd) != end: - # add 1bp right block - thickEnd = b"%d" % end - blockNum += 1 - blockSizes = blockSizes + b",1" - blockStarts = blockStarts + b"," + (b"%d" % (end-start-1)) - - bpeaks.add(chrom, start, end, - score=lvl2peak["score"], - thickStart=thickStart, - thickEnd=thickEnd, - blockNum=blockNum, - blockSizes=blockSizes, - blockStarts=blockStarts, - pileup=lvl2peak["pileup"], - pscore=lvl2peak["pscore"], - fold_change=lvl2peak["fc"], - qscore=lvl2peak["qscore"]) - return bpeaks From 6c38b1505bffa8dc537ba116f644b359ebe9dc36 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Tue, 22 Oct 2024 14:02:56 -0400 Subject: [PATCH 12/13] rewrite CallPeakUnit.py --- MACS3/IO/Parser.py | 41 +- MACS3/Signal/CallPeakUnit.py | 2256 +++++++++++++++++++++++++++++++++ MACS3/Signal/CallPeakUnit.pyx | 1781 -------------------------- MACS3/Signal/Pileup.py | 4 +- setup.py | 2 +- 5 files changed, 2279 insertions(+), 1805 deletions(-) create mode 100644 MACS3/Signal/CallPeakUnit.py delete mode 100644 MACS3/Signal/CallPeakUnit.pyx diff --git a/MACS3/IO/Parser.py b/MACS3/IO/Parser.py index b8e3057a..49954944 100644 --- a/MACS3/IO/Parser.py +++ b/MACS3/IO/Parser.py @@ -1,7 +1,7 @@ # cython: language_level=3 # cython: profile=True # cython: linetrace=True -# Time-stamp: <2024-10-16 00:09:32 Tao Liu> +# Time-stamp: <2024-10-22 10:25:23 Tao Liu> """Module for all MACS Parser classes for input. Please note that the parsers are for reading the alignment files ONLY. @@ -199,7 +199,8 @@ def __init__(self, string, strand): self.string = string def __str__(self): - return repr("Strand information can not be recognized in this line: \"%s\",\"%s\"" % (self.string, self.strand)) + return repr("Strand information can not be recognized in this line: \"%s\",\"%s\"" % + (self.string, self.strand)) @cython.cclass @@ -544,7 +545,8 @@ def pe_parse_line(self, thisline: bytes): atoi(thisfields[1]), atoi(thisfields[2])) except IndexError: - raise Exception("Less than 3 columns found at this line: %s\n" % thisline) + raise Exception("Less than 3 columns found at this line: %s\n" % + thisline) @cython.ccall def build_petrack(self): @@ -950,7 +952,9 @@ def tlen_parse_line(self, thisline: bytes) -> cython.int: thisfields = thisline.split(b'\t') bwflag = atoi(thisfields[1]) if bwflag & 4 or bwflag & 512 or bwflag & 256 or bwflag & 2048: - return 0 #unmapped sequence or bad sequence or 2nd or sup alignment + # unmapped sequence or bad sequence or 2nd or sup alignment + return 0 + if bwflag & 1: # paired read. We should only keep sequence if the mate is mapped # and if this is the left mate, all is within the flag! @@ -1068,9 +1072,11 @@ def __init__(self, filename: str, f.close() if self.gzipped: # open with gzip.open, then wrap it with BufferedReader! - self.fhd = io.BufferedReader(gzip.open(filename, mode='rb'), buffer_size=READ_BUFFER_SIZE) # buffersize set to 1M + self.fhd = io.BufferedReader(gzip.open(filename, mode='rb'), + buffer_size=READ_BUFFER_SIZE) else: - self.fhd = io.open(filename, mode='rb') # binary mode! I don't expect unicode here! + # binary mode! I don't expect unicode here! + self.fhd = io.open(filename, mode='rb') @cython.ccall def sniff(self): @@ -1089,7 +1095,8 @@ def sniff(self): return True else: self.fhd.seek(0) - raise Exception("File is not of a valid BAM format! %d" % tsize) + raise Exception("File is not of a valid BAM format! %d" % + tsize) else: self.fhd.seek(0) return False @@ -1189,7 +1196,8 @@ def build_fwtrack(self): rlengths: dict fwtrack = FWTrack(buffer_size=self.buffer_size) - references, rlengths = self.get_references() # after this, ptr at list of alignments + # after this, ptr at list of alignments + references, rlengths = self.get_references() # fseek = self.fhd.seek fread = self.fhd.read # ftell = self.fhd.tell @@ -1248,7 +1256,9 @@ def append_fwtrack(self, fwtrack): info("%d reads have been read." % i) self.fhd.close() # fwtrack.finalize() - # this is the problematic part. If fwtrack is finalized, then it's impossible to increase the length of it in a step of buffer_size for multiple input files. + # this is the problematic part. If fwtrack is finalized, then + # it's impossible to increase the length of it in a step of + # buffer_size for multiple input files. fwtrack.set_rlengths(rlengths) return fwtrack @@ -1323,14 +1333,9 @@ def build_petrack(self): if i % 1000000 == 0: info(" %d fragments parsed" % i) - # print(f"{references[chrid]:},{fpos:},{tlen:}") info("%d fragments have been read." % i) - # debug(f" {e1} Can't identify the length of entry, it may be the end of file, stop looping...") - # debug(f" {e2} Chromosome name can't be found which means this entry is skipped ...") - # assert i > 0, "Something went wrong, no fragment has been read! Check input file!" self.d = m / i self.n = i - # assert self.d >= 0, "Something went wrong (mean fragment size was negative: %d = %d / %d)" % (self.d, m, i) self.fhd.close() petrack.set_rlengths(rlengths) return petrack @@ -1349,9 +1354,7 @@ def append_petrack(self, petrack): rlengths: dict references, rlengths = self.get_references() - # fseek = self.fhd.seek fread = self.fhd.read - # ftell = self.fhd.tell # for convenience, only count valid pairs add_loc = petrack.add_loc @@ -1374,10 +1377,7 @@ def append_petrack(self, petrack): info("%d fragments have been read." % i) self.d = (self.d * self.n + m) / (self.n + i) self.n += i - # assert self.d >= 0, "Something went wrong (mean fragment size was negative: %d = %d / %d)" % (self.d, m, i) self.fhd.close() - # this is the problematic part. If fwtrack is finalized, then it's impossible to increase the length of it in a step of buffer_size for multiple input files. - # petrack.finalize() petrack.set_rlengths(rlengths) return petrack @@ -1525,7 +1525,8 @@ def pe_parse_line(self, thisline: bytes): thisfields[3], atoi(thisfields[4])) except IndexError: - raise Exception("Less than 5 columns found at this line: %s\n" % thisline) + raise Exception("Less than 5 columns found at this line: %s\n" % + thisline) @cython.ccall def build_petrack2(self): diff --git a/MACS3/Signal/CallPeakUnit.py b/MACS3/Signal/CallPeakUnit.py new file mode 100644 index 00000000..2e99613b --- /dev/null +++ b/MACS3/Signal/CallPeakUnit.py @@ -0,0 +1,2256 @@ +# cython: language_level=3 +# cython: profile=True +# cython: linetrace=True +# Time-stamp: <2024-10-22 11:42:37 Tao Liu> + +"""Module for Calculate Scores. + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file LICENSE included with +the distribution). +""" + +# ------------------------------------ +# python modules +# ------------------------------------ + +import _pickle as cPickle +from tempfile import mkstemp +import os + +# ------------------------------------ +# Other modules +# ------------------------------------ +import numpy as np +import cython +import cython.cimports.numpy as cnp +# from numpy cimport int32_t, int64_t, float32_t, float64_t +from cython.cimports.cpython import bool +from cykhash import PyObjectMap, Float32to32Map + +# ------------------------------------ +# C lib +# ------------------------------------ +from cython.cimports.libc.stdio import FILE, fopen, fprintf, fclose +from cython.cimports.libc.math import exp, log10, log1p, erf, sqrt + +# ------------------------------------ +# MACS3 modules +# ------------------------------------ +from MACS3.Signal.SignalProcessing import maxima, enforce_peakyness +from MACS3.IO.PeakIO import PeakIO, BroadPeakIO +from MACS3.Signal.FixWidthTrack import FWTrack +from MACS3.Signal.PairedEndTrack import PETrackI +from MACS3.Signal.Prob import poisson_cdf +from MACS3.Utilities.Logger import logging + +logger = logging.getLogger(__name__) +debug = logger.debug +info = logger.info +# -------------------------------------------- +# cached pscore function and LR_asym functions +# -------------------------------------------- +pscore_dict = PyObjectMap() +logLR_dict = PyObjectMap() + + +@cython.cfunc +def get_pscore(t: tuple) -> cython.float: + """t: tuple of (lambda, observation) + """ + val: cython.float + + if t in pscore_dict: + return pscore_dict[t] + else: + # calculate and cache + val = -1.0 * poisson_cdf(t[0], t[1], False, True) + pscore_dict[t] = val + return val + + +@cython.cfunc +def get_logLR_asym(t: tuple) -> cython.float: + """Calculate log10 Likelihood between H1 (enriched) and H0 ( + chromatin bias). Set minus sign for depletion. + """ + val: cython.float + x: cython.float + y: cython.float + + if t in logLR_dict: + return logLR_dict[t] + else: + x = t[0] + y = t[1] + # calculate and cache + if x > y: + val = (x*(log10(x)-log10(y))+y-x) + elif x < y: + val = (x*(-log10(x)+log10(y))-y+x) + else: + val = 0 + logLR_dict[t] = val + return val + +# ------------------------------------ +# constants +# ------------------------------------ + + +LOG10_E: cython.float = 0.43429448190325176 + +# ------------------------------------ +# Misc functions +# ------------------------------------ + + +@cython.cfunc +def clean_up_ndarray(x: cnp.ndarray): + # clean numpy ndarray in two steps + i: cython.long + + i = x.shape[0] // 2 + x.resize(100000 if i > 100000 else i, refcheck=False) + x.resize(0, refcheck=False) + return + + +@cython.cfunc +@cython.inline +def chi2_k1_cdf(x: cython.float) -> cython.float: + return erf(sqrt(x/2)) + + +@cython.cfunc +@cython.inline +def log10_chi2_k1_cdf(x: cython.float) -> cython.float: + return log10(erf(sqrt(x/2))) + + +@cython.cfunc +@cython.inline +def chi2_k2_cdf(x: cython.float) -> cython.float: + return 1 - exp(-x/2) + + +@cython.cfunc +@cython.inline +def log10_chi2_k2_cdf(x: cython.float) -> cython.float: + return log1p(- exp(-x/2)) * LOG10_E + + +@cython.cfunc +@cython.inline +def chi2_k4_cdf(x: cython.float) -> cython.float: + return 1 - exp(-x/2) * (1 + x/2) + + +@cython.cfunc +@cython.inline +def log10_chi2_k4_CDF(x: cython.float) -> cython.float: + return log1p(- exp(-x/2) * (1 + x/2)) * LOG10_E + + +@cython.cfunc +@cython.inline +def apply_multiple_cutoffs(multiple_score_arrays: list, + multiple_cutoffs: list) -> cnp.ndarray: + i: cython.int + ret: cnp.ndarray + + ret = multiple_score_arrays[0] > multiple_cutoffs[0] + + for i in range(1, len(multiple_score_arrays)): + ret += multiple_score_arrays[i] > multiple_cutoffs[i] + + return ret + + +@cython.cfunc +@cython.inline +def get_from_multiple_scores(multiple_score_arrays: list, + index: cython.int) -> list: + ret: list = [] + i: cython.int + + for i in range(len(multiple_score_arrays)): + ret.append(multiple_score_arrays[i][index]) + return ret + + +@cython.cfunc +@cython.inline +def get_logFE(x: cython.float, + y: cython.float) -> cython.float: + """ return 100* log10 fold enrichment with +1 pseudocount. + """ + return log10(x/y) + + +@cython.cfunc +@cython.inline +def get_subtraction(x: cython.float, + y: cython.float) -> cython.float: + """ return subtraction. + """ + return x - y + + +@cython.cfunc +@cython.inline +def getitem_then_subtract(peakset: list, + start: cython.int) -> list: + a: list + + a = [x["start"] for x in peakset] + for i in range(len(a)): + a[i] = a[i] - start + return a + + +@cython.cfunc +@cython.inline +def left_sum(data, pos: cython.int, + width: cython.int) -> cython.int: + """ + """ + return sum([data[x] for x in data if x <= pos and x >= pos - width]) + + +@cython.cfunc +@cython.inline +def right_sum(data, + pos: cython.int, + width: cython.int) -> cython.int: + """ + """ + return sum([data[x] for x in data if x >= pos and x <= pos + width]) + + +@cython.cfunc +@cython.inline +def left_forward(data, + pos: cython.int, + window_size: cython.int) -> cython.int: + return data.get(pos, 0) - data.get(pos-window_size, 0) + + +@cython.cfunc +@cython.inline +def right_forward(data, + pos: cython.int, + window_size: cython.int) -> cython.int: + return data.get(pos + window_size, 0) - data.get(pos, 0) + + +@cython.cfunc +def median_from_value_length(value: cnp.ndarray(cython.float, ndim=1), + length: list) -> cython.float: + """ + """ + tmp: list + c: cython.int + tmp_l: cython.int + tmp_v: cython.float + mid_l: cython.float + + c = 0 + tmp = sorted(list(zip(value, length))) + mid_l = sum(length)/2 + for (tmp_v, tmp_l) in tmp: + c += tmp_l + if c > mid_l: + return tmp_v + + +@cython.cfunc +def mean_from_value_length(value: cnp.ndarray(cython.float, ndim=1), + length: list) -> cython.float: + """take of: list values and of: list corresponding lengths, + calculate the mean. An important function for bedGraph type of + data. + + """ + i: cython.int + tmp_l: cython.int + ln: cython.int + tmp_v: cython.double + sum_v: cython.double + tmp_sum: cython.double + ret: cython.float + + sum_v = 0 + ln = 0 + + for i in range(len(length)): + tmp_l = length[i] + tmp_v = cython.cast(cython.double, value[i]) + tmp_sum = tmp_v * tmp_l + sum_v = tmp_sum + sum_v + ln += tmp_l + + ret = cython.cast(cython.float, (sum_v/ln)) + + return ret + + +@cython.cfunc +def find_optimal_cutoff(x: list, y: list) -> tuple: + """Return the best cutoff x and y. + + We assume that total peak length increase exponentially while + decreasing cutoff value. But while cutoff decreases to a point + that background noises are captured, total length increases much + faster. So we fit a linear model by taking the first 10 points, + then look for the largest cutoff that + + + *Currently, it is coded as a useless function. + """ + npx: cnp.ndarray + npy: cnp.ndarray + npA: cnp.ndarray + ln: cython.long + i: cython.long + m: cython.float + c: cython.float # slop and intercept + sst: cython.float # sum of squared total + sse: cython.float # sum of squared error + rsq: cython.float # R-squared + + ln = len(x) + assert ln == len(y) + npx = np.array(x) + npy = np.log10(np.array(y)) + npA = np.vstack([npx, np.ones(len(npx))]).T + + for i in range(10, ln): + # at least the largest 10 points + m, c = np.linalg.lstsq(npA[:i], npy[:i], rcond=None)[0] + sst = sum((npy[:i] - np.mean(npy[:i])) ** 2) + sse = sum((npy[:i] - m*npx[:i] - c) ** 2) + rsq = 1 - sse/sst + # print i, x[i], y[i], m, c, rsq + return (1.0, 1.0) + + +# ------------------------------------ +# Classes +# ------------------------------------ +@cython.cclass +class CallerFromAlignments: + """A unit to calculate scores and call peaks from alignments -- + FWTrack or PETrack objects. + + It will compute for each chromosome separately in order to save + memory usage. + """ + treat: object # FWTrack or PETrackI object for ChIP + ctrl: object # FWTrack or PETrackI object for Control + + d: cython.int # extension size for ChIP + # extension sizes for Control. Can be multiple values + ctrl_d_s: list + treat_scaling_factor: cython.float # scaling factor for ChIP + # scaling factor for Control, corresponding to each extension size. + ctrl_scaling_factor_s: list + # minimum local bias to fill missing values + lambda_bg: cython.float + # name of common chromosomes in ChIP and Control data + chromosomes: list + # the pseudocount used to calcuate logLR, FE or logFE + pseudocount: cython.double + # prefix will be added to _pileup.bdg for treatment and + # _lambda.bdg for control + bedGraph_filename_prefix: bytes + # shift of cutting ends before extension + end_shift: cython.int + # whether trackline should be saved in bedGraph + trackline: bool + # whether to save pileup and local bias in bedGraph files + save_bedGraph: bool + # whether to save pileup normalized by sequencing depth in million reads + save_SPMR: bool + # whether ignore local bias, and to use global bias instead + no_lambda_flag: bool + # whether it's in PE mode, will be detected during initiation + PE_mode: bool + + # temporary data buffer + # temporary [position, treat_pileup, ctrl_pileup] for a given chromosome + chr_pos_treat_ctrl: list + bedGraph_treat_filename: bytes + bedGraph_control_filename: bytes + bedGraph_treat_f: cython.pointer(FILE) + bedGraph_ctrl_f: cython.pointer(FILE) + + # data needed to be pre-computed before peak calling + # remember pvalue->qvalue convertion; saved in cykhash Float32to32Map + pqtable: Float32to32Map + # whether the pvalue of whole genome is all calculated. If yes, + # it's OK to calculate q-value. + pvalue_all_done: bool + # record for each pvalue cutoff, how many peaks can be called + pvalue_npeaks: dict + # record for each pvalue cutoff, the total length of called peaks + pvalue_length: dict + # automatically decide the p-value cutoff (can be translated into + # qvalue cutoff) based on p-value to total peak length analysis. + optimal_p_cutoff: cython.float + # file to save the pvalue-npeaks-totallength table + cutoff_analysis_filename: bytes + # Record the names of temporary files for storing pileup values of + # each chromosome + pileup_data_files: dict + + def __init__(self, + treat, + ctrl, + d: cython.int = 200, + ctrl_d_s: list = [200, 1000, 10000], + treat_scaling_factor: cython.float = 1.0, + ctrl_scaling_factor_s: list = [1.0, 0.2, 0.02], + stderr_on: bool = False, + pseudocount: cython.float = 1, + end_shift: cython.int = 0, + lambda_bg: cython.float = 0, + save_bedGraph: bool = False, + bedGraph_filename_prefix: str = "PREFIX", + bedGraph_treat_filename: str = "TREAT.bdg", + bedGraph_control_filename: str = "CTRL.bdg", + cutoff_analysis_filename: str = "TMP.txt", + save_SPMR: bool = False): + """Initialize. + + A calculator is unique to each comparison of treat and + control. Treat_depth and ctrl_depth should not be changed + during calculation. + + treat and ctrl are either FWTrack or PETrackI objects. + + treat_depth and ctrl_depth are effective depth in million: + sequencing depth in million after + duplicates being filtered. If + treatment is scaled down to + control sample size, then this + should be control sample size in + million. And vice versa. + + d, sregion, lregion: d is the fragment size, sregion is the + small region size, lregion is the large + region size + + pseudocount: a pseudocount used to calculate logLR, FE or + logFE. Please note this value will not be changed + with normalization method. So if you really want + to pseudocount: set 1 per million reads, it: set + after you normalize treat and control by million + reads by `change_normalizetion_method(ord('M'))`. + + """ + chr1: set + chr2: set + p: cython.float + + # decide PE mode + if isinstance(treat, FWTrack): + self.PE_mode = False + elif isinstance(treat, PETrackI): + self.PE_mode = True + else: + raise Exception("Should be FWTrack or PETrackI object!") + # decide if there is control + self.treat = treat + if ctrl: + self.ctrl = ctrl + else: # while there is no control + self.ctrl = treat + self.trackline = False + self.d = d # note, self.d doesn't make sense in PE mode + self.ctrl_d_s = ctrl_d_s # note, self.d doesn't make sense in PE mode + self.treat_scaling_factor = treat_scaling_factor + self.ctrl_scaling_factor_s = ctrl_scaling_factor_s + self.end_shift = end_shift + self.lambda_bg = lambda_bg + self.pqtable = Float32to32Map(for_int=False) # Float32 -> Float32 map + self.save_bedGraph = save_bedGraph + self.save_SPMR = save_SPMR + self.bedGraph_filename_prefix = bedGraph_filename_prefix.encode() + self.bedGraph_treat_filename = bedGraph_treat_filename.encode() + self.bedGraph_control_filename = bedGraph_control_filename.encode() + if not self.ctrl_d_s or not self.ctrl_scaling_factor_s: + self.no_lambda_flag = True + else: + self.no_lambda_flag = False + self.pseudocount = pseudocount + # get the common chromosome names from both treatment and control + chr1 = set(self.treat.get_chr_names()) + chr2 = set(self.ctrl.get_chr_names()) + self.chromosomes = sorted(list(chr1.intersection(chr2))) + + self.pileup_data_files = {} + self.pvalue_length = {} + self.pvalue_npeaks = {} + # step for optimal cutoff is 0.3 in -log10pvalue, we try from + # pvalue 1E-10 (-10logp=10) to 0.5 (-10logp=0.3) + for p in np.arange(0.3, 10, 0.3): + self.pvalue_length[p] = 0 + self.pvalue_npeaks[p] = 0 + self.optimal_p_cutoff = 0 + self.cutoff_analysis_filename = cutoff_analysis_filename.encode() + + @cython.ccall + def destroy(self): + """Remove temporary files for pileup values of each chromosome. + + Note: This function MUST be called if the class won: object't + be used anymore. + + """ + f: bytes + + for f in self.pileup_data_files.values(): + if os.path.isfile(f): + os.unlink(f) + return + + @cython.ccall + def set_pseudocount(self, pseudocount: cython.float): + self.pseudocount = pseudocount + + @cython.ccall + def enable_trackline(self): + """Turn on trackline with bedgraph output + """ + self.trackline = True + + @cython.cfunc + def pileup_treat_ctrl_a_chromosome(self, chrom: bytes): + """After this function is called, self.chr_pos_treat_ctrl will + be reand: set assigned to the pileup values of the given + chromosome. + + """ + treat_pv: list + ctrl_pv: list + f: object + temp_filename: str + + assert chrom in self.chromosomes, "chromosome %s is not valid." % chrom + + # check backup file of pileup values. If not exists, create + # it. Otherwise, load them instead of calculating new pileup + # values. + if chrom in self.pileup_data_files: + try: + f = open(self.pileup_data_files[chrom], "rb") + self.chr_pos_treat_ctrl = cPickle.load(f) + f.close() + return + except Exception: + temp_fd, temp_filename = mkstemp() + os.close(temp_fd) + self.pileup_data_files[chrom] = temp_filename + else: + temp_fd, temp_filename = mkstemp() + os.close(temp_fd) + self.pileup_data_files[chrom] = temp_filename.encode() + + # reor: set clean existing self.chr_pos_treat_ctrl + if self.chr_pos_treat_ctrl: # not a beautiful way to clean + clean_up_ndarray(self.chr_pos_treat_ctrl[0]) + clean_up_ndarray(self.chr_pos_treat_ctrl[1]) + clean_up_ndarray(self.chr_pos_treat_ctrl[2]) + + if self.PE_mode: + treat_pv = self.treat.pileup_a_chromosome(chrom, + [self.treat_scaling_factor,], + baseline_value=0.0) + else: + treat_pv = self.treat.pileup_a_chromosome(chrom, + [self.d,], + [self.treat_scaling_factor,], + baseline_value=0.0, + directional=True, + end_shift=self.end_shift) + + if not self.no_lambda_flag: + if self.PE_mode: + # note, we pileup up PE control as SE control because + # we assume the bias only can be captured at the + # surrounding regions of cutting sites from control experiments. + ctrl_pv = self.ctrl.pileup_a_chromosome_c(chrom, + self.ctrl_d_s, + self.ctrl_scaling_factor_s, + baseline_value=self.lambda_bg) + else: + ctrl_pv = self.ctrl.pileup_a_chromosome(chrom, + self.ctrl_d_s, + self.ctrl_scaling_factor_s, + baseline_value=self.lambda_bg, + directional=False) + else: + # a: set global lambda + ctrl_pv = [treat_pv[0][-1:], np.array([self.lambda_bg,], + dtype="f4")] + + self.chr_pos_treat_ctrl = self.__chrom_pair_treat_ctrl(treat_pv, + ctrl_pv) + + # clean treat_pv and ctrl_pv + treat_pv = [] + ctrl_pv = [] + + # save data to temporary file + try: + f = open(self.pileup_data_files[chrom], "wb") + cPickle.dump(self.chr_pos_treat_ctrl, f, protocol=2) + f.close() + except Exception: + # fail to write then remove the key in pileup_data_files + self.pileup_data_files.pop(chrom) + return + + @cython.cfunc + def __chrom_pair_treat_ctrl(self, treat_pv, ctrl_pv) -> list: + """*private* Pair treat and ctrl pileup for each region. + + treat_pv and ctrl_pv are [np.ndarray, np.ndarray]. + + return [p, t, c] list, each element is a numpy array. + """ + index_ret: cython.long + it: cython.long + ic: cython.long + lt: cython.long + lc: cython.long + t_p: cnp.ndarray + c_p: cnp.ndarray + ret_p: cnp.ndarray + t_v: cnp.ndarray + c_v: cnp.ndarray + ret_t: cnp.ndarray + ret_c: cnp.ndarray + t_p_view: cython.pointer(cython.int) + c_p_view: cython.pointer(cython.int) + ret_p_view: cython.pointer(cython.int) + t_v_view: cython.pointer(cython.float) + c_v_view: cython.pointer(cython.float) + ret_t_view: cython.pointer(cython.float) + ret_c_view: cython.pointer(cython.float) + + [t_p, t_v] = treat_pv + [c_p, c_v] = ctrl_pv + + lt = t_p.shape[0] + lc = c_p.shape[0] + + chrom_max_len = lt + lc + + ret_p = np.zeros(chrom_max_len, dtype="i4") # position + ret_t = np.zeros(chrom_max_len, dtype="f4") # value from treatment + ret_c = np.zeros(chrom_max_len, dtype="f4") # value from control + + # t_p_view = t_p #cython.cast(cython.pointer[cython.int], t_p.data) + # t_v_view = t_v #cython.cast(cython.pointer[cython.float], t_v.data) + # c_p_view = c_p #cython.cast(cython.pointer[cython.int], c_p.data) + # c_v_view = c_v #cython.cast(cython.pointer[cython.float], c_v.data) + # ret_p_view = ret_p #cython.cast(cython.pointer[cython.int], ret_p.data) + # ret_t_view = ret_t #cython.cast(cython.pointer[cython.float], ret_t.data) + # ret_c_view = ret_c #cython.cast(cython.pointer[cython.float], ret_c.data) + + t_p_view = cython.cast(cython.pointer(cython.int), t_p.data) + t_v_view = cython.cast(cython.pointer(cython.float), t_v.data) + c_p_view = cython.cast(cython.pointer(cython.int), c_p.data) + c_v_view = cython.cast(cython.pointer(cython.float), c_v.data) + ret_p_view = cython.cast(cython.pointer(cython.int), ret_p.data) + ret_t_view = cython.cast(cython.pointer(cython.float), ret_t.data) + ret_c_view = cython.cast(cython.pointer(cython.float), ret_c.data) + + index_ret = 0 + it = 0 + ic = 0 + + while it < lt and ic < lc: + if t_p_view[0] < c_p_view[0]: + # clip a region from pre_p to p1, then pre_p: set as p1. + ret_p_view[0] = t_p_view[0] + ret_t_view[0] = t_v_view[0] + ret_c_view[0] = c_v_view[0] + ret_p_view += 1 + ret_t_view += 1 + ret_c_view += 1 + index_ret += 1 + # call for the next p1 and v1 + it += 1 + t_p_view += 1 + t_v_view += 1 + elif t_p_view[0] > c_p_view[0]: + # clip a region from pre_p to p2, then pre_p: set as p2. + ret_p_view[0] = c_p_view[0] + ret_t_view[0] = t_v_view[0] + ret_c_view[0] = c_v_view[0] + ret_p_view += 1 + ret_t_view += 1 + ret_c_view += 1 + index_ret += 1 + # call for the next p2 and v2 + ic += 1 + c_p_view += 1 + c_v_view += 1 + else: + # from pre_p to p1 or p2, then pre_p: set as p1 or p2. + ret_p_view[0] = t_p_view[0] + ret_t_view[0] = t_v_view[0] + ret_c_view[0] = c_v_view[0] + ret_p_view += 1 + ret_t_view += 1 + ret_c_view += 1 + index_ret += 1 + # call for the next p1, v1, p2, v2. + it += 1 + ic += 1 + t_p_view += 1 + t_v_view += 1 + c_p_view += 1 + c_v_view += 1 + + ret_p.resize(index_ret, refcheck=False) + ret_t.resize(index_ret, refcheck=False) + ret_c.resize(index_ret, refcheck=False) + return [ret_p, ret_t, ret_c] + + @cython.cfunc + def __cal_score(self, + array1: cnp.ndarray(cython.float, ndim=1), + array2: cnp.ndarray(cython.float, ndim=1), + cal_func) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray(cython.float, ndim=1) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + for i in range(array1.shape[0]): + s[i] = cal_func(array1[i], array2[i]) + return s + + @cython.cfunc + def __cal_pvalue_qvalue_table(self): + """After this function is called, self.pqtable is built. All + chromosomes will be iterated. So it will take some time. + + """ + chrom: bytes + pos_array: cnp.ndarray + treat_array: cnp.ndarray + ctrl_array: cnp.ndarray + pscore_stat: dict + pre_p: cython.long + # pre_l: cython.long + l: cython.long + i: cython.long + j: cython.long + this_v: cython.float + # pre_v: cython.float + v: cython.float + q: cython.float + pre_q: cython.float + N: cython.long + k: cython.long + this_l: cython.long + f: cython.float + unique_values: list + pos_view: cython.pointer(cython.int) + treat_value_view: cython.pointer(cython.float) + ctrl_value_view: cython.pointer(cython.float) + + debug("Start to calculate pvalue stat...") + + pscore_stat = {} # dict() + for i in range(len(self.chromosomes)): + chrom = self.chromosomes[i] + pre_p = 0 + + self.pileup_treat_ctrl_a_chromosome(chrom) + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + + pos_view = cython.cast(cython.pointer(cython.int), + pos_array.data) + treat_value_view = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_value_view = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + for j in range(pos_array.shape[0]): + this_v = get_pscore((cython.cast(cython.int, + treat_value_view[0]), + ctrl_value_view[0])) + this_l = pos_view[0] - pre_p + if this_v in pscore_stat: + pscore_stat[this_v] += this_l + else: + pscore_stat[this_v] = this_l + pre_p = pos_view[0] + pos_view += 1 + treat_value_view += 1 + ctrl_value_view += 1 + + N = sum(pscore_stat.values()) # total length + k = 1 # rank + f = -log10(N) + # pre_v = -2147483647 + # pre_l = 0 + pre_q = 2147483647 # save the previous q-value + + self.pqtable = Float32to32Map(for_int=False) + unique_values = sorted(list(pscore_stat.keys()), reverse=True) + for i in range(len(unique_values)): + v = unique_values[i] + l = pscore_stat[v] + q = v + (log10(k) + f) + if q > pre_q: + q = pre_q + if q <= 0: + q = 0 + break + #q = max(0,min(pre_q,q)) # make q-score monotonic + self.pqtable[v] = q + pre_q = q + k += l + # bottom rank pscores all have qscores 0 + for j in range(i, len(unique_values)): + v = unique_values[j] + self.pqtable[v] = 0 + return + + @cython.cfunc + def __pre_computes(self, + max_gap: cython.int = 50, + min_length: cython.int = 200): + """After this function is called, self.pqtable and self.pvalue_length is built. All + chromosomes will be iterated. So it will take some time. + + """ + chrom: bytes + pos_array: cnp.ndarray + treat_array: cnp.ndarray + ctrl_array: cnp.ndarray + score_array: cnp.ndarray + pscore_stat: dict + n: cython.long + pre_p: cython.long + this_p: cython.long + j: cython.long + l: cython.long + i: cython.long + q: cython.float + pre_q: cython.float + this_v: cython.float + v: cython.float + cutoff: cython.float + N: cython.long + k: cython.long + this_l: cython.long + f: cython.float + unique_values: list + above_cutoff: cnp.ndarray + above_cutoff_endpos: cnp.ndarray + above_cutoff_startpos: cnp.ndarray + peak_content: list + peak_length: cython.long + total_l: cython.long + total_p: cython.long + tmplist: list + + # above cutoff start position pointer + acs_ptr: cython.pointer(cython.int) + # above cutoff end position pointer + ace_ptr: cython.pointer(cython.int) + # position array pointer + pos_array_ptr: cython.pointer(cython.int) + # score array pointer + score_array_ptr: cython.pointer(cython.float) + + debug("Start to calculate pvalue stat...") + + # tmpcontains: list a of: list log pvalue cutoffs from 0.3 to 10 + tmplist = [round(x, 5) + for x in sorted(list(np.arange(0.3, 10.0, 0.3)), + reverse=True)] + + pscore_stat = {} # dict() + # print (list(pscore_stat.keys())) + # print (list(self.pvalue_length.keys())) + # print (list(self.pvalue_npeaks.keys())) + for i in range(len(self.chromosomes)): + chrom = self.chromosomes[i] + self.pileup_treat_ctrl_a_chromosome(chrom) + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + + score_array = self.__cal_pscore(treat_array, ctrl_array) + + for n in range(len(tmplist)): + cutoff = tmplist[n] + total_l = 0 # total length in potential peak + total_p = 0 + + # get the regions with scores above cutoffs this is + # not an optimized method. It would be better to store + # score array in a 2-D ndarray? + above_cutoff = np.nonzero(score_array > cutoff)[0] + # end positions of regions where score is above cutoff + above_cutoff_endpos = pos_array[above_cutoff] + # start positions of regions where score is above cutoff + above_cutoff_startpos = pos_array[above_cutoff-1] + + if above_cutoff_endpos.size == 0: + continue + + # first bit of region above cutoff + acs_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_startpos.data) + ace_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_endpos.data) + + peak_content = [(acs_ptr[0], ace_ptr[0]),] + lastp = ace_ptr[0] + acs_ptr += 1 + ace_ptr += 1 + + for i in range(1, above_cutoff_startpos.size): + tl = acs_ptr[0] - lastp + if tl <= max_gap: + peak_content.append((acs_ptr[0], ace_ptr[0])) + else: + peak_length = peak_content[-1][1] - peak_content[0][0] + # if the peak is too small, reject it + if peak_length >= min_length: + total_l += peak_length + total_p += 1 + peak_content = [(acs_ptr[0], ace_ptr[0]),] + lastp = ace_ptr[0] + acs_ptr += 1 + ace_ptr += 1 + + if peak_content: + peak_length = peak_content[-1][1] - peak_content[0][0] + # if the peak is too small, reject it + if peak_length >= min_length: + total_l += peak_length + total_p += 1 + self.pvalue_length[cutoff] = self.pvalue_length.get(cutoff, 0) + total_l + self.pvalue_npeaks[cutoff] = self.pvalue_npeaks.get(cutoff, 0) + total_p + + pos_array_ptr = cython.cast(cython.pointer(cython.int), + pos_array.data) + score_array_ptr = cython.cast(cython.pointer(cython.float), + score_array.data) + + pre_p = 0 + for i in range(pos_array.shape[0]): + this_p = pos_array_ptr[0] + this_l = this_p - pre_p + this_v = score_array_ptr[0] + if this_v in pscore_stat: + pscore_stat[this_v] += this_l + else: + pscore_stat[this_v] = this_l + pre_p = this_p # pos_array[i] + pos_array_ptr += 1 + score_array_ptr += 1 + + # debug ("make pscore_stat cost %.5f seconds" % t) + + # add all pvalue cutoffs from cutoff-analysis part. So that we + # can get the corresponding qvalues for them. + for cutoff in tmplist: + if cutoff not in pscore_stat: + pscore_stat[cutoff] = 0 + + N = sum(pscore_stat.values()) # total length + k = 1 # rank + f = -log10(N) + pre_q = 2147483647 # save the previous q-value + + self.pqtable = Float32to32Map(for_int=False) # {} + # sorted(unique_values,reverse=True) + unique_values = sorted(list(pscore_stat.keys()), reverse=True) + for i in range(len(unique_values)): + v = unique_values[i] + l = pscore_stat[v] + q = v + (log10(k) + f) + if q > pre_q: + q = pre_q + if q <= 0: + q = 0 + break + # q = max(0,min(pre_q,q)) # make q-score monotonic + self.pqtable[v] = q + pre_q = q + k += l + for j in range(i, len(unique_values)): + v = unique_values[j] + self.pqtable[v] = 0 + + # write pvalue and total length of predicted peaks + # this is the output from cutoff-analysis + fhd = open(self.cutoff_analysis_filename, "w") + fhd.write("pscore\tqscore\tnpeaks\tlpeaks\tavelpeak\n") + x = [] + y = [] + for cutoff in tmplist: + if self.pvalue_npeaks[cutoff] > 0: + fhd.write("%.2f\t%.2f\t%d\t%d\t%.2f\n" % + (cutoff, self.pqtable[cutoff], + self.pvalue_npeaks[cutoff], + self.pvalue_length[cutoff], + self.pvalue_length[cutoff]/self.pvalue_npeaks[cutoff])) + x.append(cutoff) + y.append(self.pvalue_length[cutoff]) + fhd.close() + info("#3 Analysis of cutoff vs num of peaks or total length has been saved in %s" % self.cutoff_analysis_filename) + # info("#3 Suggest a cutoff...") + # optimal_cutoff, optimal_length = find_optimal_cutoff(x, y) + # info("#3 -10log10pvalue cutoff %.2f will call approximately %.0f bps regions as significant regions" % (optimal_cutoff, optimal_length)) + # print (list(pqtable.keys())) + # print (list(self.pvalue_length.keys())) + # print (list(self.pvalue_npeaks.keys())) + return + + @cython.ccall + def call_peaks(self, + scoring_function_symbols: list, + score_cutoff_s: list, + min_length: cython.int = 200, + max_gap: cython.int = 50, + call_summits: bool = False, + cutoff_analysis: bool = False): + """Call peaks for all chromosomes. Return a PeakIO object. + + scoring_function_s: symbols of functions to calculate score. 'p' for pscore, 'q' for qscore, 'f' for fold change, 's' for subtraction. for example: ['p', 'q'] + score_cutoff_s : cutoff values corresponding to scoring functions + min_length : minimum length of peak + max_gap : maximum gap of 'insignificant' regions within a peak. Note, for PE_mode, max_gap and max_length are both as: set fragment length. + call_summits : boolean. Whether or not call sub-peaks. + save_bedGraph : whether or not to save pileup and control into a bedGraph file + """ + chrom: bytes + tmp_bytes: bytes + + peaks = PeakIO() + + # prepare p-q table + if len(self.pqtable) == 0: + info("#3 Pre-compute pvalue-qvalue table...") + if cutoff_analysis: + info("#3 Cutoff vs peaks called will be analyzed!") + self.__pre_computes(max_gap=max_gap, min_length=min_length) + else: + self.__cal_pvalue_qvalue_table() + + + # prepare bedGraph file + if self.save_bedGraph: + self.bedGraph_treat_f = fopen(self.bedGraph_treat_filename, "w") + self.bedGraph_ctrl_f = fopen(self.bedGraph_control_filename, "w") + + info("#3 In the peak calling step, the following will be performed simultaneously:") + info("#3 Write bedGraph files for treatment pileup (after scaling if necessary)... %s" % + self.bedGraph_filename_prefix.decode() + "_treat_pileup.bdg") + info("#3 Write bedGraph files for control lambda (after scaling if necessary)... %s" % + self.bedGraph_filename_prefix.decode() + "_control_lambda.bdg") + + if self.save_SPMR: + info("#3 --SPMR is requested, so pileup will be normalized by sequencing depth in million reads.") + elif self.treat_scaling_factor == 1: + info("#3 Pileup will be based on sequencing depth in treatment.") + else: + info("#3 Pileup will be based on sequencing depth in control.") + + if self.trackline: + # this line is REQUIRED by the wiggle format for UCSC browser + tmp_bytes = ("track type=bedGraph name=\"treatment pileup\" description=\"treatment pileup after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() + fprintf(self.bedGraph_treat_f, tmp_bytes) + tmp_bytes = ("track type=bedGraph name=\"control lambda\" description=\"control lambda after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() + fprintf(self.bedGraph_ctrl_f, tmp_bytes) + + info("#3 Call peaks for each chromosome...") + for chrom in self.chromosomes: + # treat/control bedGraph will be saved if requested by user. + self.__chrom_call_peak_using_certain_criteria(peaks, + chrom, + scoring_function_symbols, + score_cutoff_s, + min_length, + max_gap, + call_summits, + self.save_bedGraph) + + # close bedGraph file + if self.save_bedGraph: + fclose(self.bedGraph_treat_f) + fclose(self.bedGraph_ctrl_f) + self.save_bedGraph = False + + return peaks + + @cython.cfunc + def __chrom_call_peak_using_certain_criteria(self, + peaks, + chrom: bytes, + scoring_function_s: list, + score_cutoff_s: list, + min_length: cython.int, + max_gap: cython.int, + call_summits: bool, + save_bedGraph: bool): + """ Call peaks for a chromosome. + + Combination of criteria is allowed here. + + peaks: a PeakIO object, the return value of this function + scoring_function_s: symbols of functions to calculate score as score=f(x, y) where x is treatment pileup, and y is control pileup + save_bedGraph : whether or not to save pileup and control into a bedGraph file + """ + i: cython.int + s: str + above_cutoff: cnp.ndarray + above_cutoff_endpos: cnp.ndarray + above_cutoff_startpos: cnp.ndarray + pos_array: cnp.ndarray + above_cutoff_index_array: cnp.ndarray + treat_array: cnp.ndarray + ctrl_array: cnp.ndarray + score_array_s: list # to: list keep different types of scores + peak_content: list # to store information for a + # chunk in a peak region, it + # contains lists of: 1. left + # position; 2. right + # position; 3. treatment + # value; 4. control value; + # 5. of: list scores at this + # chunk + tl: cython.long + lastp: cython.long + ts: cython.long + te: cython.long + ti: cython.long + tp: cython.float + cp: cython.float + acs_ptr: cython.pointer(cython.int) + ace_ptr: cython.pointer(cython.int) + acia_ptr: cython.pointer(cython.int) + treat_array_ptr: cython.pointer(cython.float) + ctrl_array_ptr: cython.pointer(cython.float) + + assert len(scoring_function_s) == len(score_cutoff_s), "number of functions and cutoffs should be the same!" + + peak_content = [] # to store points above cutoff + + # first, build pileup, self.chr_pos_treat_ctrl + # this step will be speeped up if pqtable is pre-computed. + self.pileup_treat_ctrl_a_chromosome(chrom) + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + + # while save_bedGraph is true, invoke __write_bedGraph_for_a_chromosome + if save_bedGraph: + self.__write_bedGraph_for_a_chromosome(chrom) + + # keep all types of scores needed + # t0 = ttime() + score_array_s = [] + for i in range(len(scoring_function_s)): + s = scoring_function_s[i] + if s == 'p': + score_array_s.append(self.__cal_pscore(treat_array, + ctrl_array)) + elif s == 'q': + score_array_s.append(self.__cal_qscore(treat_array, + ctrl_array)) + elif s == 'f': + score_array_s.append(self.__cal_FE(treat_array, + ctrl_array)) + elif s == 's': + score_array_s.append(self.__cal_subtraction(treat_array, + ctrl_array)) + + # get the regions with scores above cutoffs. this is not an + # optimized method. It would be better to store score array in + # a 2-D ndarray? + above_cutoff = np.nonzero(apply_multiple_cutoffs(score_array_s, + score_cutoff_s))[0] + # indices + above_cutoff_index_array = np.arange(pos_array.shape[0], + dtype="i4")[above_cutoff] + # end positions of regions where score is above cutoff + above_cutoff_endpos = pos_array[above_cutoff] + # start positions of regions where score is above cutoff + above_cutoff_startpos = pos_array[above_cutoff-1] + + if above_cutoff.size == 0: + # nothing above cutoff + return + + if above_cutoff[0] == 0: + # first element > cutoff, fix the first point as + # 0. otherwise it would be the last item in + # data[chrom]['pos'] + above_cutoff_startpos[0] = 0 + + # print "apply cutoff -- chrom:",chrom," time:", ttime() - t0 + # start to build peak regions + # t0 = ttime() + + # first bit of region above cutoff + acs_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_startpos.data) + ace_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_endpos.data) + acia_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_index_array.data) + treat_array_ptr = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_array_ptr = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + + peak_content.append((ts, te, tp, cp, ti)) + lastp = te + acs_ptr += 1 + ace_ptr += 1 + acia_ptr += 1 + + for i in range(1, above_cutoff_startpos.shape[0]): + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + acs_ptr += 1 + ace_ptr += 1 + acia_ptr += 1 + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + tl = ts - lastp + if tl <= max_gap: + # append. + peak_content.append((ts, te, tp, cp, ti)) + lastp = te # above_cutoff_endpos[i] + else: + # close + if call_summits: + # smooth length is min_length, i.e. fragment size 'd' + self.__close_peak_with_subpeaks(peak_content, + peaks, + min_length, + chrom, + min_length, + score_array_s, + score_cutoff_s=score_cutoff_s) + else: + # smooth length is min_length, i.e. fragment size 'd' + self.__close_peak_wo_subpeaks(peak_content, + peaks, + min_length, + chrom, + min_length, + score_array_s, + score_cutoff_s=score_cutoff_s) + peak_content = [(ts, te, tp, cp, ti),] + lastp = te # above_cutoff_endpos[i] + # save the last peak + if not peak_content: + return + else: + if call_summits: + # smooth length is min_length, i.e. fragment size 'd' + self.__close_peak_with_subpeaks(peak_content, + peaks, + min_length, + chrom, + min_length, + score_array_s, + score_cutoff_s=score_cutoff_s) + else: + # smooth length is min_length, i.e. fragment size 'd' + self.__close_peak_wo_subpeaks(peak_content, + peaks, + min_length, + chrom, + min_length, + score_array_s, + score_cutoff_s=score_cutoff_s) + + # print "close peaks -- chrom:",chrom," time:", ttime() - t0 + return + + @cython.cfunc + def __close_peak_wo_subpeaks(self, + peak_content: list, + peaks, + min_length: cython.int, + chrom: bytes, + smoothlen: cython.int, + score_array_s: list, + score_cutoff_s: list = []) -> bool: + """Close the peak region, output peak boundaries, peak summit + and scores, then add the peak to peakIO object. + + peak_content contains [start, end, treat_p, ctrl_p, index_in_score_array] + + peaks: a PeakIO object + + """ + summit_pos: cython.int + tstart: cython.int + tend: cython.int + summit_index: cython.int + i: cython.int + midindex: cython.int + ttreat_p: cython.double + tctrl_p: cython.double + tscore: cython.double + summit_treat: cython.double + summit_ctrl: cython.double + summit_p_score: cython.double + summit_q_score: cython.double + tlist_scores_p: cython.int + + peak_length = peak_content[-1][1] - peak_content[0][0] + if peak_length >= min_length: # if the peak is too small, reject it + tsummit = [] + summit_pos = 0 + summit_value = 0 + for i in range(len(peak_content)): + (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] + tscore = ttreat_p # use pscore as general score to find summit + if not summit_value or summit_value < tscore: + tsummit = [(tend + tstart) // 2,] + tsummit_index = [i,] + summit_value = tscore + elif summit_value == tscore: + # remember continuous summit values + tsummit.append((tend + tstart) // 2) + tsummit_index.append(i) + # the middle of all highest points in peak region is defined as summit + midindex = (len(tsummit) + 1) // 2 - 1 + summit_pos = tsummit[midindex] + summit_index = tsummit_index[midindex] + + summit_treat = peak_content[summit_index][2] + summit_ctrl = peak_content[summit_index][3] + + # this is a double-check to see if the summit can pass cutoff values. + for i in range(len(score_cutoff_s)): + if score_cutoff_s[i] > score_array_s[i][peak_content[summit_index][4]]: + return False # not passed, then disgard this peak. + + summit_p_score = pscore_dict[(cython.cast(cython.int, + summit_treat), + summit_ctrl)] + summit_q_score = self.pqtable[summit_p_score] + + peaks.add(chrom, # chromosome + peak_content[0][0], # start + peak_content[-1][1], # end + summit=summit_pos, # summit position + peak_score=summit_q_score, # score at summit + pileup=summit_treat, # pileup + pscore=summit_p_score, # pvalue + fold_change=(summit_treat + self.pseudocount) / (summit_ctrl + self.pseudocount), # fold change + qscore=summit_q_score # qvalue + ) + # start a new peak + return True + + @cython.cfunc + def __close_peak_with_subpeaks(self, + peak_content: list, + peaks, + min_length: cython.int, + chrom: bytes, + smoothlen: cython.int, + score_array_s: list, + score_cutoff_s: list = [], + min_valley: cython.float = 0.9) -> bool: + """Algorithm implemented by Ben, to profile the pileup signals + within a peak region then find subpeak summits. This method is + highly recommended for TFBS or DNAase I sites. + + """ + tstart: cython.int + tend: cython.int + summit_index: cython.int + summit_offset: cython.int + start: cython.int + end: cython.int + i: cython.int + start_boundary: cython.int + m: cython.int + n: cython.int + ttreat_p: cython.double + tctrl_p: cython.double + tscore: cython.double + summit_treat: cython.double + summit_ctrl: cython.double + summit_p_score: cython.double + summit_q_score: cython.double + peakdata: cnp.ndarray(cython.float, ndim=1) + peakindices: cnp.ndarray(cython.int, ndim=1) + summit_offsets: cnp.ndarray(cython.int, ndim=1) + tlist_scores_p: cython.int + + peak_length = peak_content[-1][1] - peak_content[0][0] + + if peak_length < min_length: + return # if the region is too small, reject it + + # Add 10 bp padding to peak region so that we can get true minima + end = peak_content[-1][1] + 10 + start = peak_content[0][0] - 10 + if start < 0: + # this is the offof: set original peak boundary in peakdata list. + start_boundary = 10 + start + start = 0 + else: + # this is the offof: set original peak boundary in peakdata list. + start_boundary = 10 + + # save the scores (qscore) for each position in this region + peakdata = np.zeros(end - start, dtype='f4') + # save the indices for each position in this region + peakindices = np.zeros(end - start, dtype='i4') + for i in range(len(peak_content)): + (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] + tscore = ttreat_p # use pileup as general score to find summit + m = tstart - start + start_boundary + n = tend - start + start_boundary + peakdata[m:n] = tscore + peakindices[m:n] = i + + # offsets are the indices for summits in peakdata/peakindices array. + summit_offsets = maxima(peakdata, smoothlen) + + if summit_offsets.shape[0] == 0: + # **failsafe** if no summits, fall back on old approach # + return self.__close_peak_wo_subpeaks(peak_content, + peaks, + min_length, + chrom, + smoothlen, + score_array_s, + score_cutoff_s) + else: + # remove maxima that occurred in padding + m = np.searchsorted(summit_offsets, + start_boundary) + n = np.searchsorted(summit_offsets, + peak_length + start_boundary, + 'right') + summit_offsets = summit_offsets[m:n] + + summit_offsets = enforce_peakyness(peakdata, summit_offsets) + + # print "enforced:",summit_offsets + if summit_offsets.shape[0] == 0: + # **failsafe** if no summits, fall back on old approach # + return self.__close_peak_wo_subpeaks(peak_content, + peaks, + min_length, + chrom, + smoothlen, + score_array_s, + score_cutoff_s) + + # indices are those point to peak_content + summit_indices = peakindices[summit_offsets] + + summit_offsets -= start_boundary + + for summit_offset, summit_index in list(zip(summit_offsets, + summit_indices)): + + summit_treat = peak_content[summit_index][2] + summit_ctrl = peak_content[summit_index][3] + + summit_p_score = pscore_dict[(cython.cast(cython.int, + summit_treat), + summit_ctrl)] + summit_q_score = self.pqtable[summit_p_score] + + for i in range(len(score_cutoff_s)): + if score_cutoff_s[i] > score_array_s[i][peak_content[summit_index][4]]: + return False # not passed, then disgard this summit. + + peaks.add(chrom, + peak_content[0][0], + peak_content[-1][1], + summit=start + summit_offset, + peak_score=summit_q_score, + pileup=summit_treat, + pscore=summit_p_score, + fold_change=(summit_treat + self.pseudocount) / (summit_ctrl + self.pseudocount), # fold change + qscore=summit_q_score + ) + # start a new peak + return True + + @cython.cfunc + def __cal_pscore(self, + array1: cnp.ndarray, + array2: cnp.ndarray) -> cnp.ndarray: + + i: cython.long + array1_size: cython.long + s: cnp.ndarray + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + array1_size = array1.shape[0] + + for i in range(array1_size): + s_ptr[0] = get_pscore((cython.cast(cython.int, + a1_ptr[0]), + a2_ptr[0])) + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_qscore(self, + array1: cnp.ndarray, + array2: cnp.ndarray) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = self.pqtable[get_pscore((cython.cast(cython.int, + a1_ptr[0]), + a2_ptr[0]))] + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_logLR(self, + array1: cnp.ndarray, + array2: cnp.ndarray) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = get_logLR_asym((a1_ptr[0] + self.pseudocount, + a2_ptr[0] + self.pseudocount)) + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_logFE(self, + array1: cnp.ndarray, + array2: cnp.ndarray) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = get_logFE(a1_ptr[0] + self.pseudocount, + a2_ptr[0] + self.pseudocount) + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_FE(self, + array1: cnp.ndarray, + array2: cnp.ndarray) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = (a1_ptr[0] + self.pseudocount) / (a2_ptr[0] + self.pseudocount) + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __cal_subtraction(self, + array1: cnp.ndarray, + array2: cnp.ndarray) -> cnp.ndarray: + i: cython.long + s: cnp.ndarray + a1_ptr: cython.pointer(cython.float) + a2_ptr: cython.pointer(cython.float) + s_ptr: cython.pointer(cython.float) + + assert array1.shape[0] == array2.shape[0] + s = np.zeros(array1.shape[0], dtype="f4") + + a1_ptr = cython.cast(cython.pointer(cython.float), array1.data) + a2_ptr = cython.cast(cython.pointer(cython.float), array2.data) + s_ptr = cython.cast(cython.pointer(cython.float), s.data) + + for i in range(array1.shape[0]): + s_ptr[0] = a1_ptr[0] - a2_ptr[0] + s_ptr += 1 + a1_ptr += 1 + a2_ptr += 1 + return s + + @cython.cfunc + def __write_bedGraph_for_a_chromosome(self, chrom: bytes) -> bool: + """Write treat/control values for a certain chromosome into a + specified file handler. + + """ + pos_array: cnp.ndarray + treat_array: cnp.ndarray + ctrl_array: cnp.ndarray + pos_array_ptr: cython.pointer(cython.int) + treat_array_ptr: cython.pointer(cython.float) + ctrl_array_ptr: cython.pointer(cython.float) + l: cython.int + i: cython.int + p: cython.int + pre_p_t: cython.int + # current position, previous position for treat, previous position for control + pre_p_c: cython.int + pre_v_t: cython.float + pre_v_c: cython.float + v_t: cython.float + # previous value for treat, for control, current value for treat, for control + v_c: cython.float + # 1 if save_SPMR is false, or depth in million if save_SPMR is + # true. Note, while piling up and calling peaks, treatment and + # control have been scaled to the same depth, so we need to + # find what this 'depth' is. + denominator: cython.float + ft: cython.pointer(FILE) + fc: cython.pointer(FILE) + + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + pos_array_ptr = cython.cast(cython.pointer(cython.int), + pos_array.data) + treat_array_ptr = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_array_ptr = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + if self.save_SPMR: + if self.treat_scaling_factor == 1: + # in this case, control has been asked to be scaled to depth of treatment + denominator = self.treat.total/1e6 + else: + # in this case, treatment has been asked to be scaled to depth of control + denominator = self.ctrl.total/1e6 + else: + denominator = 1.0 + + l = pos_array.shape[0] + + if l == 0: # if there is no data, return + return False + + ft = self.bedGraph_treat_f + fc = self.bedGraph_ctrl_f + # t_write_func = self.bedGraph_treat.write + # c_write_func = self.bedGraph_ctrl.write + + pre_p_t = 0 + pre_p_c = 0 + pre_v_t = treat_array_ptr[0]/denominator + pre_v_c = ctrl_array_ptr[0]/denominator + treat_array_ptr += 1 + ctrl_array_ptr += 1 + + for i in range(1, l): + v_t = treat_array_ptr[0]/denominator + v_c = ctrl_array_ptr[0]/denominator + p = pos_array_ptr[0] + pos_array_ptr += 1 + treat_array_ptr += 1 + ctrl_array_ptr += 1 + + if abs(pre_v_t - v_t) > 1e-5: # precision is 5 digits + fprintf(ft, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_t, p, pre_v_t) + pre_v_t = v_t + pre_p_t = p + + if abs(pre_v_c - v_c) > 1e-5: # precision is 5 digits + fprintf(fc, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_c, p, pre_v_c) + pre_v_c = v_c + pre_p_c = p + + p = pos_array_ptr[0] + # last one + fprintf(ft, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_t, p, pre_v_t) + fprintf(fc, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_c, p, pre_v_c) + + return True + + @cython.ccall + def call_broadpeaks(self, + scoring_function_symbols: list, + lvl1_cutoff_s: list, + lvl2_cutoff_s: list, + min_length: cython.int = 200, + lvl1_max_gap: cython.int = 50, + lvl2_max_gap: cython.int = 400, + cutoff_analysis: bool = False): + """This function try to find enriched regions within which, + scores are continuously higher than a given cutoff for level + 1, and link them using the gap above level 2 cutoff with a + maximum length of lvl2_max_gap. + + scoring_function_s: symbols of functions to calculate + score. 'p' for pscore, 'q' for qscore, 'f' for fold change, + 's' for subtraction. for example: ['p', 'q'] + + lvl1_cutoff_s: of: list cutoffs at highly enriched regions, + corresponding to scoring functions. + + lvl2_cutoff_s: of: list cutoffs at less enriched regions, + corresponding to scoring functions. + + min_length : minimum peak length, default 200. + + lvl1_max_gap : maximum gap to merge nearby enriched peaks, + default 50. + + lvl2_max_gap : maximum length of linkage regions, default 400. + + Return both general PeakIO for: object highly enriched regions + and gapped broad regions in BroadPeakIO. + + """ + i: cython.int + j: cython.int + chrom: bytes + lvl1peaks: object + lvl1peakschrom: object + lvl1: object + lvl2peaks: object + lvl2peakschrom: object + lvl2: object + broadpeaks: object + chrs: set + tmppeakset: list + + lvl1peaks = PeakIO() + lvl2peaks = PeakIO() + + # prepare p-q table + if len(self.pqtable) == 0: + info("#3 Pre-compute pvalue-qvalue table...") + if cutoff_analysis: + info("#3 Cutoff value vs broad region calls will be analyzed!") + self.__pre_computes(max_gap=lvl2_max_gap, min_length=min_length) + else: + self.__cal_pvalue_qvalue_table() + + # prepare bedGraph file + if self.save_bedGraph: + + self.bedGraph_treat_f = fopen(self.bedGraph_treat_filename, "w") + self.bedGraph_ctrl_f = fopen(self.bedGraph_control_filename, "w") + info("#3 In the peak calling step, the following will be performed simultaneously:") + info("#3 Write bedGraph files for treatment pileup (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_treat_pileup.bdg") + info("#3 Write bedGraph files for control lambda (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_control_lambda.bdg") + + if self.trackline: + # this line is REQUIRED by the wiggle format for UCSC browser + tmp_bytes = ("track type=bedGraph name=\"treatment pileup\" description=\"treatment pileup after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() + fprintf(self.bedGraph_treat_f, tmp_bytes) + tmp_bytes = ("track type=bedGraph name=\"control lambda\" description=\"control lambda after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() + fprintf(self.bedGraph_ctrl_f, tmp_bytes) + + info("#3 Call peaks for each chromosome...") + for chrom in self.chromosomes: + self.__chrom_call_broadpeak_using_certain_criteria(lvl1peaks, + lvl2peaks, + chrom, + scoring_function_symbols, + lvl1_cutoff_s, + lvl2_cutoff_s, + min_length, + lvl1_max_gap, + lvl2_max_gap, + self.save_bedGraph) + + # close bedGraph file + if self.save_bedGraph: + fclose(self.bedGraph_treat_f) + fclose(self.bedGraph_ctrl_f) + # self.bedGraph_ctrl.close() + self.save_bedGraph = False + + # now combine lvl1 and lvl2 peaks + chrs = lvl1peaks.get_chr_names() + broadpeaks = BroadPeakIO() + # use lvl2_peaks as linking regions between lvl1_peaks + for chrom in sorted(chrs): + lvl1peakschrom = lvl1peaks.get_data_from_chrom(chrom) + lvl2peakschrom = lvl2peaks.get_data_from_chrom(chrom) + lvl1peakschrom_next = iter(lvl1peakschrom).__next__ + tmppeakset = [] # to temporarily store lvl1 region inside a lvl2 region + # our assumption is lvl1 regions should be included in lvl2 regions + try: + lvl1 = lvl1peakschrom_next() + for i in range(len(lvl2peakschrom)): + # for each lvl2 peak, find all lvl1 peaks inside + # I assume lvl1 peaks can be ALL covered by lvl2 peaks. + lvl2 = lvl2peakschrom[i] + + while True: + if lvl2["start"] <= lvl1["start"] and lvl1["end"] <= lvl2["end"]: + tmppeakset.append(lvl1) + lvl1 = lvl1peakschrom_next() + else: + # make a hierarchical broad peak + # print lvl2["start"], lvl2["end"], lvl2["score"] + self.__add_broadpeak(broadpeaks, + chrom, + lvl2, + tmppeakset) + tmppeakset = [] + break + except StopIteration: + # no more strong (aka lvl1) peaks left + self.__add_broadpeak(broadpeaks, + chrom, + lvl2, + tmppeakset) + tmppeakset = [] + # add the rest lvl2 peaks + for j in range(i+1, len(lvl2peakschrom)): + self.__add_broadpeak(broadpeaks, + chrom, + lvl2peakschrom[j], + tmppeakset) + + return broadpeaks + + @cython.cfunc + def __chrom_call_broadpeak_using_certain_criteria(self, + lvl1peaks, + lvl2peaks, + chrom: bytes, + scoring_function_s: list, + lvl1_cutoff_s: list, + lvl2_cutoff_s: list, + min_length: cython.int, + lvl1_max_gap: cython.int, + lvl2_max_gap: cython.int, + save_bedGraph: bool): + """Call peaks for a chromosome. + + Combination of criteria is allowed here. + + peaks: a PeakIO object + + scoring_function_s: symbols of functions to calculate score as + score=f(x, y) where x is treatment pileup, and y is control + pileup + + save_bedGraph : whether or not to save pileup and control into + a bedGraph file + + """ + i: cython.int + s: str + above_cutoff: cnp.ndarray + above_cutoff_endpos: cnp.ndarray + above_cutoff_startpos: cnp.ndarray + pos_array: cnp.ndarray + treat_array: cnp.ndarray + ctrl_array: cnp.ndarray + above_cutoff_index_array: cnp.ndarray + score_array_s: list # to: list keep different types of scores + peak_content: list + acs_ptr: cython.pointer(cython.int) + ace_ptr: cython.pointer(cython.int) + acia_ptr: cython.pointer(cython.int) + treat_array_ptr: cython.pointer(cython.float) + ctrl_array_ptr: cython.pointer(cython.float) + + assert len(scoring_function_s) == len(lvl1_cutoff_s), "number of functions and cutoffs should be the same!" + assert len(scoring_function_s) == len(lvl2_cutoff_s), "number of functions and cutoffs should be the same!" + + # first, build pileup, self.chr_pos_treat_ctrl + self.pileup_treat_ctrl_a_chromosome(chrom) + [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl + + # while save_bedGraph is true, invoke __write_bedGraph_for_a_chromosome + if save_bedGraph: + self.__write_bedGraph_for_a_chromosome(chrom) + + # keep all types of scores needed + score_array_s = [] + for i in range(len(scoring_function_s)): + s = scoring_function_s[i] + if s == 'p': + score_array_s.append(self.__cal_pscore(treat_array, + ctrl_array)) + elif s == 'q': + score_array_s.append(self.__cal_qscore(treat_array, + ctrl_array)) + elif s == 'f': + score_array_s.append(self.__cal_FE(treat_array, + ctrl_array)) + elif s == 's': + score_array_s.append(self.__cal_subtraction(treat_array, + ctrl_array)) + + # lvl1 : strong peaks + peak_content = [] # to store points above cutoff + + # get the regions with scores above cutoffs + above_cutoff = np.nonzero(apply_multiple_cutoffs(score_array_s, + lvl1_cutoff_s))[0] # this is not an optimized method. It would be better to store score array in a 2-D ndarray? + above_cutoff_index_array = np.arange(pos_array.shape[0], + dtype="int32")[above_cutoff] # indices + above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff + above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff + + if above_cutoff.size == 0: + # nothing above cutoff + return + + if above_cutoff[0] == 0: + # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] + above_cutoff_startpos[0] = 0 + + # first bit of region above cutoff + acs_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_startpos.data) + ace_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_endpos.data) + acia_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_index_array.data) + treat_array_ptr = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_array_ptr = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + + peak_content.append((ts, te, tp, cp, ti)) + acs_ptr += 1 # move ptr + ace_ptr += 1 + acia_ptr += 1 + lastp = te + + # peak_content.append((above_cutoff_startpos[0], above_cutoff_endpos[0], treat_array[above_cutoff_index_array[0]], ctrl_array[above_cutoff_index_array[0]], score_array_s, above_cutoff_index_array[0])) + for i in range(1, above_cutoff_startpos.size): + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + acs_ptr += 1 + ace_ptr += 1 + acia_ptr += 1 + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + tl = ts - lastp + if tl <= lvl1_max_gap: + # append + peak_content.append((ts, te, tp, cp, ti)) + lastp = te + else: + # close + self.__close_peak_for_broad_region(peak_content, + lvl1peaks, + min_length, + chrom, + lvl1_max_gap//2, + score_array_s) + peak_content = [(ts, te, tp, cp, ti),] + lastp = te # above_cutoff_endpos[i] + + # save the last peak + if peak_content: + self.__close_peak_for_broad_region(peak_content, + lvl1peaks, + min_length, + chrom, + lvl1_max_gap//2, + score_array_s) + + # lvl2 : weak peaks + peak_content = [] # to store points above cutoff + + # get the regions with scores above cutoffs + + # this is not an optimized method. It would be better to store score array in a 2-D ndarray? + above_cutoff = np.nonzero(apply_multiple_cutoffs(score_array_s, + lvl2_cutoff_s))[0] + + above_cutoff_index_array = np.arange(pos_array.shape[0], + dtype="i4")[above_cutoff] # indices + above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff + above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff + + if above_cutoff.size == 0: + # nothing above cutoff + return + + if above_cutoff[0] == 0: + # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] + above_cutoff_startpos[0] = 0 + + # first bit of region above cutoff + acs_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_startpos.data) + ace_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_endpos.data) + acia_ptr = cython.cast(cython.pointer(cython.int), + above_cutoff_index_array.data) + treat_array_ptr = cython.cast(cython.pointer(cython.float), + treat_array.data) + ctrl_array_ptr = cython.cast(cython.pointer(cython.float), + ctrl_array.data) + + ts = acs_ptr[0] + te = ace_ptr[0] + ti = acia_ptr[0] + tp = treat_array_ptr[ti] + cp = ctrl_array_ptr[ti] + peak_content.append((ts, te, tp, cp, ti)) + acs_ptr += 1 # move ptr + ace_ptr += 1 + acia_ptr += 1 + + lastp = te + for i in range(1, above_cutoff_startpos.size): + # for everything above cutoff + ts = acs_ptr[0] # get the start + te = ace_ptr[0] # get the end + ti = acia_ptr[0] # get the index + + acs_ptr += 1 # move ptr + ace_ptr += 1 + acia_ptr += 1 + tp = treat_array_ptr[ti] # get the treatment pileup + cp = ctrl_array_ptr[ti] # get the control pileup + tl = ts - lastp # get the distance from the current point to last position of existing peak_content + + if tl <= lvl2_max_gap: + # append + peak_content.append((ts, te, tp, cp, ti)) + lastp = te + else: + # close + self.__close_peak_for_broad_region(peak_content, + lvl2peaks, + min_length, + chrom, + lvl2_max_gap//2, + score_array_s) + + peak_content = [(ts, te, tp, cp, ti),] + lastp = te + + # save the last peak + if peak_content: + self.__close_peak_for_broad_region(peak_content, + lvl2peaks, + min_length, + chrom, + lvl2_max_gap//2, + score_array_s) + + return + + @cython.cfunc + def __close_peak_for_broad_region(self, + peak_content: list, + peaks, + min_length: cython.int, + chrom: bytes, + smoothlen: cython.int, + score_array_s: list, + score_cutoff_s: list = []) -> bool: + """Close the broad peak region, output peak boundaries, peak summit + and scores, then add the peak to peakIO object. + + peak_content contains [start, end, treat_p, ctrl_p, list_scores] + + peaks: a BroadPeakIO object + + """ + tstart: cython.int + tend: cython.int + i: cython.int + ttreat_p: cython.double + tctrl_p: cython.double + tlist_pileup: list + tlist_control: list + tlist_length: list + tlist_scores_p: cython.int + tarray_pileup: cnp.ndarray + tarray_control: cnp.ndarray + tarray_pscore: cnp.ndarray + tarray_qscore: cnp.ndarray + tarray_fc: cnp.ndarray + + peak_length = peak_content[-1][1] - peak_content[0][0] + if peak_length >= min_length: # if the peak is too small, reject it + tlist_pileup = [] + tlist_control = [] + tlist_length = [] + for i in range(len(peak_content)): # each position in broad peak + (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] + tlist_pileup.append(ttreat_p) + tlist_control.append(tctrl_p) + tlist_length.append(tend - tstart) + + tarray_pileup = np.array(tlist_pileup, dtype="f4") + tarray_control = np.array(tlist_control, dtype="f4") + tarray_pscore = self.__cal_pscore(tarray_pileup, tarray_control) + tarray_qscore = self.__cal_qscore(tarray_pileup, tarray_control) + tarray_fc = self.__cal_FE(tarray_pileup, tarray_control) + + peaks.add(chrom, # chromosome + peak_content[0][0], # start + peak_content[-1][1], # end + summit=0, + peak_score=mean_from_value_length(tarray_qscore, tlist_length), + pileup=mean_from_value_length(tarray_pileup, tlist_length), + pscore=mean_from_value_length(tarray_pscore, tlist_length), + fold_change=mean_from_value_length(tarray_fc, tlist_length), + qscore=mean_from_value_length(tarray_qscore, tlist_length), + ) + # if chrom == "chr1" and peak_content[0][0] == 237643 and peak_content[-1][1] == 237935: + # print tarray_qscore, tlist_length + # start a new peak + return True + + @cython.cfunc + def __add_broadpeak(self, + bpeaks, + chrom: bytes, + lvl2peak: object, + lvl1peakset: list): + """Internal function to create broad peak. + + *Note* lvl1peakset/strong_regions might be empty + """ + + blockNum: cython.int + start: cython.int + end: cython.int + blockSizes: bytes + blockStarts: bytes + thickStart: bytes + thickEnd: bytes + + start = lvl2peak["start"] + end = lvl2peak["end"] + + if not lvl1peakset: + # will complement by adding 1bps start and end to this region + # may change in the future if gappedPeak format was improved. + bpeaks.add(chrom, start, end, + score=lvl2peak["score"], + thickStart=(b"%d" % start), + thickEnd=(b"%d" % end), + blockNum=2, + blockSizes=b"1,1", + blockStarts=(b"0,%d" % (end-start-1)), + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) + return bpeaks + + thickStart = b"%d" % (lvl1peakset[0]["start"]) + thickEnd = b"%d" % (lvl1peakset[-1]["end"]) + blockNum = len(lvl1peakset) + blockSizes = b",".join([b"%d" % y for y in [x["length"] for x in lvl1peakset]]) + blockStarts = b",".join([b"%d" % x for x in getitem_then_subtract(lvl1peakset, start)]) + + # add 1bp left and/or right block if necessary + if int(thickStart) != start: + # add 1bp left block + thickStart = b"%d" % start + blockNum += 1 + blockSizes = b"1,"+blockSizes + blockStarts = b"0,"+blockStarts + if int(thickEnd) != end: + # add 1bp right block + thickEnd = b"%d" % end + blockNum += 1 + blockSizes = blockSizes + b",1" + blockStarts = blockStarts + b"," + (b"%d" % (end-start-1)) + + bpeaks.add(chrom, start, end, + score=lvl2peak["score"], + thickStart=thickStart, + thickEnd=thickEnd, + blockNum=blockNum, + blockSizes=blockSizes, + blockStarts=blockStarts, + pileup=lvl2peak["pileup"], + pscore=lvl2peak["pscore"], + fold_change=lvl2peak["fc"], + qscore=lvl2peak["qscore"]) + return bpeaks diff --git a/MACS3/Signal/CallPeakUnit.pyx b/MACS3/Signal/CallPeakUnit.pyx deleted file mode 100644 index c83aba7e..00000000 --- a/MACS3/Signal/CallPeakUnit.pyx +++ /dev/null @@ -1,1781 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# cython: linetrace=True -# Time-stamp: <2024-10-10 16:45:01 Tao Liu> - -"""Module for Calculate Scores. - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file LICENSE included with -the distribution). -""" - -# ------------------------------------ -# python modules -# ------------------------------------ - -from collections import Counter -from copy import copy -from time import time as ttime -import _pickle as cPickle -from tempfile import mkstemp -import os - -import logging -import MACS3.Utilities.Logger - -logger = logging.getLogger(__name__) -debug = logger.debug -info = logger.info -# ------------------------------------ -# Other modules -# ------------------------------------ -import numpy as np -cimport numpy as np -from numpy cimport uint8_t, uint16_t, uint32_t, uint64_t, int8_t, int16_t, int32_t, int64_t, float32_t, float64_t -from cpython cimport bool -from cykhash import PyObjectMap, Float32to32Map - -# ------------------------------------ -# C lib -# ------------------------------------ -from libc.stdio cimport * -from libc.math cimport exp,log,log10, M_LN10, log1p, erf, sqrt, floor, ceil - -# ------------------------------------ -# MACS3 modules -# ------------------------------------ -from MACS3.Signal.SignalProcessing import maxima, enforce_valleys, enforce_peakyness -from MACS3.IO.PeakIO import PeakIO, BroadPeakIO -from MACS3.Signal.FixWidthTrack import FWTrack -from MACS3.Signal.PairedEndTrack import PETrackI -from MACS3.Signal.Prob import poisson_cdf -# -------------------------------------------- -# cached pscore function and LR_asym functions -# -------------------------------------------- -pscore_dict = PyObjectMap() -logLR_dict = PyObjectMap() - -cdef float32_t get_pscore ( tuple t ): - """t: tuple of ( lambda, observation ) - """ - cdef: - float32_t val - if t in pscore_dict: - return pscore_dict[ t ] - else: - # calculate and cache - val = -1.0 * poisson_cdf ( t[0], t[1], False, True ) - pscore_dict[ t ] = val - return val - -cdef float32_t get_logLR_asym ( tuple t ): - """Calculate log10 Likelihood between H1 ( enriched ) and H0 ( - chromatin bias ). Set minus sign for depletion. - """ - cdef: - float32_t val - float32_t x - float32_t y - if t in logLR_dict: - return logLR_dict[ t ] - else: - x = t[0] - y = t[1] - # calculate and cache - if x > y: - val = (x*(log10(x)-log10(y))+y-x) - elif x < y: - val = (x*(-log10(x)+log10(y))-y+x) - else: - val = 0 - logLR_dict[ t ] = val - return val - -# ------------------------------------ -# constants -# ------------------------------------ -__version__ = "CallPeakUnit $Revision$" -__author__ = "Tao Liu " -__doc__ = "CallPeakUnit" - -LOG10_E = 0.43429448190325176 - -# ------------------------------------ -# Misc functions -# ------------------------------------ - -cdef void clean_up_ndarray ( np.ndarray x ): - # clean numpy ndarray in two steps - cdef: - int64_t i - i = x.shape[0] // 2 - x.resize( 100000 if i > 100000 else i, refcheck=False) - x.resize( 0, refcheck=False) - return - -cdef inline float32_t chi2_k1_cdf ( float32_t x ): - return erf( sqrt(x/2) ) - -cdef inline float32_t log10_chi2_k1_cdf ( float32_t x ): - return log10( erf( sqrt(x/2) ) ) - -cdef inline float32_t chi2_k2_cdf ( float32_t x ): - return 1 - exp( -x/2 ) - -cdef inline float32_t log10_chi2_k2_cdf ( float32_t x ): - return log1p( - exp( -x/2 ) ) * LOG10_E - -cdef inline float32_t chi2_k4_cdf ( float32_t x ): - return 1 - exp( -x/2 ) * ( 1 + x/2 ) - -cdef inline float32_t log10_chi2_k4_CDF ( float32_t x ): - return log1p( - exp( -x/2 ) * ( 1 + x/2 ) ) * LOG10_E - -cdef inline np.ndarray apply_multiple_cutoffs ( list multiple_score_arrays, list multiple_cutoffs ): - cdef: - int32_t i - np.ndarray ret - - ret = multiple_score_arrays[0] > multiple_cutoffs[0] - - for i in range(1,len(multiple_score_arrays)): - ret += multiple_score_arrays[i] > multiple_cutoffs[i] - - return ret - -cdef inline list get_from_multiple_scores ( list multiple_score_arrays, int32_t index ): - cdef: - list ret = [] - int32_t i - - for i in range(len(multiple_score_arrays)): - ret.append(multiple_score_arrays[i][index]) - return ret - - -cdef inline float32_t get_logFE ( float32_t x, float32_t y ): - """ return 100* log10 fold enrichment with +1 pseudocount. - """ - return log10( x/y ) - -cdef inline float32_t get_subtraction ( float32_t x, float32_t y): - """ return subtraction. - """ - return x - y - -cdef inline list getitem_then_subtract ( list peakset, int32_t start ): - cdef: - list a - - a = [x["start"] for x in peakset] - for i in range(len(a)): - a[i] = a[i] - start - return a - -cdef inline int32_t left_sum ( data, int32_t pos, int32_t width ): - """ - """ - return sum([data[x] for x in data if x <= pos and x >= pos - width]) - -cdef inline int32_t right_sum ( data, int32_t pos, int32_t width ): - """ - """ - return sum([data[x] for x in data if x >= pos and x <= pos + width]) - -cdef inline int32_t left_forward ( data, int32_t pos, int32_t window_size ): - return data.get(pos,0) - data.get(pos-window_size, 0) - -cdef inline int32_t right_forward ( data, int32_t pos, int32_t window_size ): - return data.get(pos + window_size, 0) - data.get(pos, 0) - -cdef float32_t median_from_value_length ( np.ndarray[np.float32_t, ndim=1] value, list length ): - """ - """ - cdef: - list tmp - int32_t c, tmp_l - float32_t tmp_v, mid_l - - c = 0 - tmp = sorted(list(zip( value, length ))) - mid_l = sum( length )/2 - for (tmp_v, tmp_l) in tmp: - c += tmp_l - if c > mid_l: - return tmp_v - -cdef float32_t mean_from_value_length ( np.ndarray[np.float32_t, ndim=1] value, list length ): - """take list of values and list of corresponding lengths, calculate the mean. - An important function for bedGraph type of data. - """ - cdef: - int32_t i - int32_t tmp_l, l - float64_t tmp_v, sum_v, tmp_sum #try to solve precision issue - float32_t ret - - sum_v = 0 - l = 0 - - for i in range( len(length) ): - tmp_l = length[ i ] - tmp_v = value[ i ] - tmp_sum = tmp_v * tmp_l - sum_v = tmp_sum + sum_v - l += tmp_l - - ret = (sum_v/l) - - return ret - - -cdef tuple find_optimal_cutoff( list x, list y ): - """Return the best cutoff x and y. - - We assume that total peak length increase exponentially while - decreasing cutoff value. But while cutoff decreases to a point - that background noises are captured, total length increases much - faster. So we fit a linear model by taking the first 10 points, - then look for the largest cutoff that - - """ - cdef: - np.ndarray npx, npy, npA - float32_t optimal_x, optimal_y - int64_t l, i - float32_t m, c # slop and intercept - float32_t sst # sum of squared total - float32_t sse # sum of squared error - float32_t rsq # R-squared - - l = len(x) - assert l == len(y) - npx = np.array( x ) - npy = np.log10( np.array( y ) ) - npA = np.vstack( [npx, np.ones(len(npx))] ).T - - for i in range( 10, l ): - # at least the largest 10 points - m, c = np.linalg.lstsq( npA[:i], npy[:i], rcond=None )[ 0 ] - sst = sum( ( npy[:i] - np.mean( npy[:i] ) ) ** 2 ) - sse = sum( ( npy[:i] - m*npx[:i] - c ) ** 2 ) - rsq = 1 - sse/sst - #print i, x[i], y[i], m, c, rsq - return ( 1.0, 1.0 ) - - - -# ------------------------------------ -# Classes -# ------------------------------------ -cdef class CallerFromAlignments: - """A unit to calculate scores and call peaks from alignments -- - FWTrack or PETrack objects. - - It will compute for each chromosome separately in order to save - memory usage. - """ - cdef: - object treat # FWTrack or PETrackI object for ChIP - object ctrl # FWTrack or PETrackI object for Control - - int32_t d # extension size for ChIP - list ctrl_d_s # extension sizes for Control. Can be multiple values - float32_t treat_scaling_factor # scaling factor for ChIP - list ctrl_scaling_factor_s # scaling factor for Control, corresponding to each extension size. - float32_t lambda_bg # minimum local bias to fill missing values - list chromosomes # name of common chromosomes in ChIP and Control data - float64_t pseudocount # the pseudocount used to calcuate logLR, FE or logFE - bytes bedGraph_filename_prefix # prefix will be added to _pileup.bdg for treatment and _lambda.bdg for control - - int32_t end_shift # shift of cutting ends before extension - bool trackline # whether trackline should be saved in bedGraph - bool save_bedGraph # whether to save pileup and local bias in bedGraph files - bool save_SPMR # whether to save pileup normalized by sequencing depth in million reads - bool no_lambda_flag # whether ignore local bias, and to use global bias instead - bool PE_mode # whether it's in PE mode, will be detected during initiation - - # temporary data buffer - list chr_pos_treat_ctrl # temporary [position, treat_pileup, ctrl_pileup] for a given chromosome - bytes bedGraph_treat_filename - bytes bedGraph_control_filename - FILE * bedGraph_treat_f - FILE * bedGraph_ctrl_f - - # data needed to be pre-computed before peak calling - object pqtable # remember pvalue->qvalue convertion; saved in cykhash Float32to32Map - bool pvalue_all_done # whether the pvalue of whole genome is all calculated. If yes, it's OK to calculate q-value. - - dict pvalue_npeaks # record for each pvalue cutoff, how many peaks can be called - dict pvalue_length # record for each pvalue cutoff, the total length of called peaks - float32_t optimal_p_cutoff # automatically decide the p-value cutoff ( can be translated into qvalue cutoff ) based - # on p-value to total peak length analysis. - bytes cutoff_analysis_filename # file to save the pvalue-npeaks-totallength table - - dict pileup_data_files # Record the names of temporary files for storing pileup values of each chromosome - - - def __init__ (self, treat, ctrl, - int32_t d = 200, list ctrl_d_s = [200, 1000, 10000], - float32_t treat_scaling_factor = 1.0, list ctrl_scaling_factor_s = [1.0, 0.2, 0.02], - bool stderr_on = False, - float32_t pseudocount = 1, - int32_t end_shift = 0, - float32_t lambda_bg = 0, - bool save_bedGraph = False, - str bedGraph_filename_prefix = "PREFIX", - str bedGraph_treat_filename = "TREAT.bdg", - str bedGraph_control_filename = "CTRL.bdg", - str cutoff_analysis_filename = "TMP.txt", - bool save_SPMR = False ): - """Initialize. - - A calculator is unique to each comparison of treat and - control. Treat_depth and ctrl_depth should not be changed - during calculation. - - treat and ctrl are either FWTrack or PETrackI objects. - - treat_depth and ctrl_depth are effective depth in million: - sequencing depth in million after - duplicates being filtered. If - treatment is scaled down to - control sample size, then this - should be control sample size in - million. And vice versa. - - d, sregion, lregion: d is the fragment size, sregion is the - small region size, lregion is the large - region size - - pseudocount: a pseudocount used to calculate logLR, FE or - logFE. Please note this value will not be changed - with normalization method. So if you really want - to set pseudocount 1 per million reads, set it - after you normalize treat and control by million - reads by `change_normalizetion_method(ord('M'))`. - - """ - cdef: - set chr1, chr2 - int32_t i - char * tmp - bytes tmp_bytes - float32_t p - # decide PE mode - if isinstance(treat, FWTrack): - self.PE_mode = False - elif isinstance(treat, PETrackI): - self.PE_mode = True - else: - raise Exception("Should be FWTrack or PETrackI object!") - # decide if there is control - self.treat = treat - if ctrl: - self.ctrl = ctrl - else: # while there is no control - self.ctrl = treat - self.trackline = False - self.d = d # note, self.d doesn't make sense in PE mode - self.ctrl_d_s = ctrl_d_s# note, self.d doesn't make sense in PE mode - self.treat_scaling_factor = treat_scaling_factor - self.ctrl_scaling_factor_s= ctrl_scaling_factor_s - self.end_shift = end_shift - self.lambda_bg = lambda_bg - self.pqtable = Float32to32Map( for_int = False ) # Float32 -> Float32 map - self.save_bedGraph = save_bedGraph - self.save_SPMR = save_SPMR - self.bedGraph_filename_prefix = bedGraph_filename_prefix.encode() - self.bedGraph_treat_filename = bedGraph_treat_filename.encode() - self.bedGraph_control_filename = bedGraph_control_filename.encode() - if not self.ctrl_d_s or not self.ctrl_scaling_factor_s: - self.no_lambda_flag = True - else: - self.no_lambda_flag = False - self.pseudocount = pseudocount - # get the common chromosome names from both treatment and control - chr1 = set(self.treat.get_chr_names()) - chr2 = set(self.ctrl.get_chr_names()) - self.chromosomes = sorted(list(chr1.intersection(chr2))) - - self.pileup_data_files = {} - self.pvalue_length = {} - self.pvalue_npeaks = {} - for p in np.arange( 0.3, 10, 0.3 ): # step for optimal cutoff is 0.3 in -log10pvalue, we try from pvalue 1E-10 (-10logp=10) to 0.5 (-10logp=0.3) - self.pvalue_length[ p ] = 0 - self.pvalue_npeaks[ p ] = 0 - self.optimal_p_cutoff = 0 - self.cutoff_analysis_filename = cutoff_analysis_filename.encode() - - cpdef destroy ( self ): - """Remove temporary files for pileup values of each chromosome. - - Note: This function MUST be called if the class object won't - be used anymore. - - """ - cdef: - bytes f - - for f in self.pileup_data_files.values(): - if os.path.isfile( f ): - os.unlink( f ) - return - - cpdef set_pseudocount( self, float32_t pseudocount ): - self.pseudocount = pseudocount - - cpdef enable_trackline( self ): - """Turn on trackline with bedgraph output - """ - self.trackline = True - - cdef __pileup_treat_ctrl_a_chromosome ( self, bytes chrom ): - """After this function is called, self.chr_pos_treat_ctrl will - be reset and assigned to the pileup values of the given - chromosome. - - """ - cdef: - list treat_pv, ctrl_pv - int64_t i - float32_t t - object f - str temp_filename - - assert chrom in self.chromosomes, "chromosome %s is not valid." % chrom - - # check backup file of pileup values. If not exists, create - # it. Otherwise, load them instead of calculating new pileup - # values. - if chrom in self.pileup_data_files: - try: - f = open( self.pileup_data_files[ chrom ],"rb" ) - self.chr_pos_treat_ctrl = cPickle.load( f ) - f.close() - return - except: - temp_fd, temp_filename = mkstemp() - os.close(temp_fd) - self.pileup_data_files[ chrom ] = temp_filename - else: - temp_fd, temp_filename = mkstemp() - os.close(temp_fd) - self.pileup_data_files[ chrom ] = temp_filename.encode() - - # reset or clean existing self.chr_pos_treat_ctrl - if self.chr_pos_treat_ctrl: # not a beautiful way to clean - clean_up_ndarray( self.chr_pos_treat_ctrl[0] ) - clean_up_ndarray( self.chr_pos_treat_ctrl[1] ) - clean_up_ndarray( self.chr_pos_treat_ctrl[2] ) - - if self.PE_mode: - treat_pv = self.treat.pileup_a_chromosome ( chrom, [self.treat_scaling_factor,], baseline_value = 0.0 ) - else: - treat_pv = self.treat.pileup_a_chromosome( chrom, [self.d,], [self.treat_scaling_factor,], baseline_value = 0.0, - directional = True, - end_shift = self.end_shift ) - - if not self.no_lambda_flag: - if self.PE_mode: - # note, we pileup up PE control as SE control because - # we assume the bias only can be captured at the - # surrounding regions of cutting sites from control experiments. - ctrl_pv = self.ctrl.pileup_a_chromosome_c( chrom, self.ctrl_d_s, self.ctrl_scaling_factor_s, baseline_value = self.lambda_bg ) - else: - ctrl_pv = self.ctrl.pileup_a_chromosome( chrom, self.ctrl_d_s, self.ctrl_scaling_factor_s, - baseline_value = self.lambda_bg, - directional = False ) - else: - ctrl_pv = [treat_pv[0][-1:], np.array([self.lambda_bg,], dtype="float32")] # set a global lambda - - self.chr_pos_treat_ctrl = self.__chrom_pair_treat_ctrl( treat_pv, ctrl_pv) - - # clean treat_pv and ctrl_pv - treat_pv = [] - ctrl_pv = [] - - # save data to temporary file - try: - f = open(self.pileup_data_files[ chrom ],"wb") - cPickle.dump( self.chr_pos_treat_ctrl, f , protocol=2 ) - f.close() - except: - # fail to write then remove the key in pileup_data_files - self.pileup_data_files.pop(chrom) - return - - cdef list __chrom_pair_treat_ctrl ( self, treat_pv, ctrl_pv ): - """*private* Pair treat and ctrl pileup for each region. - - treat_pv and ctrl_pv are [np.ndarray, np.ndarray]. - - return [p, t, c] list, each element is a numpy array. - """ - cdef: - list ret - int64_t pre_p, index_ret, it, ic, lt, lc - np.ndarray[np.int32_t, ndim=1] t_p, c_p, ret_p - np.ndarray[np.float32_t, ndim=1] t_v, c_v, ret_t, ret_c - - int32_t * t_p_ptr - int32_t * c_p_ptr - int32_t * ret_p_ptr - - float32_t * t_v_ptr - float32_t * c_v_ptr - float32_t * ret_t_ptr - float32_t * ret_c_ptr - - [ t_p, t_v ] = treat_pv - [ c_p, c_v ] = ctrl_pv - - lt = t_p.shape[0] - lc = c_p.shape[0] - - chrom_max_len = lt + lc - - ret_p = np.zeros( chrom_max_len, dtype="int32" ) # position - ret_t = np.zeros( chrom_max_len, dtype="float32" ) # value from treatment - ret_c = np.zeros( chrom_max_len, dtype="float32" ) # value from control - - t_p_ptr = t_p.data - t_v_ptr = t_v.data - c_p_ptr = c_p.data - c_v_ptr = c_v.data - ret_p_ptr = ret_p.data - ret_t_ptr = ret_t.data - ret_c_ptr = ret_c.data - - pre_p = 0 - index_ret = 0 - it = 0 - ic = 0 - - while it < lt and ic < lc: - if t_p_ptr[0] < c_p_ptr[0]: - # clip a region from pre_p to p1, then set pre_p as p1. - ret_p_ptr[0] = t_p_ptr[0] - ret_t_ptr[0] = t_v_ptr[0] - ret_c_ptr[0] = c_v_ptr[0] - ret_p_ptr += 1 - ret_t_ptr += 1 - ret_c_ptr += 1 - pre_p = t_p_ptr[0] - index_ret += 1 - # call for the next p1 and v1 - it += 1 - t_p_ptr += 1 - t_v_ptr += 1 - elif t_p_ptr[0] > c_p_ptr[0]: - # clip a region from pre_p to p2, then set pre_p as p2. - ret_p_ptr[0] = c_p_ptr[0] - ret_t_ptr[0] = t_v_ptr[0] - ret_c_ptr[0] = c_v_ptr[0] - ret_p_ptr += 1 - ret_t_ptr += 1 - ret_c_ptr += 1 - pre_p = c_p_ptr[0] - index_ret += 1 - # call for the next p2 and v2 - ic += 1 - c_p_ptr += 1 - c_v_ptr += 1 - else: - # from pre_p to p1 or p2, then set pre_p as p1 or p2. - ret_p_ptr[0] = t_p_ptr[0] - ret_t_ptr[0] = t_v_ptr[0] - ret_c_ptr[0] = c_v_ptr[0] - ret_p_ptr += 1 - ret_t_ptr += 1 - ret_c_ptr += 1 - pre_p = t_p_ptr[0] - index_ret += 1 - # call for the next p1, v1, p2, v2. - it += 1 - ic += 1 - t_p_ptr += 1 - t_v_ptr += 1 - c_p_ptr += 1 - c_v_ptr += 1 - - ret_p.resize( index_ret, refcheck=False) - ret_t.resize( index_ret, refcheck=False) - ret_c.resize( index_ret, refcheck=False) - return [ret_p, ret_t, ret_c] - - cdef np.ndarray __cal_score ( self, np.ndarray[np.float32_t, ndim=1] array1, np.ndarray[np.float32_t, ndim=1] array2, cal_func ): - cdef: - int64_t i - np.ndarray[np.float32_t, ndim=1] s - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="float32") - for i in range(array1.shape[0]): - s[i] = cal_func( array1[i], array2[i] ) - return s - - cdef void __cal_pvalue_qvalue_table ( self ): - """After this function is called, self.pqtable is built. All - chromosomes will be iterated. So it will take some time. - - """ - cdef: - bytes chrom - np.ndarray pos_array, treat_array, ctrl_array, score_array - dict pscore_stat - int64_t n, pre_p, length, pre_l, l, i, j - float32_t this_v, pre_v, v, q, pre_q - int64_t N, k, this_l - float32_t f - list unique_values - int32_t * pos_ptr - float32_t * treat_value_ptr - float32_t * ctrl_value_ptr - - debug ( "Start to calculate pvalue stat..." ) - - pscore_stat = {} #dict() - for i in range( len( self.chromosomes ) ): - chrom = self.chromosomes[ i ] - pre_p = 0 - - self.__pileup_treat_ctrl_a_chromosome( chrom ) - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - - pos_ptr = pos_array.data - treat_value_ptr = treat_array.data - ctrl_value_ptr = ctrl_array.data - - for j in range(pos_array.shape[0]): - this_v = get_pscore( ((treat_value_ptr[0]), ctrl_value_ptr[0] ) ) - this_l = pos_ptr[0] - pre_p - if this_v in pscore_stat: - pscore_stat[ this_v ] += this_l - else: - pscore_stat[ this_v ] = this_l - pre_p = pos_ptr[0] - pos_ptr += 1 - treat_value_ptr += 1 - ctrl_value_ptr += 1 - - N = sum(pscore_stat.values()) # total length - k = 1 # rank - f = -log10(N) - pre_v = -2147483647 - pre_l = 0 - pre_q = 2147483647 # save the previous q-value - - self.pqtable = Float32to32Map( for_int = False ) - unique_values = sorted(list(pscore_stat.keys()), reverse=True) - for i in range(len(unique_values)): - v = unique_values[i] - l = pscore_stat[v] - q = v + (log10(k) + f) - if q > pre_q: - q = pre_q - if q <= 0: - q = 0 - break - #q = max(0,min(pre_q,q)) # make q-score monotonic - self.pqtable[ v ] = q - pre_q = q - k += l - # bottom rank pscores all have qscores 0 - for j in range(i, len(unique_values) ): - v = unique_values[ j ] - self.pqtable[ v ] = 0 - return - - cdef void __pre_computes ( self, int32_t max_gap = 50, int32_t min_length = 200 ): - """After this function is called, self.pqtable and self.pvalue_length is built. All - chromosomes will be iterated. So it will take some time. - - """ - cdef: - bytes chrom - np.ndarray pos_array, treat_array, ctrl_array, score_array - dict pscore_stat - int64_t n, pre_p, this_p, length, j, pre_l, l, i - float32_t q, pre_q, this_t, this_c - float32_t this_v, pre_v, v, cutoff - int64_t N, k, this_l - float32_t f - list unique_values - float64_t t0, t1, t - - np.ndarray above_cutoff, above_cutoff_endpos, above_cutoff_startpos - list peak_content - int64_t peak_length, total_l, total_p - - list tmplist - - int32_t * acs_ptr # above cutoff start position pointer - int32_t * ace_ptr # above cutoff end position pointer - int32_t * pos_array_ptr # position array pointer - float32_t * score_array_ptr # score array pointer - - debug ( "Start to calculate pvalue stat..." ) - - # tmplist contains a list of log pvalue cutoffs from 0.3 to 10 - tmplist = [round(x,5) for x in sorted( list(np.arange(0.3, 10.0, 0.3)), reverse = True )] - - pscore_stat = {} #dict() - #print (list(pscore_stat.keys())) - #print (list(self.pvalue_length.keys())) - #print (list(self.pvalue_npeaks.keys())) - for i in range( len( self.chromosomes ) ): - chrom = self.chromosomes[ i ] - self.__pileup_treat_ctrl_a_chromosome( chrom ) - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - - score_array = self.__cal_pscore( treat_array, ctrl_array ) - - for n in range( len( tmplist ) ): - cutoff = tmplist[ n ] - total_l = 0 # total length in potential peak - total_p = 0 - - # get the regions with scores above cutoffs - above_cutoff = np.nonzero( score_array > cutoff )[0]# this is not an optimized method. It would be better to store score array in a 2-D ndarray? - above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff - above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff - - if above_cutoff_endpos.size == 0: - continue - - # first bit of region above cutoff - acs_ptr = above_cutoff_startpos.data - ace_ptr = above_cutoff_endpos.data - - peak_content = [( acs_ptr[ 0 ], ace_ptr[ 0 ] ), ] - lastp = ace_ptr[ 0 ] - acs_ptr += 1 - ace_ptr += 1 - - for i in range( 1, above_cutoff_startpos.size ): - tl = acs_ptr[ 0 ] - lastp - if tl <= max_gap: - peak_content.append( ( acs_ptr[ 0 ], ace_ptr[ 0 ] ) ) - else: - peak_length = peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] - if peak_length >= min_length: # if the peak is too small, reject it - total_l += peak_length - total_p += 1 - peak_content = [ ( acs_ptr[ 0 ], ace_ptr[ 0 ] ), ] - lastp = ace_ptr[ 0 ] - acs_ptr += 1 - ace_ptr += 1 - - if peak_content: - peak_length = peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] - if peak_length >= min_length: # if the peak is too small, reject it - total_l += peak_length - total_p += 1 - self.pvalue_length[ cutoff ] = self.pvalue_length.get( cutoff, 0 ) + total_l - self.pvalue_npeaks[ cutoff ] = self.pvalue_npeaks.get( cutoff, 0 ) + total_p - - pos_array_ptr = pos_array.data - score_array_ptr = score_array.data - - pre_p = 0 - for i in range(pos_array.shape[0]): - this_p = pos_array_ptr[ 0 ] - this_l = this_p - pre_p - this_v = score_array_ptr[ 0 ] - if this_v in pscore_stat: - pscore_stat[ this_v ] += this_l - else: - pscore_stat[ this_v ] = this_l - pre_p = this_p #pos_array[ i ] - pos_array_ptr += 1 - score_array_ptr += 1 - - #debug ( "make pscore_stat cost %.5f seconds" % t ) - - # add all pvalue cutoffs from cutoff-analysis part. So that we - # can get the corresponding qvalues for them. - for cutoff in tmplist: - if cutoff not in pscore_stat: - pscore_stat[ cutoff ] = 0 - - nhval = 0 - - N = sum(pscore_stat.values()) # total length - k = 1 # rank - f = -log10(N) - pre_v = -2147483647 - pre_l = 0 - pre_q = 2147483647 # save the previous q-value - - self.pqtable = Float32to32Map( for_int = False ) #{} - unique_values = sorted(list(pscore_stat.keys()), reverse=True) #sorted(unique_values,reverse=True) - for i in range(len(unique_values)): - v = unique_values[i] - l = pscore_stat[v] - q = v + (log10(k) + f) - if q > pre_q: - q = pre_q - if q <= 0: - q = 0 - break - #q = max(0,min(pre_q,q)) # make q-score monotonic - self.pqtable[ v ] = q - pre_v = v - pre_q = q - k+=l - for j in range(i, len(unique_values) ): - v = unique_values[ j ] - self.pqtable[ v ] = 0 - - # write pvalue and total length of predicted peaks - # this is the output from cutoff-analysis - fhd = open( self.cutoff_analysis_filename, "w" ) - fhd.write( "pscore\tqscore\tnpeaks\tlpeaks\tavelpeak\n" ) - x = [] - y = [] - for cutoff in tmplist: - if self.pvalue_npeaks[ cutoff ] > 0: - fhd.write( "%.2f\t%.2f\t%d\t%d\t%.2f\n" % ( cutoff, self.pqtable[ cutoff ], self.pvalue_npeaks[ cutoff ], self.pvalue_length[ cutoff ], self.pvalue_length[ cutoff ]/self.pvalue_npeaks[ cutoff ] ) ) - x.append( cutoff ) - y.append( self.pvalue_length[ cutoff ] ) - fhd.close() - info( "#3 Analysis of cutoff vs num of peaks or total length has been saved in %s" % self.cutoff_analysis_filename ) - #info( "#3 Suggest a cutoff..." ) - #optimal_cutoff, optimal_length = find_optimal_cutoff( x, y ) - #info( "#3 -10log10pvalue cutoff %.2f will call approximately %.0f bps regions as significant regions" % ( optimal_cutoff, optimal_length ) ) - #print (list(pqtable.keys())) - #print (list(self.pvalue_length.keys())) - #print (list(self.pvalue_npeaks.keys())) - return - - cpdef call_peaks ( self, list scoring_function_symbols, list score_cutoff_s, int32_t min_length = 200, - int32_t max_gap = 50, bool call_summits = False, bool cutoff_analysis = False ): - """Call peaks for all chromosomes. Return a PeakIO object. - - scoring_function_s: symbols of functions to calculate score. 'p' for pscore, 'q' for qscore, 'f' for fold change, 's' for subtraction. for example: ['p', 'q'] - score_cutoff_s : cutoff values corresponding to scoring functions - min_length : minimum length of peak - max_gap : maximum gap of 'insignificant' regions within a peak. Note, for PE_mode, max_gap and max_length are both set as fragment length. - call_summits : boolean. Whether or not call sub-peaks. - save_bedGraph : whether or not to save pileup and control into a bedGraph file - """ - cdef: - bytes chrom - bytes tmp_bytes - - peaks = PeakIO() - - # prepare p-q table - if len( self.pqtable ) == 0: - info("#3 Pre-compute pvalue-qvalue table...") - if cutoff_analysis: - info("#3 Cutoff vs peaks called will be analyzed!") - self.__pre_computes( max_gap = max_gap, min_length = min_length ) - else: - self.__cal_pvalue_qvalue_table() - - - # prepare bedGraph file - if self.save_bedGraph: - self.bedGraph_treat_f = fopen( self.bedGraph_treat_filename, "w" ) - self.bedGraph_ctrl_f = fopen( self.bedGraph_control_filename, "w" ) - - info ("#3 In the peak calling step, the following will be performed simultaneously:") - info ("#3 Write bedGraph files for treatment pileup (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_treat_pileup.bdg") - info ("#3 Write bedGraph files for control lambda (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_control_lambda.bdg") - - if self.save_SPMR: - info ( "#3 --SPMR is requested, so pileup will be normalized by sequencing depth in million reads." ) - elif self.treat_scaling_factor == 1: - info ( "#3 Pileup will be based on sequencing depth in treatment." ) - else: - info ( "#3 Pileup will be based on sequencing depth in control." ) - - if self.trackline: - # this line is REQUIRED by the wiggle format for UCSC browser - tmp_bytes = ("track type=bedGraph name=\"treatment pileup\" description=\"treatment pileup after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() - fprintf( self.bedGraph_treat_f, tmp_bytes ) - tmp_bytes = ("track type=bedGraph name=\"control lambda\" description=\"control lambda after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() - fprintf( self.bedGraph_ctrl_f, tmp_bytes ) - - info("#3 Call peaks for each chromosome...") - for chrom in self.chromosomes: - # treat/control bedGraph will be saved if requested by user. - self.__chrom_call_peak_using_certain_criteria ( peaks, chrom, scoring_function_symbols, score_cutoff_s, min_length, max_gap, call_summits, self.save_bedGraph ) - - # close bedGraph file - if self.save_bedGraph: - fclose(self.bedGraph_treat_f) - fclose(self.bedGraph_ctrl_f) - self.save_bedGraph = False - - return peaks - - cdef void __chrom_call_peak_using_certain_criteria ( self, peaks, bytes chrom, list scoring_function_s, list score_cutoff_s, int32_t min_length, - int32_t max_gap, bool call_summits, bool save_bedGraph ): - """ Call peaks for a chromosome. - - Combination of criteria is allowed here. - - peaks: a PeakIO object, the return value of this function - scoring_function_s: symbols of functions to calculate score as score=f(x, y) where x is treatment pileup, and y is control pileup - save_bedGraph : whether or not to save pileup and control into a bedGraph file - """ - cdef: - float64_t t0 - int32_t i, n - str s - np.ndarray above_cutoff - np.ndarray[np.int32_t, ndim=1] above_cutoff_endpos, above_cutoff_startpos, pos_array, above_cutoff_index_array - - np.ndarray[np.float32_t, ndim=1] treat_array, ctrl_array - list score_array_s # list to keep different types of scores - list peak_content # to store information for a - # chunk in a peak region, it - # contains lists of: 1. left - # position; 2. right - # position; 3. treatment - # value; 4. control value; - # 5. list of scores at this - # chunk - int64_t tl, lastp, ts, te, ti - float32_t tp, cp - int32_t * acs_ptr - int32_t * ace_ptr - int32_t * acia_ptr - float32_t * treat_array_ptr - float32_t * ctrl_array_ptr - - - assert len(scoring_function_s) == len(score_cutoff_s), "number of functions and cutoffs should be the same!" - - peak_content = [] # to store points above cutoff - - # first, build pileup, self.chr_pos_treat_ctrl - # this step will be speeped up if pqtable is pre-computed. - self.__pileup_treat_ctrl_a_chromosome( chrom ) - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - - # while save_bedGraph is true, invoke __write_bedGraph_for_a_chromosome - if save_bedGraph: - self.__write_bedGraph_for_a_chromosome ( chrom ) - - # keep all types of scores needed - #t0 = ttime() - score_array_s = [] - for i in range(len(scoring_function_s)): - s = scoring_function_s[i] - if s == 'p': - score_array_s.append( self.__cal_pscore( treat_array, ctrl_array ) ) - elif s == 'q': - score_array_s.append( self.__cal_qscore( treat_array, ctrl_array ) ) - elif s == 'f': - score_array_s.append( self.__cal_FE( treat_array, ctrl_array ) ) - elif s == 's': - score_array_s.append( self.__cal_subtraction( treat_array, ctrl_array ) ) - - # get the regions with scores above cutoffs - above_cutoff = np.nonzero( apply_multiple_cutoffs(score_array_s,score_cutoff_s) )[0] # this is not an optimized method. It would be better to store score array in a 2-D ndarray? - above_cutoff_index_array = np.arange(pos_array.shape[0],dtype="int32")[above_cutoff] # indices - above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff - above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff - - if above_cutoff.size == 0: - # nothing above cutoff - return - - if above_cutoff[0] == 0: - # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] - above_cutoff_startpos[0] = 0 - - #print "apply cutoff -- chrom:",chrom," time:", ttime() - t0 - # start to build peak regions - #t0 = ttime() - - # first bit of region above cutoff - acs_ptr = above_cutoff_startpos.data - ace_ptr = above_cutoff_endpos.data - acia_ptr= above_cutoff_index_array.data - treat_array_ptr = treat_array.data - ctrl_array_ptr = ctrl_array.data - - ts = acs_ptr[ 0 ] - te = ace_ptr[ 0 ] - ti = acia_ptr[ 0 ] - tp = treat_array_ptr[ ti ] - cp = ctrl_array_ptr[ ti ] - - peak_content.append( ( ts, te, tp, cp, ti ) ) - lastp = te - acs_ptr += 1 - ace_ptr += 1 - acia_ptr+= 1 - - for i in range( 1, above_cutoff_startpos.shape[0] ): - ts = acs_ptr[ 0 ] - te = ace_ptr[ 0 ] - ti = acia_ptr[ 0 ] - acs_ptr += 1 - ace_ptr += 1 - acia_ptr+= 1 - tp = treat_array_ptr[ ti ] - cp = ctrl_array_ptr[ ti ] - tl = ts - lastp - if tl <= max_gap: - # append. - peak_content.append( ( ts, te, tp, cp, ti ) ) - lastp = te #above_cutoff_endpos[i] - else: - # close - if call_summits: - self.__close_peak_with_subpeaks (peak_content, peaks, min_length, chrom, min_length, score_array_s, score_cutoff_s = score_cutoff_s ) # smooth length is min_length, i.e. fragment size 'd' - else: - self.__close_peak_wo_subpeaks (peak_content, peaks, min_length, chrom, min_length, score_array_s, score_cutoff_s = score_cutoff_s ) # smooth length is min_length, i.e. fragment size 'd' - peak_content = [ ( ts, te, tp, cp, ti ), ] - lastp = te #above_cutoff_endpos[i] - # save the last peak - if not peak_content: - return - else: - if call_summits: - self.__close_peak_with_subpeaks (peak_content, peaks, min_length, chrom, min_length, score_array_s, score_cutoff_s = score_cutoff_s ) # smooth length is min_length, i.e. fragment size 'd' - else: - self.__close_peak_wo_subpeaks (peak_content, peaks, min_length, chrom, min_length, score_array_s, score_cutoff_s = score_cutoff_s ) # smooth length is min_length, i.e. fragment size 'd' - - #print "close peaks -- chrom:",chrom," time:", ttime() - t0 - return - - cdef bool __close_peak_wo_subpeaks (self, list peak_content, peaks, int32_t min_length, - bytes chrom, int32_t smoothlen, list score_array_s, list score_cutoff_s=[]): - """Close the peak region, output peak boundaries, peak summit - and scores, then add the peak to peakIO object. - - peak_content contains [start, end, treat_p, ctrl_p, index_in_score_array] - - peaks: a PeakIO object - - """ - cdef: - int32_t summit_pos, tstart, tend, tmpindex, summit_index, i, midindex - float64_t treat_v, ctrl_v, tsummitvalue, ttreat_p, tctrl_p, tscore, summit_treat, summit_ctrl, summit_p_score, summit_q_score - int32_t tlist_scores_p - - peak_length = peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] - if peak_length >= min_length: # if the peak is too small, reject it - tsummit = [] - summit_pos = 0 - summit_value = 0 - for i in range(len(peak_content)): - (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] - tscore = ttreat_p # use pscore as general score to find summit - if not summit_value or summit_value < tscore: - tsummit = [(tend + tstart) // 2, ] - tsummit_index = [ i, ] - summit_value = tscore - elif summit_value == tscore: - # remember continuous summit values - tsummit.append((tend + tstart) // 2) - tsummit_index.append( i ) - # the middle of all highest points in peak region is defined as summit - midindex = (len(tsummit) + 1) // 2 - 1 - summit_pos = tsummit[ midindex ] - summit_index = tsummit_index[ midindex ] - - summit_treat = peak_content[ summit_index ][ 2 ] - summit_ctrl = peak_content[ summit_index ][ 3 ] - - # this is a double-check to see if the summit can pass cutoff values. - for i in range(len(score_cutoff_s)): - if score_cutoff_s[i] > score_array_s[ i ][ peak_content[ summit_index ][ 4 ] ]: - return False # not passed, then disgard this peak. - - summit_p_score = pscore_dict[ ( (summit_treat), summit_ctrl ) ] #get_pscore(( (summit_treat), summit_ctrl ) ) - summit_q_score = self.pqtable[ summit_p_score ] - - peaks.add( chrom, # chromosome - peak_content[0][0], # start - peak_content[-1][1], # end - summit = summit_pos, # summit position - peak_score = summit_q_score, # score at summit - pileup = summit_treat, # pileup - pscore = summit_p_score, # pvalue - fold_change = ( summit_treat + self.pseudocount ) / ( summit_ctrl + self.pseudocount ), # fold change - qscore = summit_q_score # qvalue - ) - # start a new peak - return True - - cdef bool __close_peak_with_subpeaks (self, list peak_content, peaks, int32_t min_length, - bytes chrom, int32_t smoothlen, list score_array_s, list score_cutoff_s=[], - float32_t min_valley = 0.9 ): - """Algorithm implemented by Ben, to profile the pileup signals - within a peak region then find subpeak summits. This method is - highly recommended for TFBS or DNAase I sites. - - """ - cdef: - int32_t summit_pos, tstart, tend, tmpindex, summit_index, summit_offset - int32_t start, end, i, j, start_boundary, m, n, l - float64_t summit_value, tvalue, tsummitvalue, ttreat_p, tctrl_p, tscore, summit_treat, summit_ctrl, summit_p_score, summit_q_score - np.ndarray[np.float32_t, ndim=1] peakdata - np.ndarray[np.int32_t, ndim=1] peakindices, summit_offsets - int32_t tlist_scores_p - - peak_length = peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] - - if peak_length < min_length: return # if the region is too small, reject it - - # Add 10 bp padding to peak region so that we can get true minima - end = peak_content[ -1 ][ 1 ] + 10 - start = peak_content[ 0 ][ 0 ] - 10 - if start < 0: - start_boundary = 10 + start # this is the offset of original peak boundary in peakdata list. - start = 0 - else: - start_boundary = 10 # this is the offset of original peak boundary in peakdata list. - - peakdata = np.zeros(end - start, dtype='float32') # save the scores (qscore) for each position in this region - peakindices = np.zeros(end - start, dtype='int32') # save the indices for each position in this region - for i in range(len(peak_content)): - (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] - tscore = ttreat_p # use pileup as general score to find summit - m = tstart - start + start_boundary - n = tend - start + start_boundary - peakdata[m:n] = tscore - peakindices[m:n] = i - - summit_offsets = maxima(peakdata, smoothlen) # offsets are the indices for summits in peakdata/peakindices array. - - if summit_offsets.shape[0] == 0: - # **failsafe** if no summits, fall back on old approach # - return self.__close_peak_wo_subpeaks(peak_content, peaks, min_length, chrom, smoothlen, score_array_s, score_cutoff_s) - else: - # remove maxima that occurred in padding - m = np.searchsorted(summit_offsets, start_boundary) - n = np.searchsorted(summit_offsets, peak_length + start_boundary, 'right') - summit_offsets = summit_offsets[m:n] - - summit_offsets = enforce_peakyness(peakdata, summit_offsets) - - #print "enforced:",summit_offsets - if summit_offsets.shape[0] == 0: - # **failsafe** if no summits, fall back on old approach # - return self.__close_peak_wo_subpeaks(peak_content, peaks, min_length, chrom, smoothlen, score_array_s, score_cutoff_s) - - summit_indices = peakindices[summit_offsets] # indices are those point to peak_content - summit_offsets -= start_boundary - - for summit_offset, summit_index in list(zip(summit_offsets, summit_indices)): - - summit_treat = peak_content[ summit_index ][ 2 ] - summit_ctrl = peak_content[ summit_index ][ 3 ] - - summit_p_score = pscore_dict[ ( (summit_treat), summit_ctrl ) ] # get_pscore(( (summit_treat), summit_ctrl ) ) - summit_q_score = self.pqtable[ summit_p_score ] - - for i in range(len(score_cutoff_s)): - if score_cutoff_s[i] > score_array_s[ i ][ peak_content[ summit_index ][ 4 ] ]: - return False # not passed, then disgard this summit. - - peaks.add( chrom, - peak_content[ 0 ][ 0 ], - peak_content[ -1 ][ 1 ], - summit = start + summit_offset, - peak_score = summit_q_score, - pileup = summit_treat, - pscore = summit_p_score, - fold_change = (summit_treat + self.pseudocount ) / ( summit_ctrl + self.pseudocount ), # fold change - qscore = summit_q_score - ) - # start a new peak - return True - - cdef np.ndarray __cal_pscore ( self, np.ndarray[np.float32_t, ndim=1] array1, np.ndarray[np.float32_t, ndim=1] array2 ): - cdef: - int64_t i, array1_size - np.ndarray[np.float32_t, ndim=1] s - float32_t * a1_ptr - float32_t * a2_ptr - float32_t * s_ptr - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="float32") - - a1_ptr = array1.data - a2_ptr = array2.data - s_ptr = s.data - - array1_size = array1.shape[0] - - for i in range(array1_size): - s_ptr[0] = get_pscore(( (a1_ptr[0]), a2_ptr[0] )) - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - cdef np.ndarray __cal_qscore ( self, np.ndarray[np.float32_t, ndim=1] array1, np.ndarray[np.float32_t, ndim=1] array2 ): - cdef: - int64_t i, array1_size - np.ndarray[np.float32_t, ndim=1] s - float32_t * a1_ptr - float32_t * a2_ptr - float32_t * s_ptr - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="float32") - - a1_ptr = array1.data - a2_ptr = array2.data - s_ptr = s.data - - for i in range(array1.shape[0]): - s_ptr[0] = self.pqtable[ get_pscore(( (a1_ptr[0]), a2_ptr[0] )) ] - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - cdef np.ndarray __cal_logLR ( self, np.ndarray[np.float32_t, ndim=1] array1, np.ndarray[np.float32_t, ndim=1] array2 ): - cdef: - int64_t i, array1_size - np.ndarray[np.float32_t, ndim=1] s - float32_t * a1_ptr - float32_t * a2_ptr - float32_t * s_ptr - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="float32") - - a1_ptr = array1.data - a2_ptr = array2.data - s_ptr = s.data - - for i in range(array1.shape[0]): - s_ptr[0] = get_logLR_asym( (a1_ptr[0] + self.pseudocount, a2_ptr[0] + self.pseudocount ) ) - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - cdef np.ndarray __cal_logFE ( self, np.ndarray[np.float32_t, ndim=1] array1, np.ndarray[np.float32_t, ndim=1] array2 ): - cdef: - int64_t i, array1_size - np.ndarray[np.float32_t, ndim=1] s - float32_t * a1_ptr - float32_t * a2_ptr - float32_t * s_ptr - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="float32") - - a1_ptr = array1.data - a2_ptr = array2.data - s_ptr = s.data - - for i in range(array1.shape[0]): - s_ptr[0] = get_logFE( a1_ptr[0] + self.pseudocount, a2_ptr[0] + self.pseudocount ) - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - cdef np.ndarray __cal_FE ( self, np.ndarray[np.float32_t, ndim=1] array1, np.ndarray[np.float32_t, ndim=1] array2 ): - cdef: - int64_t i, array1_size - np.ndarray[np.float32_t, ndim=1] s - float32_t * a1_ptr - float32_t * a2_ptr - float32_t * s_ptr - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="float32") - - a1_ptr = array1.data - a2_ptr = array2.data - s_ptr = s.data - - for i in range(array1.shape[0]): - s_ptr[0] = (a1_ptr[0] + self.pseudocount) / ( a2_ptr[0] + self.pseudocount ) - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - cdef np.ndarray __cal_subtraction ( self, np.ndarray[np.float32_t, ndim=1] array1, np.ndarray[np.float32_t, ndim=1] array2 ): - cdef: - int64_t i, array1_size - np.ndarray[np.float32_t, ndim=1] s - float32_t * a1_ptr - float32_t * a2_ptr - float32_t * s_ptr - - assert array1.shape[0] == array2.shape[0] - s = np.zeros(array1.shape[0], dtype="float32") - - a1_ptr = array1.data - a2_ptr = array2.data - s_ptr = s.data - - for i in range(array1.shape[0]): - s_ptr[0] = a1_ptr[0] - a2_ptr[0] - s_ptr += 1 - a1_ptr += 1 - a2_ptr += 1 - return s - - - cdef bool __write_bedGraph_for_a_chromosome ( self, bytes chrom ): - """Write treat/control values for a certain chromosome into a - specified file handler. - - """ - cdef: - np.ndarray[np.int32_t, ndim=1] pos_array - np.ndarray[np.float32_t, ndim=1] treat_array, ctrl_array - int32_t * pos_array_ptr - float32_t * treat_array_ptr - float32_t * ctrl_array_ptr - int32_t l, i - int32_t p, pre_p_t, pre_p_c # current position, previous position for treat, previous position for control - float32_t pre_v_t, pre_v_c, v_t, v_c # previous value for treat, for control, current value for treat, for control - float32_t denominator # 1 if save_SPMR is false, or depth in million if save_SPMR is true. Note, while piling up and calling peaks, treatment and control have been scaled to the same depth, so we need to find what this 'depth' is. - FILE * ft - FILE * fc - basestring tmp_bytes - - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - pos_array_ptr = pos_array.data - treat_array_ptr = treat_array.data - ctrl_array_ptr = ctrl_array.data - - if self.save_SPMR: - if self.treat_scaling_factor == 1: - # in this case, control has been asked to be scaled to depth of treatment - denominator = self.treat.total/1e6 - else: - # in this case, treatment has been asked to be scaled to depth of control - denominator = self.ctrl.total/1e6 - else: - denominator = 1.0 - - l = pos_array.shape[ 0 ] - - if l == 0: # if there is no data, return - return False - - ft = self.bedGraph_treat_f - fc = self.bedGraph_ctrl_f - #t_write_func = self.bedGraph_treat.write - #c_write_func = self.bedGraph_ctrl.write - - pre_p_t = 0 - pre_p_c = 0 - pre_v_t = treat_array_ptr[ 0 ]/denominator - pre_v_c = ctrl_array_ptr [ 0 ]/denominator - treat_array_ptr += 1 - ctrl_array_ptr += 1 - - for i in range( 1, l ): - v_t = treat_array_ptr[ 0 ]/denominator - v_c = ctrl_array_ptr [ 0 ]/denominator - p = pos_array_ptr [ 0 ] - pos_array_ptr += 1 - treat_array_ptr += 1 - ctrl_array_ptr += 1 - - if abs(pre_v_t - v_t) > 1e-5: # precision is 5 digits - fprintf( ft, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_t, p, pre_v_t ) - pre_v_t = v_t - pre_p_t = p - - if abs(pre_v_c - v_c) > 1e-5: # precision is 5 digits - fprintf( fc, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_c, p, pre_v_c ) - pre_v_c = v_c - pre_p_c = p - - p = pos_array_ptr[ 0 ] - # last one - fprintf( ft, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_t, p, pre_v_t ) - fprintf( fc, b"%s\t%d\t%d\t%.5f\n", chrom, pre_p_c, p, pre_v_c ) - - return True - - cpdef call_broadpeaks (self, list scoring_function_symbols, list lvl1_cutoff_s, list lvl2_cutoff_s, int32_t min_length=200, int32_t lvl1_max_gap=50, int32_t lvl2_max_gap=400, bool cutoff_analysis = False): - """This function try to find enriched regions within which, - scores are continuously higher than a given cutoff for level - 1, and link them using the gap above level 2 cutoff with a - maximum length of lvl2_max_gap. - - scoring_function_s: symbols of functions to calculate score. 'p' for pscore, 'q' for qscore, 'f' for fold change, 's' for subtraction. for example: ['p', 'q'] - - lvl1_cutoff_s: list of cutoffs at highly enriched regions, corresponding to scoring functions. - lvl2_cutoff_s: list of cutoffs at less enriched regions, corresponding to scoring functions. - min_length : minimum peak length, default 200. - lvl1_max_gap : maximum gap to merge nearby enriched peaks, default 50. - lvl2_max_gap : maximum length of linkage regions, default 400. - - Return both general PeakIO object for highly enriched regions - and gapped broad regions in BroadPeakIO. - """ - cdef: - int32_t i, j - bytes chrom - object lvl1peaks, lvl1peakschrom, lvl1 - object lvl2peaks, lvl2peakschrom, lvl2 - object broadpeaks - set chrs - list tmppeakset - - lvl1peaks = PeakIO() - lvl2peaks = PeakIO() - - # prepare p-q table - if len( self.pqtable ) == 0: - info("#3 Pre-compute pvalue-qvalue table...") - if cutoff_analysis: - info("#3 Cutoff value vs broad region calls will be analyzed!") - self.__pre_computes( max_gap = lvl2_max_gap, min_length = min_length ) - else: - self.__cal_pvalue_qvalue_table() - - # prepare bedGraph file - if self.save_bedGraph: - - self.bedGraph_treat_f = fopen( self.bedGraph_treat_filename, "w" ) - self.bedGraph_ctrl_f = fopen( self.bedGraph_control_filename, "w" ) - info ("#3 In the peak calling step, the following will be performed simultaneously:") - info ("#3 Write bedGraph files for treatment pileup (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_treat_pileup.bdg") - info ("#3 Write bedGraph files for control lambda (after scaling if necessary)... %s" % self.bedGraph_filename_prefix.decode() + "_control_lambda.bdg") - - if self.trackline: - # this line is REQUIRED by the wiggle format for UCSC browser - tmp_bytes = ("track type=bedGraph name=\"treatment pileup\" description=\"treatment pileup after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() - fprintf( self.bedGraph_treat_f, tmp_bytes ) - tmp_bytes = ("track type=bedGraph name=\"control lambda\" description=\"control lambda after possible scaling for \'%s\'\"\n" % self.bedGraph_filename_prefix).encode() - fprintf( self.bedGraph_ctrl_f, tmp_bytes ) - - - info("#3 Call peaks for each chromosome...") - for chrom in self.chromosomes: - self.__chrom_call_broadpeak_using_certain_criteria ( lvl1peaks, lvl2peaks, chrom, scoring_function_symbols, lvl1_cutoff_s, lvl2_cutoff_s, min_length, lvl1_max_gap, lvl2_max_gap, self.save_bedGraph ) - - # close bedGraph file - if self.save_bedGraph: - fclose( self.bedGraph_treat_f ) - fclose( self.bedGraph_ctrl_f ) - #self.bedGraph_ctrl.close() - self.save_bedGraph = False - - # now combine lvl1 and lvl2 peaks - chrs = lvl1peaks.get_chr_names() - broadpeaks = BroadPeakIO() - # use lvl2_peaks as linking regions between lvl1_peaks - for chrom in sorted(chrs): - lvl1peakschrom = lvl1peaks.get_data_from_chrom(chrom) - lvl2peakschrom = lvl2peaks.get_data_from_chrom(chrom) - lvl1peakschrom_next = iter(lvl1peakschrom).__next__ - tmppeakset = [] # to temporarily store lvl1 region inside a lvl2 region - # our assumption is lvl1 regions should be included in lvl2 regions - try: - lvl1 = lvl1peakschrom_next() - for i in range( len(lvl2peakschrom) ): - # for each lvl2 peak, find all lvl1 peaks inside - # I assume lvl1 peaks can be ALL covered by lvl2 peaks. - lvl2 = lvl2peakschrom[i] - - while True: - if lvl2["start"] <= lvl1["start"] and lvl1["end"] <= lvl2["end"]: - tmppeakset.append(lvl1) - lvl1 = lvl1peakschrom_next() - else: - # make a hierarchical broad peak - #print lvl2["start"], lvl2["end"], lvl2["score"] - self.__add_broadpeak ( broadpeaks, chrom, lvl2, tmppeakset) - tmppeakset = [] - break - except StopIteration: - # no more strong (aka lvl1) peaks left - self.__add_broadpeak ( broadpeaks, chrom, lvl2, tmppeakset) - tmppeakset = [] - # add the rest lvl2 peaks - for j in range( i+1, len(lvl2peakschrom) ): - self.__add_broadpeak( broadpeaks, chrom, lvl2peakschrom[j], tmppeakset ) - - return broadpeaks - - cdef void __chrom_call_broadpeak_using_certain_criteria ( self, lvl1peaks, lvl2peaks, bytes chrom, list scoring_function_s, list lvl1_cutoff_s, list lvl2_cutoff_s, - int32_t min_length, int32_t lvl1_max_gap, int32_t lvl2_max_gap, bool save_bedGraph): - """ Call peaks for a chromosome. - - Combination of criteria is allowed here. - - peaks: a PeakIO object - scoring_function_s: symbols of functions to calculate score as score=f(x, y) where x is treatment pileup, and y is control pileup - save_bedGraph : whether or not to save pileup and control into a bedGraph file - """ - cdef: - int32_t i - str s - np.ndarray above_cutoff, above_cutoff_endpos, above_cutoff_startpos - np.ndarray pos_array, treat_array, ctrl_array - np.ndarray above_cutoff_index_array - list score_array_s # list to keep different types of scores - list peak_content - int32_t * acs_ptr - int32_t * ace_ptr - int32_t * acia_ptr - float32_t * treat_array_ptr - float32_t * ctrl_array_ptr - - assert len(scoring_function_s) == len(lvl1_cutoff_s), "number of functions and cutoffs should be the same!" - assert len(scoring_function_s) == len(lvl2_cutoff_s), "number of functions and cutoffs should be the same!" - - # first, build pileup, self.chr_pos_treat_ctrl - self.__pileup_treat_ctrl_a_chromosome( chrom ) - [pos_array, treat_array, ctrl_array] = self.chr_pos_treat_ctrl - - # while save_bedGraph is true, invoke __write_bedGraph_for_a_chromosome - if save_bedGraph: - self.__write_bedGraph_for_a_chromosome ( chrom ) - - # keep all types of scores needed - score_array_s = [] - for i in range(len(scoring_function_s)): - s = scoring_function_s[i] - if s == 'p': - score_array_s.append( self.__cal_pscore( treat_array, ctrl_array ) ) - elif s == 'q': - score_array_s.append( self.__cal_qscore( treat_array, ctrl_array ) ) - elif s == 'f': - score_array_s.append( self.__cal_FE( treat_array, ctrl_array ) ) - elif s == 's': - score_array_s.append( self.__cal_subtraction( treat_array, ctrl_array ) ) - - # lvl1 : strong peaks - peak_content = [] # to store points above cutoff - - # get the regions with scores above cutoffs - above_cutoff = np.nonzero( apply_multiple_cutoffs(score_array_s,lvl1_cutoff_s) )[0] # this is not an optimized method. It would be better to store score array in a 2-D ndarray? - above_cutoff_index_array = np.arange(pos_array.shape[0],dtype="int32")[above_cutoff] # indices - above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff - above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff - - if above_cutoff.size == 0: - # nothing above cutoff - return - - if above_cutoff[0] == 0: - # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] - above_cutoff_startpos[0] = 0 - - # first bit of region above cutoff - acs_ptr = above_cutoff_startpos.data - ace_ptr = above_cutoff_endpos.data - acia_ptr= above_cutoff_index_array.data - treat_array_ptr = treat_array.data - ctrl_array_ptr = ctrl_array.data - - ts = acs_ptr[ 0 ] - te = ace_ptr[ 0 ] - ti = acia_ptr[ 0 ] - tp = treat_array_ptr[ ti ] - cp = ctrl_array_ptr[ ti ] - - peak_content.append( ( ts, te, tp, cp, ti ) ) - acs_ptr += 1 # move ptr - ace_ptr += 1 - acia_ptr+= 1 - lastp = te - - #peak_content.append( (above_cutoff_startpos[0], above_cutoff_endpos[0], treat_array[above_cutoff_index_array[0]], ctrl_array[above_cutoff_index_array[0]], score_array_s, above_cutoff_index_array[0] ) ) - for i in range( 1, above_cutoff_startpos.size ): - ts = acs_ptr[ 0 ] - te = ace_ptr[ 0 ] - ti = acia_ptr[ 0 ] - acs_ptr += 1 - ace_ptr += 1 - acia_ptr+= 1 - tp = treat_array_ptr[ ti ] - cp = ctrl_array_ptr[ ti ] - tl = ts - lastp - if tl <= lvl1_max_gap: - # append - #peak_content.append( (above_cutoff_startpos[i], above_cutoff_endpos[i], treat_array[above_cutoff_index_array[i]], ctrl_array[above_cutoff_index_array[i]], score_array_s, above_cutoff_index_array[i] ) ) - peak_content.append( ( ts, te, tp, cp, ti ) ) - lastp = te - else: - # close - self.__close_peak_for_broad_region (peak_content, lvl1peaks, min_length, chrom, lvl1_max_gap//2, score_array_s ) - #peak_content = [ (above_cutoff_startpos[i], above_cutoff_endpos[i], treat_array[above_cutoff_index_array[i]], ctrl_array[above_cutoff_index_array[i]], score_array_s, above_cutoff_index_array[i]) , ] - peak_content = [ ( ts, te, tp, cp, ti ), ] - lastp = te #above_cutoff_endpos[i] - - # save the last peak - if peak_content: - self.__close_peak_for_broad_region (peak_content, lvl1peaks, min_length, chrom, lvl1_max_gap//2, score_array_s ) - - # lvl2 : weak peaks - peak_content = [] # to store points above cutoff - - # get the regions with scores above cutoffs - above_cutoff = np.nonzero( apply_multiple_cutoffs(score_array_s,lvl2_cutoff_s) )[0] # this is not an optimized method. It would be better to store score array in a 2-D ndarray? - above_cutoff_index_array = np.arange(pos_array.shape[0],dtype="int32")[above_cutoff] # indices - above_cutoff_endpos = pos_array[above_cutoff] # end positions of regions where score is above cutoff - above_cutoff_startpos = pos_array[above_cutoff-1] # start positions of regions where score is above cutoff - - if above_cutoff.size == 0: - # nothing above cutoff - return - - if above_cutoff[0] == 0: - # first element > cutoff, fix the first point as 0. otherwise it would be the last item in data[chrom]['pos'] - above_cutoff_startpos[0] = 0 - - # first bit of region above cutoff - acs_ptr = above_cutoff_startpos.data - ace_ptr = above_cutoff_endpos.data - acia_ptr= above_cutoff_index_array.data - treat_array_ptr = treat_array.data - ctrl_array_ptr = ctrl_array.data - - ts = acs_ptr[ 0 ] - te = ace_ptr[ 0 ] - ti = acia_ptr[ 0 ] - tp = treat_array_ptr[ ti ] - cp = ctrl_array_ptr[ ti ] - peak_content.append( ( ts, te, tp, cp, ti ) ) - acs_ptr += 1 # move ptr - ace_ptr += 1 - acia_ptr+= 1 - - lastp = te - for i in range( 1, above_cutoff_startpos.size ): - # for everything above cutoff - ts = acs_ptr[ 0 ] # get the start - te = ace_ptr[ 0 ] # get the end - ti = acia_ptr[ 0 ]# get the index - - acs_ptr += 1 # move ptr - ace_ptr += 1 - acia_ptr+= 1 - tp = treat_array_ptr[ ti ] # get the treatment pileup - cp = ctrl_array_ptr[ ti ] # get the control pileup - tl = ts - lastp # get the distance from the current point to last position of existing peak_content - - if tl <= lvl2_max_gap: - # append - peak_content.append( ( ts, te, tp, cp, ti ) ) - lastp = te - else: - # close - self.__close_peak_for_broad_region (peak_content, lvl2peaks, min_length, chrom, lvl2_max_gap//2, score_array_s ) - - peak_content = [ ( ts, te, tp, cp, ti ), ] - lastp = te - - # save the last peak - if peak_content: - self.__close_peak_for_broad_region (peak_content, lvl2peaks, min_length, chrom, lvl2_max_gap//2, score_array_s ) - - return - - cdef bool __close_peak_for_broad_region (self, list peak_content, peaks, int32_t min_length, - bytes chrom, int32_t smoothlen, list score_array_s, list score_cutoff_s=[]): - """Close the broad peak region, output peak boundaries, peak summit - and scores, then add the peak to peakIO object. - - peak_content contains [start, end, treat_p, ctrl_p, list_scores] - - peaks: a BroadPeakIO object - - """ - cdef: - int32_t summit_pos, tstart, tend, tmpindex, summit_index, i, midindex - float64_t treat_v, ctrl_v, tsummitvalue, ttreat_p, tctrl_p, tscore, summit_treat, summit_ctrl, summit_p_score, summit_q_score - list tlist_pileup, tlist_control, tlist_length - int32_t tlist_scores_p - np.ndarray tarray_pileup, tarray_control, tarray_pscore, tarray_qscore, tarray_fc - - peak_length = peak_content[ -1 ][ 1 ] - peak_content[ 0 ][ 0 ] - if peak_length >= min_length: # if the peak is too small, reject it - tlist_pileup = [] - tlist_control= [] - tlist_length = [] - for i in range(len(peak_content)): # each position in broad peak - (tstart, tend, ttreat_p, tctrl_p, tlist_scores_p) = peak_content[i] - tlist_pileup.append( ttreat_p ) - tlist_control.append( tctrl_p ) - tlist_length.append( tend - tstart ) - - tarray_pileup = np.array( tlist_pileup, dtype="float32") - tarray_control = np.array( tlist_control, dtype="float32") - tarray_pscore = self.__cal_pscore( tarray_pileup, tarray_control ) - tarray_qscore = self.__cal_qscore( tarray_pileup, tarray_control ) - tarray_fc = self.__cal_FE ( tarray_pileup, tarray_control ) - - peaks.add( chrom, # chromosome - peak_content[0][0], # start - peak_content[-1][1], # end - summit = 0, - peak_score = mean_from_value_length( tarray_qscore, tlist_length ), - pileup = mean_from_value_length( tarray_pileup, tlist_length ), - pscore = mean_from_value_length( tarray_pscore, tlist_length ), - fold_change = mean_from_value_length( tarray_fc, tlist_length ), - qscore = mean_from_value_length( tarray_qscore, tlist_length ), - ) - #if chrom == "chr1" and peak_content[0][0] == 237643 and peak_content[-1][1] == 237935: - # print tarray_qscore, tlist_length - # start a new peak - return True - - cdef __add_broadpeak (self, bpeaks, bytes chrom, object lvl2peak, list lvl1peakset): - """Internal function to create broad peak. - - *Note* lvl1peakset/strong_regions might be empty - """ - - cdef: - int32_t blockNum, start, end - bytes blockSizes, blockStarts, thickStart, thickEnd, - - start = lvl2peak["start"] - end = lvl2peak["end"] - - if not lvl1peakset: - # will complement by adding 1bps start and end to this region - # may change in the future if gappedPeak format was improved. - bpeaks.add(chrom, start, end, score=lvl2peak["score"], thickStart=(b"%d" % start), thickEnd=(b"%d" % end), - blockNum = 2, blockSizes = b"1,1", blockStarts = (b"0,%d" % (end-start-1)), pileup = lvl2peak["pileup"], - pscore = lvl2peak["pscore"], fold_change = lvl2peak["fc"], - qscore = lvl2peak["qscore"] ) - return bpeaks - - thickStart = b"%d" % (lvl1peakset[0]["start"]) - thickEnd = b"%d" % (lvl1peakset[-1]["end"]) - blockNum = len(lvl1peakset) - blockSizes = b",".join([b"%d" % y for y in [x["length"] for x in lvl1peakset]]) - blockStarts = b",".join([b"%d" % x for x in getitem_then_subtract(lvl1peakset, start)]) - - # add 1bp left and/or right block if necessary - if int(thickStart) != start: - # add 1bp left block - thickStart = b"%d" % start - blockNum += 1 - blockSizes = b"1,"+blockSizes - blockStarts = b"0,"+blockStarts - if int(thickEnd) != end: - # add 1bp right block - thickEnd = b"%d" % end - blockNum += 1 - blockSizes = blockSizes + b",1" - blockStarts = blockStarts + b"," + (b"%d" % (end-start-1)) - - bpeaks.add(chrom, start, end, score=lvl2peak["score"], thickStart=thickStart, thickEnd=thickEnd, - blockNum = blockNum, blockSizes = blockSizes, blockStarts = blockStarts, pileup = lvl2peak["pileup"], - pscore = lvl2peak["pscore"], fold_change = lvl2peak["fc"], - qscore = lvl2peak["qscore"] ) - return bpeaks - - diff --git a/MACS3/Signal/Pileup.py b/MACS3/Signal/Pileup.py index dbb674fe..074d14a6 100644 --- a/MACS3/Signal/Pileup.py +++ b/MACS3/Signal/Pileup.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2024-10-14 20:08:53 Tao Liu> +# Time-stamp: <2024-10-22 10:35:32 Tao Liu> """Module Description: For pileup functions. @@ -61,8 +61,6 @@ def fix_coordinates(poss: cnp.ndarray, rlength: cython.int) -> cnp.ndarray: i: cython.long ptr: cython.pointer(cython.int) = cython.cast(cython.pointer(cython.int), poss.data) # pointer - #ptr = poss.data - # fix those negative coordinates for i in range(poss.shape[0]): if ptr[i] < 0: diff --git a/setup.py b/setup.py index 1ee36921..7248fb2b 100644 --- a/setup.py +++ b/setup.py @@ -134,7 +134,7 @@ def main(): include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.CallPeakUnit", - ["MACS3/Signal/CallPeakUnit.pyx"], + ["MACS3/Signal/CallPeakUnit.py"], libraries=["m"], include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), From a6ddf2874c685ffe916e1d23920a108cf60df8ac Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Tue, 22 Oct 2024 17:24:54 -0400 Subject: [PATCH 13/13] rewrite all remaining pyx for callvar to py --- .gitignore | 4 + MACS3/Signal/PeakVariants.py | 402 ++++++++ MACS3/Signal/PeakVariants.pyx | 358 ------- MACS3/Signal/PosReadsInfo.py | 670 +++++++++++++ MACS3/Signal/PosReadsInfo.pyx | 600 ------------ MACS3/Signal/RACollection.pxd | 61 ++ MACS3/Signal/RACollection.py | 906 ++++++++++++++++++ MACS3/Signal/RACollection.pyx | 898 ----------------- .../{ReadAlignment.pyx => ReadAlignment.py} | 443 ++++----- MACS3/Signal/UnitigRACollection.py | 324 +++++++ MACS3/Signal/UnitigRACollection.pyx | 309 ------ MACS3/Signal/VariantStat.py | 549 +++++++++++ MACS3/Signal/VariantStat.pyx | 461 --------- setup.py | 26 +- 14 files changed, 3152 insertions(+), 2859 deletions(-) create mode 100644 MACS3/Signal/PeakVariants.py delete mode 100644 MACS3/Signal/PeakVariants.pyx create mode 100644 MACS3/Signal/PosReadsInfo.py delete mode 100644 MACS3/Signal/PosReadsInfo.pyx create mode 100644 MACS3/Signal/RACollection.pxd create mode 100644 MACS3/Signal/RACollection.py delete mode 100644 MACS3/Signal/RACollection.pyx rename MACS3/Signal/{ReadAlignment.pyx => ReadAlignment.py} (54%) create mode 100644 MACS3/Signal/UnitigRACollection.py delete mode 100644 MACS3/Signal/UnitigRACollection.pyx create mode 100644 MACS3/Signal/VariantStat.py delete mode 100644 MACS3/Signal/VariantStat.pyx diff --git a/.gitignore b/.gitignore index f284581c..2401ffb3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ other_test/ MACS3/IO/BedGraphIO.c MACS3/IO/Parser.c MACS3/IO/PeakIO.c +MACS3/IO/BAM.c MACS3/Signal/BedGraph.c MACS3/Signal/CallPeakUnit.c MACS3/Signal/FixWidthTrack.c @@ -28,6 +29,7 @@ MACS3/Signal/PairedEndTrack.c MACS3/Signal/PeakDetect.c MACS3/Signal/PeakModel.c MACS3/Signal/Pileup.c +MACS3/Signal/PileupV2.c MACS3/Signal/Prob.c MACS3/Signal/RACollection.c MACS3/Signal/ReadAlignment.c @@ -37,6 +39,8 @@ MACS3/Signal/Signal.c MACS3/Signal/SignalProcessing.c MACS3/Signal/UnitigRACollection.c MACS3/Signal/VariantStat.c +MACS3/Signal/PeakVariants.c +MACS3/Signal/PosReadsInfo.c # MacOSX temp .DS_Store diff --git a/MACS3/Signal/PeakVariants.py b/MACS3/Signal/PeakVariants.py new file mode 100644 index 00000000..451b38b0 --- /dev/null +++ b/MACS3/Signal/PeakVariants.py @@ -0,0 +1,402 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-22 17:12:29 Tao Liu> + +"""Module for SAPPER PeakVariants class. + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file COPYING included +with the distribution). +""" + +# ------------------------------------ +# python modules +# ------------------------------------ +from copy import copy +import cython +from cython.cimports.cpython import bool + + +@cython.cclass +class Variant: + v_ref_pos: cython.long + v_ref_allele: str + v_alt_allele: str + v_GQ: cython.int + v_filter: str + v_type: str + v_mutation_type: str + v_top1allele: str + v_top2allele: str + v_DPT: cython.int + v_DPC: cython.int + v_DP1T: cython.int + v_DP2T: cython.int + v_DP1C: cython.int + v_DP2C: cython.int + v_PLUS1T: cython.int + v_PLUS2T: cython.int + v_MINUS1T: cython.int + v_MINUS2T: cython.int + v_deltaBIC: cython.float + v_BIC_homo_major: cython.float + v_BIC_homo_minor: cython.float + v_BIC_heter_noAS: cython.float + v_BIC_heter_AS: cython.float + v_AR: cython.float + v_GT: str + v_DP: cython.int + v_PL_00: cython.int + v_PL_01: cython.int + v_PL_11: cython.int + + def __init__(self, + ref_allele: str, + alt_allele: str, + GQ: cython.int, + filter: str, + type: str, + mutation_type: str, + top1allele: str, + top2allele: str, + DPT: cython.int, + DPC: cython.int, + DP1T: cython.int, + DP2T: cython.int, + DP1C: cython.int, + DP2C: cython.int, + PLUS1T: cython.int, + PLUS2T: cython.int, + MINUS1T: cython.int, + MINUS2T: cython.int, + deltaBIC: cython.float, + BIC_homo_major: cython.float, + BIC_homo_minor: cython.float, + BIC_heter_noAS: cython.float, + BIC_heter_AS: cython.float, + AR: cython.float, + GT: str, + DP: cython.int, + PL_00: cython.int, + PL_01: cython.int, + PL_11: cython.int): + self.v_ref_allele = ref_allele + self.v_alt_allele = alt_allele + self.v_GQ = GQ + self.v_filter = filter + self.v_type = type + self.v_mutation_type = mutation_type + self.v_top1allele = top1allele + self.v_top2allele = top2allele + self.v_DPT = DPT + self.v_DPC = DPC + self.v_DP1T = DP1T + self.v_DP2T = DP2T + self.v_DP1C = DP1C + self.v_DP2C = DP2C + self.v_PLUS1T = PLUS1T + self.v_PLUS2T = PLUS2T + self.v_MINUS1T = MINUS1T + self.v_MINUS2T = MINUS2T + self.v_deltaBIC = deltaBIC + self.v_BIC_homo_major = BIC_homo_major + self.v_BIC_homo_minor = BIC_homo_minor + self.v_BIC_heter_noAS = BIC_heter_noAS + self.v_BIC_heter_AS = BIC_heter_AS + self.v_AR = AR + self.v_GT = GT + self.v_DP = DP + self.v_PL_00 = PL_00 + self.v_PL_01 = PL_01 + self.v_PL_11 = PL_11 + + def __getstate__(self): + # self.v_ref_pos, + return (self.v_ref_allele, + self.v_alt_allele, + self.v_GQ, + self.v_filter, + self.v_type, + self.v_mutation_type, + self.v_top1allele, + self.v_top2allele, + self.v_DPT, + self.v_DPC, + self.v_DP1T, + self.v_DP2T, + self.v_DP1C, + self.v_DP2C, + self.v_PLUS1T, + self.v_PLUS2T, + self.v_MINUS1T, + self.v_MINUS2T, + self.v_deltaBIC, + self.v_BIC_homo_major, + self.v_BIC_homo_minor, + self.v_BIC_heter_noAS, + self.v_BIC_heter_AS, + self.v_AR, + self.v_GT, + self.v_DP, + self.v_PL_00, + self.v_PL_01, + self.v_PL_11) + + def __setstate__(self, state): + # self.v_ref_pos, + (self.v_ref_allele, + self.v_alt_allele, + self.v_GQ, + self.v_filter, + self.v_type, + self.v_mutation_type, + self.v_top1allele, + self.v_top2allele, + self.v_DPT, + self.v_DPC, + self.v_DP1T, + self.v_DP2T, + self.v_DP1C, + self.v_DP2C, + self.v_PLUS1T, + self.v_PLUS2T, + self.v_MINUS1T, + self.v_MINUS2T, + self.v_deltaBIC, + self.v_BIC_homo_major, + self.v_BIC_homo_minor, + self.v_BIC_heter_noAS, + self.v_BIC_heter_AS, + self.v_AR, + self.v_GT, + self.v_DP, + self.v_PL_00, + self.v_PL_01, + self.v_PL_11) = state + + @cython.ccall + def is_indel(self) -> bool: + if self.v_mutation_type.find("Insertion") != -1 or self.v_mutation_type.find("Deletion") != -1: + return True + else: + return False + + @cython.ccall + def is_only_del(self) -> bool: + if self.v_mutation_type == "Deletion": + return True + else: + return False + + @cython.ccall + def is_only_insertion(self) -> bool: + if self.v_mutation_type == "Insertion": + return True + else: + return False + + def __getitem__(self, keyname): + if keyname == "ref_allele": + return self.v_ref_allele + elif keyname == "alt_allele": + return self.v_alt_allele + elif keyname == "top1allele": + return self.v_top1allele + elif keyname == "top2allele": + return self.v_top2allele + elif keyname == "type": + return self.type + elif keyname == "mutation_type": + return self.mutation_type + else: + raise Exception("keyname is not accessible:", keyname) + + def __setitem__(self, keyname, v): + if keyname == "ref_allele": + self.v_ref_allele = v + elif keyname == "alt_allele": + self.v_alt_allele = v + elif keyname == "top1allele": + self.v_top1allele = v + elif keyname == "top2allele": + self.v_top2allele = v + elif keyname == "type": + self.type = v + elif keyname == "mutation_type": + self.mutation_type = v + else: + raise Exception("keyname is not accessible:", keyname) + + @cython.ccall + def is_refer_biased_01(self, + ar: cython.float = 0.85) -> bool: + if self.v_AR >= ar and self.v_ref_allele == self.v_top1allele: + return True + else: + return False + + @cython.ccall + def top1isreference(self) -> bool: + if self.v_ref_allele == self.v_top1allele: + return True + else: + return False + + @cython.ccall + def top2isreference(self) -> bool: + if self.v_ref_allele == self.v_top2allele: + return True + else: + return False + + @cython.ccall + def toVCF(self) -> str: + return "\t".join((self.v_ref_allele, self.v_alt_allele, "%d" % self.v_GQ, self.v_filter, + "M=%s;MT=%s;DPT=%d;DPC=%d;DP1T=%d%s;DP2T=%d%s;DP1C=%d%s;DP2C=%d%s;SB=%d,%d,%d,%d;DBIC=%.2f;BICHOMOMAJOR=%.2f;BICHOMOMINOR=%.2f;BICHETERNOAS=%.2f;BICHETERAS=%.2f;AR=%.2f" % + (self.v_type, self.v_mutation_type, self.v_DPT, self.v_DPC, self.v_DP1T, self.v_top1allele, + self.v_DP2T, self.v_top2allele, self.v_DP1C, self.v_top1allele, self.v_DP2C, self.v_top2allele, + self.v_PLUS1T, self.v_PLUS2T, self.v_MINUS1T, self.v_MINUS2T, + self.v_deltaBIC, + self.v_BIC_homo_major, self.v_BIC_homo_minor, self.v_BIC_heter_noAS,self.v_BIC_heter_AS, + self.v_AR + ), + "GT:DP:GQ:PL", + "%s:%d:%d:%d,%d,%d" % (self.v_GT, self.v_DP, self.v_GQ, self.v_PL_00, self.v_PL_01, self.v_PL_11) + )) + + +@cython.cclass +class PeakVariants: + chrom: str + d_Variants: dict + start: cython.long + end: cython.long + refseq: bytes + + def __init__(self, + chrom: str, + start: cython.long, + end: cython.long, + s: bytes): + self.chrom = chrom + self.d_Variants = {} + self.start = start + self.end = end + self.refseq = s + + def __getstate__(self): + return (self.d_Variants, self.chrom) + + def __setstate__(self, state): + (self.d_Variants, self.chrom) = state + + @cython.ccall + def n_variants(self) -> cython.int: + return len(self.d_Variants) + + @cython.ccall + def add_variant(self, p: cython.long, v: Variant): + self.d_Variants[p] = v + + @cython.ccall + def has_indel(self) -> bool: + p: cython.long + + for p in sorted(self.d_Variants.keys()): + if self.d_Variants[p].is_indel(): + return True + return False + + @cython.ccall + def has_refer_biased_01(self) -> bool: + p: cython.long + + for p in sorted(self.d_Variants.keys()): + if self.d_Variants[p].is_refer_biased_01(): + return True + return False + + @cython.ccall + def get_refer_biased_01s(self) -> list: + ret_poss: list = [] + p: cython.long + + for p in sorted(self.d_Variants.keys()): + if self.d_Variants[p].is_refer_biased_01(): + ret_poss.append(p) + return ret_poss + + @cython.ccall + def remove_variant(self, p: cython.long): + assert p in self.d_Variants + self.d_Variants.pop(p) + + @cython.ccall + def replace_variant(self, p: cython.long, v: Variant): + assert p in self.d_Variants + self.d_Variants[p] = v + + @cython.ccall + def fix_indels(self): + p0: cython.long + p1: cython.long + p: cython.long + + # merge continuous deletion + p0 = -1 #start of deletion chunk + p1 = -1 #end of deletion chunk + for p in sorted(self.d_Variants.keys()): + if p == p1+1 and self.d_Variants[p].is_only_del() and self.d_Variants[p0].is_only_del(): + # we keep p0, remove p, and add p's ref_allele to p0, keep other information as in p0 + if self.d_Variants[p0].top1isreference: + if self.d_Variants[p0]["top1allele"] == "*": + self.d_Variants[p0]["top1allele"] = "" + self.d_Variants[p0]["top1allele"] += self.d_Variants[p]["ref_allele"] + elif self.d_Variants[p0].top2isreference: + if self.d_Variants[p0]["top2allele"] == "*": + self.d_Variants[p0]["top2allele"] = "" + self.d_Variants[p0]["top2allele"] += self.d_Variants[p]["ref_allele"] + self.d_Variants[p0]["ref_allele"] += self.d_Variants[p]["ref_allele"] + self.d_Variants.pop(p) + p1 = p + else: + p0 = p + p1 = p + + # fix deletion so that if the preceding base is 0/0 -- i.e. not in d_Variants, the reference base will be added. + for p in sorted(self.d_Variants.keys()): + if self.d_Variants[p].is_only_del(): + if not ((p-1) in self.d_Variants): + if p > self.start: # now add the reference base + self.d_Variants[p-1] = copy(self.d_Variants[p]) + rs = str(self.refseq) + self.d_Variants[p-1]["ref_allele"] = rs[p - self.start] + self.d_Variants[p-1]["ref_allele"] + self.d_Variants[p-1]["alt_allele"] = rs[p - self.start] + if self.d_Variants[p].top1isreference: + self.d_Variants[p-1]["top1allele"] = self.d_Variants[p-1]["ref_allele"] + self.d_Variants[p-1]["top2allele"] = self.d_Variants[p-1]["alt_allele"] + elif self.d_Variants[p].top2isreference: + self.d_Variants[p-1]["top1allele"] = self.d_Variants[p-1]["alt_allele"] + self.d_Variants[p-1]["top2allele"] = self.d_Variants[p-1]["ref_allele"] + self.d_Variants.pop(p) + + # remove indel if a deletion is immediately following an + # insertion -- either a third genotype is found which is not + # allowed in this version of sapper, or problem caused by + # assembling in a simple repeat region. + for p in sorted(self.d_Variants.keys()): + if self.d_Variants[p].is_only_del(): + if (p-1) in self.d_Variants and self.d_Variants[p-1].is_only_insertion(): + self.d_Variants.pop(p) + self.d_Variants.pop(p - 1) + return + + @cython.ccall + def toVCF(self) -> str: + p: cython.long + res: str + + res = "" + for p in sorted(self.d_Variants.keys()): + res += "\t".join((self.chrom, str(p+1), ".", self.d_Variants[p].toVCF())) + "\n" + return res diff --git a/MACS3/Signal/PeakVariants.pyx b/MACS3/Signal/PeakVariants.pyx deleted file mode 100644 index 485988d1..00000000 --- a/MACS3/Signal/PeakVariants.pyx +++ /dev/null @@ -1,358 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2020-12-04 22:11:09 Tao Liu> - -"""Module for SAPPER PeakVariants class. - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file COPYING included -with the distribution). -""" - -# ------------------------------------ -# python modules -# ------------------------------------ -from copy import copy -from cpython cimport bool - -cdef class Variant: - cdef: - long v_ref_pos - str v_ref_allele - str v_alt_allele - int v_GQ - str v_filter - str v_type - str v_mutation_type - str v_top1allele - str v_top2allele - int v_DPT - int v_DPC - int v_DP1T - int v_DP2T - int v_DP1C - int v_DP2C - int v_PLUS1T - int v_PLUS2T - int v_MINUS1T - int v_MINUS2T - float v_deltaBIC - float v_BIC_homo_major - float v_BIC_homo_minor - float v_BIC_heter_noAS - float v_BIC_heter_AS - float v_AR - str v_GT - int v_DP - int v_PL_00 - int v_PL_01 - int v_PL_11 - - def __init__ ( self, str ref_allele, str alt_allele, int GQ, str filter, str type, str mutation_type, - str top1allele, str top2allele, int DPT, int DPC, int DP1T, int DP2T, int DP1C, int DP2C, - int PLUS1T, int PLUS2T, int MINUS1T, int MINUS2T, - float deltaBIC, float BIC_homo_major, float BIC_homo_minor, float BIC_heter_noAS, float BIC_heter_AS, - float AR, str GT, int DP, int PL_00, int PL_01, int PL_11): - self.v_ref_allele = ref_allele - self.v_alt_allele = alt_allele - self.v_GQ = GQ - self.v_filter = filter - self.v_type = type - self.v_mutation_type = mutation_type - self.v_top1allele = top1allele - self.v_top2allele = top2allele - self.v_DPT = DPT - self.v_DPC = DPC - self.v_DP1T = DP1T - self.v_DP2T = DP2T - self.v_DP1C = DP1C - self.v_DP2C = DP2C - self.v_PLUS1T = PLUS1T - self.v_PLUS2T = PLUS2T - self.v_MINUS1T = MINUS1T - self.v_MINUS2T = MINUS2T - self.v_deltaBIC = deltaBIC - self.v_BIC_homo_major = BIC_homo_major - self.v_BIC_homo_minor = BIC_homo_minor - self.v_BIC_heter_noAS = BIC_heter_noAS - self.v_BIC_heter_AS = BIC_heter_AS - self.v_AR = AR - self.v_GT = GT - self.v_DP = DP - self.v_PL_00 = PL_00 - self.v_PL_01 = PL_01 - self.v_PL_11 = PL_11 - - def __getstate__ ( self ): - return ( - #self.v_ref_pos, - self.v_ref_allele, - self.v_alt_allele, - self.v_GQ, - self.v_filter, - self.v_type, - self.v_mutation_type, - self.v_top1allele, - self.v_top2allele, - self.v_DPT, - self.v_DPC, - self.v_DP1T, - self.v_DP2T, - self.v_DP1C, - self.v_DP2C, - self.v_PLUS1T, - self.v_PLUS2T, - self.v_MINUS1T, - self.v_MINUS2T, - self.v_deltaBIC, - self.v_BIC_homo_major, - self.v_BIC_homo_minor, - self.v_BIC_heter_noAS, - self.v_BIC_heter_AS, - self.v_AR, - self.v_GT, - self.v_DP, - self.v_PL_00, - self.v_PL_01, - self.v_PL_11 ) - - def __setstate__ ( self, state ): - ( #self.v_ref_pos, - self.v_ref_allele, - self.v_alt_allele, - self.v_GQ, - self.v_filter, - self.v_type, - self.v_mutation_type, - self.v_top1allele, - self.v_top2allele, - self.v_DPT, - self.v_DPC, - self.v_DP1T, - self.v_DP2T, - self.v_DP1C, - self.v_DP2C, - self.v_PLUS1T, - self.v_PLUS2T, - self.v_MINUS1T, - self.v_MINUS2T, - self.v_deltaBIC, - self.v_BIC_homo_major, - self.v_BIC_homo_minor, - self.v_BIC_heter_noAS, - self.v_BIC_heter_AS, - self.v_AR, - self.v_GT, - self.v_DP, - self.v_PL_00, - self.v_PL_01, - self.v_PL_11 ) = state - - cpdef bool is_indel ( self ): - if self.v_mutation_type.find("Insertion") != -1 or self.v_mutation_type.find("Deletion") != -1: - return True - else: - return False - - cpdef bool is_only_del ( self ): - if self.v_mutation_type == "Deletion": - return True - else: - return False - - cpdef bool is_only_insertion ( self ): - if self.v_mutation_type == "Insertion": - return True - else: - return False - - def __getitem__ ( self, keyname ): - if keyname == "ref_allele": - return self.v_ref_allele - elif keyname == "alt_allele": - return self.v_alt_allele - elif keyname == "top1allele": - return self.v_top1allele - elif keyname == "top2allele": - return self.v_top2allele - elif keyname == "type": - return self.type - elif keyname == "mutation_type": - return self.mutation_type - else: - raise Exception("keyname is not accessible:", keyname) - - def __setitem__ ( self, keyname, v ): - if keyname == "ref_allele": - self.v_ref_allele = v - elif keyname == "alt_allele": - self.v_alt_allele = v - elif keyname == "top1allele": - self.v_top1allele = v - elif keyname == "top2allele": - self.v_top2allele = v - elif keyname == "type": - self.type = v - elif keyname == "mutation_type": - self.mutation_type = v - else: - raise Exception("keyname is not accessible:", keyname) - - cpdef bool is_refer_biased_01 ( self, float ar=0.85 ): - if self.v_AR >= ar and self.v_ref_allele == self.v_top1allele: - return True - else: - return False - - - cpdef bool top1isreference ( self ): - if self.v_ref_allele == self.v_top1allele: - return True - else: - return False - - cpdef bool top2isreference ( self ): - if self.v_ref_allele == self.v_top2allele: - return True - else: - return False - - cpdef str toVCF ( self ): - return "\t".join( ( self.v_ref_allele, self.v_alt_allele, "%d" % self.v_GQ, self.v_filter, - "M=%s;MT=%s;DPT=%d;DPC=%d;DP1T=%d%s;DP2T=%d%s;DP1C=%d%s;DP2C=%d%s;SB=%d,%d,%d,%d;DBIC=%.2f;BICHOMOMAJOR=%.2f;BICHOMOMINOR=%.2f;BICHETERNOAS=%.2f;BICHETERAS=%.2f;AR=%.2f" % \ - (self.v_type, self.v_mutation_type, self.v_DPT, self.v_DPC, self.v_DP1T, self.v_top1allele, - self.v_DP2T, self.v_top2allele, self.v_DP1C, self.v_top1allele, self.v_DP2C, self.v_top2allele, - self.v_PLUS1T, self.v_PLUS2T, self.v_MINUS1T, self.v_MINUS2T, - self.v_deltaBIC, - self.v_BIC_homo_major, self.v_BIC_homo_minor, self.v_BIC_heter_noAS,self.v_BIC_heter_AS, - self.v_AR - ), - "GT:DP:GQ:PL", - "%s:%d:%d:%d,%d,%d" % (self.v_GT, self.v_DP, self.v_GQ, self.v_PL_00, self.v_PL_01, self.v_PL_11) - ) ) - -cdef class PeakVariants: - cdef: - str chrom - dict d_Variants - long start - long end - bytes refseq - - - def __init__ ( self, str chrom, long start, long end, bytes s ): - self.chrom = chrom - self.d_Variants = {} - self.start = start - self.end = end - self.refseq = s - - def __getstate__ ( self ): - return ( self.d_Variants, self.chrom ) - - def __setstate__ ( self, state ): - ( self.d_Variants, self.chrom ) = state - - cpdef int n_variants ( self ): - return len(self.d_Variants) - - cpdef add_variant ( self, long p, Variant v ): - self.d_Variants[ p ] = v - - cpdef bool has_indel ( self ): - cdef: - long p - for p in sorted( self.d_Variants.keys() ): - if self.d_Variants[ p ].is_indel(): - return True - return False - - cpdef bool has_refer_biased_01 ( self ): - cdef: - long p - for p in sorted( self.d_Variants.keys() ): - if self.d_Variants[ p ].is_refer_biased_01(): - return True - return False - - cpdef list get_refer_biased_01s ( self ): - cdef: - list ret_poss = [] - long p - for p in sorted( self.d_Variants.keys() ): - if self.d_Variants[ p ].is_refer_biased_01(): - ret_poss.append( p ) - return ret_poss - - cpdef remove_variant ( self, long p ): - assert p in self.d_Variants - self.d_Variants.pop( p ) - - cpdef replace_variant ( self, long p, Variant v ): - assert p in self.d_Variants - self.d_Variants[ p ] = v - - cpdef fix_indels ( self ): - cdef: - long p0, p1, p - - # merge continuous deletion - p0 = -1 #start of deletion chunk - p1 = -1 #end of deletion chunk - for p in sorted( self.d_Variants.keys() ): - if p == p1+1 and self.d_Variants[ p ].is_only_del() and self.d_Variants[ p0 ].is_only_del() : - # we keep p0, remove p, and add p's ref_allele to p0, keep other information as in p0 - if self.d_Variants[ p0 ].top1isreference: - if self.d_Variants[ p0 ]["top1allele"] == "*": - self.d_Variants[ p0 ]["top1allele"] = "" - self.d_Variants[ p0 ]["top1allele"] += self.d_Variants[ p ]["ref_allele"] - elif self.d_Variants[ p0 ].top2isreference: - if self.d_Variants[ p0 ]["top2allele"] == "*": - self.d_Variants[ p0 ]["top2allele"] = "" - self.d_Variants[ p0 ]["top2allele"] += self.d_Variants[ p ]["ref_allele"] - self.d_Variants[ p0 ]["ref_allele"] += self.d_Variants[ p ]["ref_allele"] - self.d_Variants.pop ( p ) - p1 = p - else: - p0 = p - p1 = p - - # fix deletion so that if the preceding base is 0/0 -- i.e. not in d_Variants, the reference base will be added. - for p in sorted( self.d_Variants.keys() ): - if self.d_Variants[ p ].is_only_del(): - if not( ( p-1 ) in self.d_Variants ): - if p > self.start: # now add the reference base - self.d_Variants[ p-1 ] = copy(self.d_Variants[ p ]) - rs = str(self.refseq) - self.d_Variants[ p-1 ]["ref_allele"] = rs[ p - self.start ] + self.d_Variants[ p-1 ]["ref_allele"] - self.d_Variants[ p-1 ]["alt_allele"] = rs[ p - self.start ] - if self.d_Variants[ p ].top1isreference: - self.d_Variants[ p-1 ]["top1allele"] = self.d_Variants[ p-1 ]["ref_allele"] - self.d_Variants[ p-1 ]["top2allele"] = self.d_Variants[ p-1 ]["alt_allele"] - elif self.d_Variants[ p ].top2isreference: - self.d_Variants[ p-1 ]["top1allele"] = self.d_Variants[ p-1 ]["alt_allele"] - self.d_Variants[ p-1 ]["top2allele"] = self.d_Variants[ p-1 ]["ref_allele"] - self.d_Variants.pop( p ) - - # remove indel if a deletion is immediately following an - # insertion -- either a third genotype is found which is not - # allowed in this version of sapper, or problem caused by - # assembling in a simple repeat region. - - for p in sorted( self.d_Variants.keys() ): - if self.d_Variants[ p ].is_only_del(): - if ( p-1 ) in self.d_Variants and self.d_Variants[p-1].is_only_insertion(): - self.d_Variants.pop( p ) - self.d_Variants.pop( p - 1 ) - return - - cpdef str toVCF ( self ): - cdef: - long p - str res - res = "" - for p in sorted( self.d_Variants.keys() ): - res += "\t".join( ( self.chrom, str(p+1), ".", self.d_Variants[ p ].toVCF() ) ) + "\n" - return res - - diff --git a/MACS3/Signal/PosReadsInfo.py b/MACS3/Signal/PosReadsInfo.py new file mode 100644 index 00000000..43b52538 --- /dev/null +++ b/MACS3/Signal/PosReadsInfo.py @@ -0,0 +1,670 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-22 16:59:53 Tao Liu> + +"""Module for SAPPER PosReadsInfo class. + +Copyright (c) 2017 Tao Liu + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file COPYING included +with the distribution). + +@status: experimental +@version: $Revision$ +@author: Tao Liu +@contact: tliu4@buffalo.edu +""" + +# ------------------------------------ +# python modules +# ------------------------------------ +from MACS3.Signal.VariantStat import (CalModel_Homo, + CalModel_Heter_noAS, + CalModel_Heter_AS) +# calculate_GQ, +# calculate_GQ_heterASsig) +from MACS3.Signal.Prob import binomial_cdf +from MACS3.Signal.PeakVariants import Variant + +import cython +import numpy as np +import cython.cimports.numpy as cnp +from cython.cimports.cpython import bool + +LN10 = 2.3025850929940458 + +# ------------------------------------ +# constants +# ------------------------------------ +__version__ = "Parser $Revision$" +__author__ = "Tao Liu " +__doc__ = "All Parser classes" + +# ------------------------------------ +# Misc functions +# ------------------------------------ + +# ------------------------------------ +# Classes +# ------------------------------------ + + +@cython.cclass +class PosReadsInfo: + ref_pos: cython.long + ref_allele: cython.bytes + alt_allele: cython.bytes + filterout: bool # if true, do not output + bq_set_T: dict # {A:[], C:[], G:[], T:[], N:[]} for treatment + bq_set_C: dict + n_reads_T: dict # {A:[], C:[], G:[], T:[], N:[]} for treatment + n_reads_C: dict + n_reads: dict + n_strand: list # [{A:[], C:[], G:[], T:[], N:[]},{A:[], C:[], G:[], T:[], N:[]}] for total appearance on plus strand and minus strand for ChIP sample only + n_tips: dict # count of nt appearing at tips + top1allele: cython.bytes + top2allele: cython.bytes + top12alleles_ratio: cython.float + lnL_homo_major: cython.double + lnL_heter_AS: cython.double + lnL_heter_noAS: cython.double + lnL_homo_minor: cython.double + BIC_homo_major: cython.double + BIC_heter_AS: cython.double + BIC_heter_noAS: cython.double + BIC_homo_minor: cython.double + PL_00: cython.double + PL_01: cython.double + PL_11: cython.double + deltaBIC: cython.double + heter_noAS_kc: cython.int + heter_noAS_ki: cython.int + heter_AS_kc: cython.int + heter_AS_ki: cython.int + heter_AS_alleleratio: cython.double + GQ_homo_major: cython.int + GQ_heter_noAS: cython.int + GQ_heter_AS: cython.int # phred scale of prob by standard formular + GQ_heter_ASsig: cython.int # phred scale of prob, to measure the difference between AS and noAS + GQ: cython.double + GT: str + type: str + mutation_type: str # SNV or Insertion or Deletion + hasfermiinfor: bool # if no fermi bam overlap in the position, false; if fermi bam in the position GT: N, false; if anyone of top2allele is not in fermi GT NTs, false; + fermiNTs: bytearray + + def __cinit__(self): + self.filterout = False + self.GQ = 0 + self.GT = "unsure" + self.alt_allele = b'.' + + def __init__(self, + ref_pos: cython.long, + ref_allele: cython.bytes): + self.ref_pos = ref_pos + self.ref_allele = ref_allele + self.bq_set_T = {ref_allele: [], b'A': [], b'C': [], b'G': [], b'T': [], b'N': [], b'*': []} + self.bq_set_C = {ref_allele: [], b'A': [], b'C': [], b'G': [], b'T': [], b'N': [], b'*': []} + self.n_reads_T = {ref_allele: 0, b'A': 0, b'C': 0, b'G': 0, b'T': 0, b'N': 0, b'*': 0} + self.n_reads_C = {ref_allele: 0, b'A': 0, b'C': 0, b'G': 0, b'T': 0, b'N': 0, b'*': 0} + self.n_reads = {ref_allele: 0, b'A': 0, b'C': 0, b'G': 0, b'T': 0, b'N': 0, b'*': 0} + self.n_strand = [{ref_allele: 0, b'A': 0, b'C': 0, b'G': 0, b'T': 0, b'N': 0, b'*': 0}, + {ref_allele: 0, b'A': 0, b'C': 0, b'G': 0, b'T': 0, b'N': 0, b'*': 0}] + self.n_tips = {ref_allele: 0, b'A': 0, b'C': 0, b'G': 0, b'T': 0, b'N': 0, b'*': 0} + + def __getstate__(self): + return (self.ref_pos, + self.ref_allele, + self.alt_allele, + self.filterout, + self.bq_set_T, + self.bq_set_C, + self.n_reads_T, + self.n_reads_C, + self.n_reads, + self.n_strand, + self.n_tips, + self.top1allele, + self.top2allele, + self.top12alleles_ratio, + self.lnL_homo_major, + self.lnL_heter_AS, + self.lnL_heter_noAS, + self.lnL_homo_minor, + self.BIC_homo_major, + self.BIC_heter_AS, + self.BIC_heter_noAS, + self.BIC_homo_minor, + self.heter_noAS_kc, + self.heter_noAS_ki, + self.heter_AS_kc, + self.heter_AS_ki, + self.heter_AS_alleleratio, + self.GQ_homo_major, + self.GQ_heter_noAS, + self.GQ_heter_AS, + self.GQ_heter_ASsig, + self.GQ, + self.GT, + self.type, + self.hasfermiinfor, + self.fermiNTs) + + def __setstate__(self, state): + (self.ref_pos, + self.ref_allele, + self.alt_allele, + self.filterout, + self.bq_set_T, + self.bq_set_C, + self.n_reads_T, + self.n_reads_C, + self.n_reads, + self.n_strand, + self.n_tips, + self.top1allele, + self.top2allele, + self.top12alleles_ratio, + self.lnL_homo_major, + self.lnL_heter_AS, + self.lnL_heter_noAS, + self.lnL_homo_minor, + self.BIC_homo_major, + self.BIC_heter_AS, + self.BIC_heter_noAS, + self.BIC_homo_minor, + self.heter_noAS_kc, + self.heter_noAS_ki, + self.heter_AS_kc, + self.heter_AS_ki, + self.heter_AS_alleleratio, + self.GQ_homo_major, + self.GQ_heter_noAS, + self.GQ_heter_AS, + self.GQ_heter_ASsig, + self.GQ, + self.GT, + self.type, + self.hasfermiinfor, + self.fermiNTs) = state + + @cython.ccall + def filterflag(self) -> bool: + return self.filterout + + @cython.ccall + def apply_GQ_cutoff(self, + min_homo_GQ: cython.int = 50, + min_heter_GQ: cython.int = 100): + if self.filterout: + return + if self.type.startswith('homo') and self.GQ < min_homo_GQ: + self.filterout = True + elif self.type.startswith('heter') and self.GQ < min_heter_GQ: + self.filterout = True + return + + @cython.ccall + def apply_deltaBIC_cutoff(self, + min_delta_BIC: cython.float = 10): + if self.filterout: + return + if self.deltaBIC < min_delta_BIC: + self.filterout = True + return + + @cython.ccall + def add_T(self, + read_index: cython.int, + read_allele: cython.bytes, + read_bq: cython.int, + strand: cython.int, + tip: bool, + Q: cython.int = 20): + """ Strand 0: plus, 1: minus + + Q is the quality cutoff. By default, only consider Q20 or read_bq > 20. + """ + if read_bq <= Q: + return + if not self.n_reads.has_key(read_allele): + self.bq_set_T[read_allele] = [] + self.bq_set_C[read_allele] = [] + self.n_reads_T[read_allele] = 0 + self.n_reads_C[read_allele] = 0 + self.n_reads[read_allele] = 0 + self.n_strand[0][read_allele] = 0 + self.n_strand[1][read_allele] = 0 + self.n_tips[read_allele] = 0 + self.bq_set_T[read_allele].append(read_bq) + self.n_reads_T[read_allele] += 1 + self.n_reads[read_allele] += 1 + self.n_strand[strand][read_allele] += 1 + if tip: + self.n_tips[read_allele] += 1 + + @cython.ccall + def add_C(self, + read_index: cython.int, + read_allele: cython.bytes, + read_bq: cython.int, + strand: cython.int, + Q: cython.int = 20): + if read_bq <= Q: + return + if not self.n_reads.has_key(read_allele): + self.bq_set_T[read_allele] = [] + self.bq_set_C[read_allele] = [] + self.n_reads_T[read_allele] = 0 + self.n_reads_C[read_allele] = 0 + self.n_reads[read_allele] = 0 + self.n_strand[0][read_allele] = 0 + self.n_strand[1][read_allele] = 0 + self.n_tips[read_allele] = 0 + self.bq_set_C[read_allele].append(read_bq) + self.n_reads_C[read_allele] += 1 + self.n_reads[read_allele] += 1 + + @cython.ccall + def raw_read_depth(self, + opt: str = "all") -> cython.int: + if opt == "all": + return sum(self.n_reads.values()) + elif opt == "T": + return sum(self.n_reads_T.values()) + elif opt == "C": + return sum(self.n_reads_C.values()) + else: + raise Exception("opt should be either 'all', 'T' or 'C'.") + + @cython.ccall + def update_top_alleles(self, + min_top12alleles_ratio: cython.float = 0.8, + min_altallele_count: cython.int = 2, + max_allowed_ar: cython.float = 0.95): + """Identify top1 and top2 NT. the ratio of (top1+top2)/total + """ + [self.top1allele, self.top2allele] = sorted(self.n_reads, + key=self.n_reads_T.get, + reverse=True)[:2] + + # if top2 allele count in ChIP is lower than + # min_altallele_count, or when allele ratio top1/(top1+top2) + # is larger than max_allowed_ar in ChIP, we won't consider + # this allele at all. we set values of top2 allele in + # dictionaries to [] and ignore top2 allele entirely. + # max(self.n_strand[0][self.top2allele], self.n_strand[1][self.top2allele]) < min_altallele_count + # if self.ref_pos == 52608504: + # prself: cython.int.ref_pos, self.n_reads_T[self.top1allele], self.n_reads_T[self.top2allele], self.n_reads_C[self.top1allele], self.n_reads_C[self.top2allele] + if self.n_reads_T[self.top1allele] + self.n_reads_T[self.top2allele] == 0: + self.filterout = True + return + + if (len(self.top1allele) == 1 and len(self.top2allele) == 1) and (self.top2allele != self.ref_allele and ((self.n_reads_T[self.top2allele] - self.n_tips[self.top2allele]) < min_altallele_count) or self.n_reads_T[self.top1allele]/(self.n_reads_T[self.top1allele] + self.n_reads_T[self.top2allele]) > max_allowed_ar): + self.bq_set_T[self.top2allele] = [] + self.bq_set_C[self.top2allele] = [] + self.n_reads_T[self.top2allele] = 0 + self.n_reads_C[self.top2allele] = 0 + self.n_reads[self.top2allele] = 0 + self.n_tips[self.top2allele] = 0 + if (self.top1allele != self.ref_allele and (self.n_reads_T[self.top1allele] - self.n_tips[self.top1allele]) < min_altallele_count): + self.bq_set_T[self.top1allele] = [] + self.bq_set_C[self.top1allele] = [] + self.n_reads_T[self.top1allele] = 0 + self.n_reads_C[self.top1allele] = 0 + self.n_reads[self.top1allele] = 0 + self.n_tips[self.top1allele] = 0 + + if self.n_reads_T[self.top1allele] + self.n_reads_T[self.top2allele] == 0: + self.filterout = True + return + + self.top12alleles_ratio = (self.n_reads[self.top1allele] + self.n_reads[self.top2allele]) / sum(self.n_reads.values()) + if self.top12alleles_ratio < min_top12alleles_ratio: + self.filterout = True + return + + if self.top1allele == self.ref_allele and self.n_reads[self.top2allele] == 0: + # This means this position only contains top1allele which is the ref_allele. So the GT must be 0/0 + self.type = "homo_ref" + self.filterout = True + return + return + + @cython.ccall + def top12alleles(self): + print(self.ref_pos, self.ref_allele) + print("Top1allele", self.top1allele, "Treatment", + self.bq_set_T[self.top1allele], "Control", + self.bq_set_C[self.top1allele]) + print("Top2allele", self.top2allele, "Treatment", + self.bq_set_T[self.top2allele], "Control", + self.bq_set_C[self.top2allele]) + + @cython.ccall + def call_GT(self, max_allowed_ar: cython.float = 0.99): + """Require update_top_alleles being called. + """ + top1_bq_T: cnp.ndarray(cython.int, ndim=1) + top2_bq_T: cnp.ndarray(cython.int, ndim=1) + top1_bq_C: cnp.ndarray(cython.int, ndim=1) + top2_bq_C: cnp.ndarray(cython.int, ndim=1) + tmp_mutation_type: list + tmp_alt: cython.bytes + + if self.filterout: + return + + top1_bq_T = np.array(self.bq_set_T[self.top1allele], dtype="i4") + top2_bq_T = np.array(self.bq_set_T[self.top2allele], dtype="i4") + top1_bq_C = np.array(self.bq_set_C[self.top1allele], dtype="i4") + top2_bq_C = np.array(self.bq_set_C[self.top2allele], dtype="i4") + (self.lnL_homo_major, self.BIC_homo_major) = CalModel_Homo(top1_bq_T, + top1_bq_C, + top2_bq_T, + top2_bq_C) + (self.lnL_homo_minor, self.BIC_homo_minor) = CalModel_Homo(top2_bq_T, + top2_bq_C, + top1_bq_T, + top1_bq_C) + (self.lnL_heter_noAS, self.BIC_heter_noAS) = CalModel_Heter_noAS(top1_bq_T, + top1_bq_C, + top2_bq_T, + top2_bq_C) + (self.lnL_heter_AS, self.BIC_heter_AS) = CalModel_Heter_AS(top1_bq_T, + top1_bq_C, + top2_bq_T, + top2_bq_C, + max_allowed_ar) + + # if self.ref_pos == 71078525: + # print "---" + # prlen: cython.int(top1_bq_T), len(top1_bq_C), len(top2_bq_T), len(top2_bq_C) + # prself: cython.int.lnL_homo_major, self.lnL_homo_minor, self.lnL_heter_noAS, self.lnL_heter_AS + # prself: cython.int.BIC_homo_major, self.BIC_homo_minor, self.BIC_heter_noAS, self.BIC_heter_AS + + if self.top1allele != self.ref_allele and self.n_reads[self.top2allele] == 0: + # in this case, there is no top2 nt (or socalled minor + # allele) in either treatment or control, we should assume + # it's a 1/1 genotype. We will take 1/1 if it passes BIC + # test (deltaBIC >=2), and will skip this loci if it can't + # pass the test. + + self.deltaBIC = min(self.BIC_heter_noAS, self.BIC_heter_AS, self.BIC_homo_minor) - self.BIC_homo_major + if self.deltaBIC < 2: + self.filterout = True + return + + self.type = "homo" + self.GT = "1/1" + + self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 + self.PL_01 = -10.0 * max(self.lnL_heter_noAS, self.lnL_heter_AS) / LN10 + self.PL_11 = -10.0 * self.lnL_homo_major / LN10 + + self.PL_00 = max(0, self.PL_00 - self.PL_11) + self.PL_01 = max(0, self.PL_01 - self.PL_11) + self.PL_11 = 0 + + self.GQ = min(self.PL_00, self.PL_01) + self.alt_allele = self.top1allele + else: + # assign GQ, GT, and type + if self.ref_allele != self.top1allele and self.BIC_homo_major + 2 <= self.BIC_homo_minor and self.BIC_homo_major + 2 <= self.BIC_heter_noAS and self.BIC_homo_major + 2 <= self.BIC_heter_AS: + self.type = "homo" + self.deltaBIC = min(self.BIC_heter_noAS, self.BIC_heter_AS, self.BIC_homo_minor) - self.BIC_homo_major + self.GT = "1/1" + self.alt_allele = self.top1allele + + self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 + self.PL_01 = -10.0 * max(self.lnL_heter_noAS, self.lnL_heter_AS) / LN10 + self.PL_11 = -10.0 * self.lnL_homo_major / LN10 + + self.PL_00 = self.PL_00 - self.PL_11 + self.PL_01 = self.PL_01 - self.PL_11 + self.PL_11 = 0 + + self.GQ = min(self.PL_00, self.PL_01) + + elif self.BIC_heter_noAS + 2 <= self.BIC_homo_major and self.BIC_heter_noAS + 2 <= self.BIC_homo_minor and self.BIC_heter_noAS + 2 <= self.BIC_heter_AS: + self.type = "heter_noAS" + self.deltaBIC = min(self.BIC_homo_major, self.BIC_homo_minor) - self.BIC_heter_noAS + + self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 + self.PL_01 = -10.0 * self.lnL_heter_noAS / LN10 + self.PL_11 = -10.0 * self.lnL_homo_major / LN10 + + self.PL_00 = self.PL_00 - self.PL_01 + self.PL_11 = self.PL_11 - self.PL_01 + self.PL_01 = 0 + + self.GQ = min(self.PL_00, self.PL_11) + + elif self.BIC_heter_AS + 2 <= self.BIC_homo_major and self.BIC_heter_AS + 2 <= self.BIC_homo_minor and self.BIC_heter_AS + 2 <= self.BIC_heter_noAS: + self.type = "heter_AS" + self.deltaBIC = min(self.BIC_homo_major, self.BIC_homo_minor) - self.BIC_heter_AS + + self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 + self.PL_01 = -10.0 * self.lnL_heter_AS / LN10 + self.PL_11 = -10.0 * self.lnL_homo_major / LN10 + + self.PL_00 = self.PL_00 - self.PL_01 + self.PL_11 = self.PL_11 - self.PL_01 + self.PL_01 = 0 + + self.GQ = min(self.PL_00, self.PL_11) + + elif self.BIC_heter_AS + 2 <= self.BIC_homo_major and self.BIC_heter_AS + 2 <= self.BIC_homo_minor: + # can't decide if it's noAS or AS + self.type = "heter_unsure" + self.deltaBIC = min(self.BIC_homo_major, self.BIC_homo_minor) - max(self.BIC_heter_AS, self.BIC_heter_noAS) + + self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 + self.PL_01 = -10.0 * max(self.lnL_heter_noAS, self.lnL_heter_AS) / LN10 + self.PL_11 = -10.0 * self.lnL_homo_major / LN10 + + self.PL_00 = self.PL_00 - self.PL_01 + self.PL_11 = self.PL_11 - self.PL_01 + self.PL_01 = 0 + + self.GQ = min(self.PL_00, self.PL_11) + + elif self.ref_allele == self.top1allele and self.BIC_homo_major < self.BIC_homo_minor and self.BIC_homo_major < self.BIC_heter_noAS and self.BIC_homo_major < self.BIC_heter_AS: + self.type = "homo_ref" + # we do not calculate GQ if type is homo_ref + self.GT = "0/0" + self.filterout = True + else: + self.type = "unsure" + self.filterout = True + + if self.type.startswith("heter"): + if self.ref_allele == self.top1allele: + self.alt_allele = self.top2allele + self.GT = "0/1" + elif self.ref_allele == self.top2allele: + self.alt_allele = self.top1allele + self.GT = "0/1" + else: + self.alt_allele = self.top1allele+b','+self.top2allele + self.GT = "1/2" + + tmp_mutation_type = [] + for tmp_alt in self.alt_allele.split(b','): + if tmp_alt == b'*': + tmp_mutation_type.append("Deletion") + elif len(tmp_alt) > 1: + tmp_mutation_type.append("Insertion") + else: + tmp_mutation_type.append("SNV") + self.mutation_type = ",".join(tmp_mutation_type) + return + + @cython.cfunc + def SB_score_ChIP(self, + a: cython.int, + b: cython.int, + c: cython.int, + d: cython.int) -> cython.float: + """ calculate score for filtering variants with strange strand biases. + + a: top1/major allele plus strand + b: top2/minor allele plus strand + c: top1/major allele minus strand + d: top2/minor allele minus strand + + Return a value: cython.float so that if this value >= 1, the variant will be filtered out. + """ + p1_l: cython.double + p1_r: cython.double + p2_l: cython.double + p2_r: cython.double + + if a + b == 0 or c + d == 0: + # if major allele and minor allele both bias to the same strand, allow it + return 0.0 + + # Rule: + # if there is bias in top2 allele then bias in top1 allele should not be significantly smaller than it. + # or there is no significant bias (0.5) in top2 allele. + + # pra: cython.int, b, c, d + p1_l = binomial_cdf(a, (a+c), 0.5, lower=True) # alternative: less than 0.5 + p1_r = binomial_cdf(c, (a+c), 0.5, lower=True) # greater than 0.5 + p2_l = binomial_cdf(b, (b+d), 0.5, lower=True) # alternative: less than 0.5 + p2_r = binomial_cdf(d, (b+d), 0.5, lower=True) # greater than 0.5 + # prp1_l: cython.int, p1_r, p2_l, p2_r + + if (p1_l < 0.05 and p2_r < 0.05) or (p1_r < 0.05 and p2_l < 0.05): + # we reject loci where the significant biases are inconsistent between top1 and top2 alleles. + return 1.0 + else: + # if b<=2 and d=0 or b=0 and d<=2 -- highly possible FPs + # if (b<=2 and d==0 or b==0 and d<=2): + # return 1 + # can't decide + return 0.0 + + @cython.cfunc + def SB_score_ATAC(self, + a: cython.int, + b: cython.int, + c: cython.int, + d: cython.int) -> cython.float: + """ calculate score for filtering variants with strange strand biases. + + ATAC-seq version + + a: top1/major allele plus strand + b: top2/minor allele plus strand + c: top1/major allele minus strand + d: top2/minor allele minus strand + + Return a value: cython.float so that if this value >= 1, the variant will be filtered out. + """ + p1_l: cython.double + p1_r: cython.double + p2_l: cython.double + p2_r: cython.double + + if a+b == 0 or c+d == 0: + # if major allele and minor allele both bias to the same strand, allow it + return 0.0 + + # Rule: + # if there is bias in top2 allele then bias in top1 allele should not be significantly smaller than it. + # or there is no significant bias (0.5) in top2 allele. + # pra: cython.int, b, c, d + p1_l = binomial_cdf(a, (a+c), 0.5, lower=True) # alternative: less than 0.5 + p1_r = binomial_cdf(c, (a+c), 0.5, lower=True) # greater than 0.5 + p2_l = binomial_cdf(b, (b+d), 0.5, lower=True) # alternative: less than 0.5 + p2_r = binomial_cdf(d, (b+d), 0.5, lower=True) # greater than 0.5 + # prp1_l: cython.int, p1_r, p2_l, p2_r + + if (p1_l < 0.05 and p2_r < 0.05) or (p1_r < 0.05 and p2_l < 0.05): + # we reject loci where the significant biases are inconsistent between top1 and top2 alleles. + return 1.0 + else: + # can't decide + return 0.0 + + @cython.ccall + def to_vcf(self) -> str: + """Output REF,ALT,QUAL,FILTER,INFO,FORMAT, SAMPLE columns. + """ + vcf_ref: str + vcf_alt: str + vcf_qual: str + vcf_filter: str + vcf_info: str + vcf_format: str + vcf_sample: str + + vcf_ref = self.ref_allele.decode() + vcf_alt = self.alt_allele.decode() + vcf_qual = "%d" % self.GQ + vcf_filter = "." + vcf_info = (b"M=%s;MT=%s;DPT=%d;DPC=%d;DP1T=%d%s;DP2T=%d%s;DP1C=%d%s;DP2C=%d%s;SB=%d,%d,%d,%d;DBIC=%.2f;BICHOMOMAJOR=%.2f;BICHOMOMINOR=%.2f;BICHETERNOAS=%.2f;BICHETERAS=%.2f;AR=%.2f" % + (self.type.encode(), + self.mutation_type.encode(), + sum(self.n_reads_T.values()), + sum(self.n_reads_C.values()), + self.n_reads_T[self.top1allele], + self.top1allele, + self.n_reads_T[self.top2allele], + self.top2allele, + self.n_reads_C[self.top1allele], + self.top1allele, + self.n_reads_C[self.top2allele], + self.top2allele, + self.n_strand[0][self.top1allele], + self.n_strand[0][self.top2allele], + self.n_strand[1][self.top1allele], + self.n_strand[1][self.top2allele], + self.deltaBIC, + self.BIC_homo_major, + self.BIC_homo_minor, + self.BIC_heter_noAS, + self.BIC_heter_AS, + self.n_reads_T[self.top1allele]/(self.n_reads_T[self.top1allele]+self.n_reads_T[self.top2allele]) + )).decode() + vcf_format = "GT:DP:GQ:PL" + vcf_sample = "%s:%d:%d:%d,%d,%d" % (self.GT, self.raw_read_depth(opt="all"), self.GQ, self.PL_00, self.PL_01, self.PL_11) + return "\t".join((vcf_ref, vcf_alt, vcf_qual, vcf_filter, vcf_info, vcf_format, vcf_sample)) + + @cython.ccall + def toVariant(self): + v: Variant + + v = Variant(self.ref_allele.decode(), + self.alt_allele.decode(), + self.GQ, + '.', + self.type, + self.mutation_type, + self.top1allele.decode(), + self.top2allele.decode(), + sum(self.n_reads_T.values()), + sum(self.n_reads_C.values()), + self.n_reads_T[self.top1allele], + self.n_reads_T[self.top2allele], + self.n_reads_C[self.top1allele], + self.n_reads_C[self.top2allele], + self.n_strand[0][self.top1allele], + self.n_strand[0][self.top2allele], + self.n_strand[1][self.top1allele], + self.n_strand[1][self.top2allele], + self.deltaBIC, + self.BIC_homo_major, + self.BIC_homo_minor, + self.BIC_heter_noAS, + self.BIC_heter_AS, + self.n_reads_T[self.top1allele]/(self.n_reads_T[self.top1allele]+self.n_reads_T[self.top2allele]), + self.GT, + self.raw_read_depth(opt="all"), + self.PL_00, + self.PL_01, + self.PL_11) + return v diff --git a/MACS3/Signal/PosReadsInfo.pyx b/MACS3/Signal/PosReadsInfo.pyx deleted file mode 100644 index 93d38348..00000000 --- a/MACS3/Signal/PosReadsInfo.pyx +++ /dev/null @@ -1,600 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2020-12-04 23:10:35 Tao Liu> - -"""Module for SAPPER PosReadsInfo class. - -Copyright (c) 2017 Tao Liu - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file COPYING included -with the distribution). - -@status: experimental -@version: $Revision$ -@author: Tao Liu -@contact: tliu4@buffalo.edu -""" - -# ------------------------------------ -# python modules -# ------------------------------------ -from MACS3.Signal.VariantStat import CalModel_Homo, CalModel_Heter_noAS, CalModel_Heter_AS, calculate_GQ, calculate_GQ_heterASsig -from MACS3.Signal.Prob import binomial_cdf -from MACS3.Signal.PeakVariants import Variant - -from cpython cimport bool - -import numpy as np -cimport numpy as np -from numpy cimport uint32_t, uint64_t, int32_t, float32_t - -LN10 = 2.3025850929940458 - -cdef extern from "stdlib.h": - ctypedef unsigned int size_t - size_t strlen(char *s) - void *malloc(size_t size) - void *calloc(size_t n, size_t size) - void free(void *ptr) - int strcmp(char *a, char *b) - char * strcpy(char *a, char *b) - long atol(char *bytes) - int atoi(char *bytes) - -# ------------------------------------ -# constants -# ------------------------------------ -__version__ = "Parser $Revision$" -__author__ = "Tao Liu " -__doc__ = "All Parser classes" - -# ------------------------------------ -# Misc functions -# ------------------------------------ - -# ------------------------------------ -# Classes -# ------------------------------------ - -cdef class PosReadsInfo: - cdef: - long ref_pos - bytes ref_allele - bytes alt_allele - bool filterout # if true, do not output - - dict bq_set_T #{A:[], C:[], G:[], T:[], N:[]} for treatment - dict bq_set_C - dict n_reads_T #{A:[], C:[], G:[], T:[], N:[]} for treatment - dict n_reads_C - dict n_reads - - list n_strand #[{A:[], C:[], G:[], T:[], N:[]},{A:[], C:[], G:[], T:[], N:[]}] for total appearance on plus strand and minus strand for ChIP sample only - dict n_tips # count of nt appearing at tips - - bytes top1allele - bytes top2allele - float top12alleles_ratio - - double lnL_homo_major,lnL_heter_AS,lnL_heter_noAS,lnL_homo_minor - double BIC_homo_major,BIC_heter_AS,BIC_heter_noAS,BIC_homo_minor - double PL_00, PL_01, PL_11 - double deltaBIC - int heter_noAS_kc, heter_noAS_ki - int heter_AS_kc, heter_AS_ki - double heter_AS_alleleratio - - int GQ_homo_major,GQ_heter_noAS,GQ_heter_AS #phred scale of prob by standard formular - int GQ_heter_ASsig #phred scale of prob, to measure the difference between AS and noAS - - double GQ - - str GT - str type - str mutation_type # SNV or Insertion or Deletion - - bool hasfermiinfor #if no fermi bam overlap in the position, false; if fermi bam in the position GT: N, false; if anyone of top2allele is not in fermi GT NTs, false; - bytearray fermiNTs # - - def __cinit__ ( self ): - self.filterout = False - self.GQ = 0 - self.GT = "unsure" - self.alt_allele = b'.' - - def __init__ ( self, long ref_pos, bytes ref_allele ): - self.ref_pos = ref_pos - self.ref_allele = ref_allele - self.bq_set_T = { ref_allele:[],b'A':[], b'C':[], b'G':[], b'T':[], b'N':[], b'*':[] } - self.bq_set_C = { ref_allele:[],b'A':[], b'C':[], b'G':[], b'T':[], b'N':[], b'*':[] } - self.n_reads_T = { ref_allele:0,b'A':0, b'C':0, b'G':0, b'T':0, b'N':0, b'*':0 } - self.n_reads_C = { ref_allele:0,b'A':0, b'C':0, b'G':0, b'T':0, b'N':0, b'*':0 } - self.n_reads = { ref_allele:0,b'A':0, b'C':0, b'G':0, b'T':0, b'N':0, b'*':0 } - self.n_strand = [ { ref_allele:0,b'A':0, b'C':0, b'G':0, b'T':0, b'N':0, b'*':0 }, { ref_allele:0,b'A':0, b'C':0, b'G':0, b'T':0, b'N':0, b'*':0 } ] - self.n_tips = { ref_allele:0,b'A':0, b'C':0, b'G':0, b'T':0, b'N':0, b'*':0 } - - - #cpdef void merge ( self, PosReadsInfo PRI2 ): - # """Merge two PRIs. No check available. - # - # """ - # assert self.ref_pos == PRI2.ref_pos - # assert self.ref_allele == PRI2.ref_allele - # for b in set( self.n_reads.keys() ).union( set( PRI2.n_reads.keys() ) ): - # self.bq_set_T[ b ] = self.bq_set_T.get( b, []).extend( PRI2.bq_set_T.get( b, [] ) ) - # self.bq_set_C[ b ] = self.bq_set_C.get( b, []).extend( PRI2.bq_set_C.get( b, [] ) ) - # self.n_reads_T[ b ] = self.n_reads_T.get( b, 0) + PRI2.n_reads_T.get( b, 0 ) - # self.n_reads_C[ b ] = self.n_reads_C.get( b, 0) + PRI2.n_reads_C.get( b, 0 ) - # self.n_reads[ b ] = self.n_reads.get( b, 0) + PRI2.n_reads.get( b, 0 ) - # return - - def __getstate__ ( self ): - return ( self.ref_pos, self.ref_allele, self.alt_allele, self.filterout, - self.bq_set_T, self.bq_set_C, self.n_reads_T, self.n_reads_C, self.n_reads, self.n_strand, self.n_tips, - self.top1allele, self.top2allele, self.top12alleles_ratio, - self.lnL_homo_major, self.lnL_heter_AS, self.lnL_heter_noAS, self.lnL_homo_minor, - self.BIC_homo_major, self.BIC_heter_AS, self.BIC_heter_noAS, self.BIC_homo_minor, - self.heter_noAS_kc, self.heter_noAS_ki, - self.heter_AS_kc, self.heter_AS_ki, - self.heter_AS_alleleratio, - self.GQ_homo_major, self.GQ_heter_noAS, self.GQ_heter_AS, - self.GQ_heter_ASsig, - self.GQ, - self.GT, - self.type, - self.hasfermiinfor, - self.fermiNTs ) - - def __setstate__ ( self, state ): - ( self.ref_pos, self.ref_allele, self.alt_allele, self.filterout, - self.bq_set_T, self.bq_set_C, self.n_reads_T, self.n_reads_C, self.n_reads, self.n_strand, self.n_tips, - self.top1allele, self.top2allele, self.top12alleles_ratio, - self.lnL_homo_major, self.lnL_heter_AS, self.lnL_heter_noAS, self.lnL_homo_minor, - self.BIC_homo_major, self.BIC_heter_AS, self.BIC_heter_noAS, self.BIC_homo_minor, - self.heter_noAS_kc, self.heter_noAS_ki, - self.heter_AS_kc, self.heter_AS_ki, - self.heter_AS_alleleratio, - self.GQ_homo_major, self.GQ_heter_noAS, self.GQ_heter_AS, - self.GQ_heter_ASsig, - self.GQ, - self.GT, - self.type, - self.hasfermiinfor, - self.fermiNTs ) = state - - cpdef bool filterflag ( self ): - return self.filterout - - cpdef void apply_GQ_cutoff ( self, int min_homo_GQ = 50, int min_heter_GQ = 100 ): - if self.filterout: - return - if self.type.startswith('homo') and self.GQ < min_homo_GQ: - self.filterout = True - elif self.type.startswith('heter') and self.GQ < min_heter_GQ: - self.filterout = True - return - - cpdef void apply_deltaBIC_cutoff ( self, float min_delta_BIC = 10 ): - if self.filterout: - return - if self.deltaBIC < min_delta_BIC: - self.filterout = True - return - - cpdef void add_T ( self, int read_index, bytes read_allele, int read_bq, int strand, bool tip, int Q=20 ): - """ Strand 0: plus, 1: minus - - Q is the quality cutoff. By default, only consider Q20 or read_bq > 20. - """ - if read_bq <= Q: - return - if not self.n_reads.has_key( read_allele ): - self.bq_set_T[read_allele] = [] - self.bq_set_C[read_allele] = [] - self.n_reads_T[read_allele] = 0 - self.n_reads_C[read_allele] = 0 - self.n_reads[read_allele] = 0 - self.n_strand[ 0 ][ read_allele ] = 0 - self.n_strand[ 1 ][ read_allele ] = 0 - self.n_tips[read_allele] = 0 - self.bq_set_T[read_allele].append( read_bq ) - self.n_reads_T[ read_allele ] += 1 - self.n_reads[ read_allele ] += 1 - self.n_strand[ strand ][ read_allele ] += 1 - if tip: self.n_tips[ read_allele ] += 1 - - cpdef void add_C ( self, int read_index, bytes read_allele, int read_bq, int strand, int Q=20 ): - if read_bq <= Q: - return - if not self.n_reads.has_key( read_allele ): - self.bq_set_T[read_allele] = [] - self.bq_set_C[read_allele] = [] - self.n_reads_T[read_allele] = 0 - self.n_reads_C[read_allele] = 0 - self.n_reads[read_allele] = 0 - self.n_strand[ 0 ][ read_allele ] = 0 - self.n_strand[ 1 ][ read_allele ] = 0 - self.n_tips[read_allele] = 0 - self.bq_set_C[read_allele].append( read_bq ) - self.n_reads_C[ read_allele ] += 1 - self.n_reads[ read_allele ] += 1 - #self.n_strand[ strand ][ read_allele ] += 1 - - cpdef int raw_read_depth ( self, str opt = "all" ): - if opt == "all": - return sum( self.n_reads.values() ) - elif opt == "T": - return sum( self.n_reads_T.values() ) - elif opt == "C": - return sum( self.n_reads_C.values() ) - else: - raise Exception( "opt should be either 'all', 'T' or 'C'." ) - - cpdef void update_top_alleles ( self, float min_top12alleles_ratio = 0.8, int min_altallele_count = 2, float max_allowed_ar = 0.95 ): - #cpdef update_top_alleles ( self, float min_top12alleles_ratio = 0.8 ): - """Identify top1 and top2 NT. the ratio of (top1+top2)/total - """ - cdef: - float r - - [self.top1allele, self.top2allele] = sorted(self.n_reads, key=self.n_reads_T.get, reverse=True)[:2] - - # if top2 allele count in ChIP is lower than - # min_altallele_count, or when allele ratio top1/(top1+top2) - # is larger than max_allowed_ar in ChIP, we won't consider - # this allele at all. we set values of top2 allele in - # dictionaries to [] and ignore top2 allele entirely. - - # max(self.n_strand[ 0 ][ self.top2allele ], self.n_strand[ 1 ][ self.top2allele ]) < min_altallele_count - #if self.ref_pos == 52608504: - # print self.ref_pos, self.n_reads_T[ self.top1allele ], self.n_reads_T[ self.top2allele ], self.n_reads_C[ self.top1allele ], self.n_reads_C[ self.top2allele ] - if self.n_reads_T[ self.top1allele ] + self.n_reads_T[ self.top2allele ] == 0: - self.filterout = True - return - - if (len(self.top1allele)==1 and len(self.top2allele)==1) and ( self.top2allele != self.ref_allele and ( ( self.n_reads_T[ self.top2allele ] - self.n_tips[ self.top2allele ] ) < min_altallele_count ) or \ - self.n_reads_T[ self.top1allele ]/(self.n_reads_T[ self.top1allele ] + self.n_reads_T[ self.top2allele ]) > max_allowed_ar ): - self.bq_set_T[ self.top2allele ] = [] - self.bq_set_C[ self.top2allele ] = [] - self.n_reads_T[ self.top2allele ] = 0 - self.n_reads_C[ self.top2allele ] = 0 - self.n_reads[ self.top2allele ] = 0 - self.n_tips[ self.top2allele ] = 0 - if ( self.top1allele != self.ref_allele and ( self.n_reads_T[ self.top1allele ] - self.n_tips[ self.top1allele ] ) < min_altallele_count ): - self.bq_set_T[ self.top1allele ] = [] - self.bq_set_C[ self.top1allele ] = [] - self.n_reads_T[ self.top1allele ] = 0 - self.n_reads_C[ self.top1allele ] = 0 - self.n_reads[ self.top1allele ] = 0 - self.n_tips[ self.top1allele ] = 0 - - if self.n_reads_T[ self.top1allele ] + self.n_reads_T[ self.top2allele ] == 0: - self.filterout = True - return - - self.top12alleles_ratio = ( self.n_reads[ self.top1allele ] + self.n_reads[ self.top2allele ] ) / sum( self.n_reads.values() ) - if self.top12alleles_ratio < min_top12alleles_ratio: - self.filterout = True - return - - if self.top1allele == self.ref_allele and self.n_reads[ self.top2allele ] == 0: - # This means this position only contains top1allele which is the ref_allele. So the GT must be 0/0 - self.type = "homo_ref" - self.filterout = True - return - return - - cpdef void top12alleles ( self ): - print ( self.ref_pos, self.ref_allele) - print ("Top1allele",self.top1allele, "Treatment", self.bq_set_T[self.top1allele], "Control", self.bq_set_C[self.top1allele]) - print ("Top2allele",self.top2allele, "Treatment", self.bq_set_T[self.top2allele], "Control", self.bq_set_C[self.top2allele]) - - cpdef void call_GT ( self, float max_allowed_ar = 0.99 ): - """Require update_top_alleles being called. - """ - cdef: - np.ndarray[np.int32_t, ndim=1] top1_bq_T - np.ndarray[np.int32_t, ndim=1] top2_bq_T - np.ndarray[np.int32_t, ndim=1] top1_bq_C - np.ndarray[np.int32_t, ndim=1] top2_bq_C - int i - list top1_bq_T_l - list top2_bq_T_l - list top1_bq_C_l - list top2_bq_C_l - list tmp_mutation_type - bytes tmp_alt - - if self.filterout: - return - - top1_bq_T = np.array( self.bq_set_T[ self.top1allele ], dtype="int32" ) - top2_bq_T = np.array( self.bq_set_T[ self.top2allele ], dtype="int32" ) - top1_bq_C = np.array( self.bq_set_C[ self.top1allele ], dtype="int32" ) - top2_bq_C = np.array( self.bq_set_C[ self.top2allele ], dtype="int32" ) - (self.lnL_homo_major, self.BIC_homo_major) = CalModel_Homo( top1_bq_T, top1_bq_C, top2_bq_T, top2_bq_C ) - (self.lnL_homo_minor, self.BIC_homo_minor) = CalModel_Homo( top2_bq_T, top2_bq_C, top1_bq_T, top1_bq_C ) - (self.lnL_heter_noAS, self.BIC_heter_noAS) = CalModel_Heter_noAS( top1_bq_T, top1_bq_C, top2_bq_T, top2_bq_C ) - (self.lnL_heter_AS, self.BIC_heter_AS) = CalModel_Heter_AS( top1_bq_T, top1_bq_C, top2_bq_T, top2_bq_C, max_allowed_ar ) - - #if self.ref_pos == 71078525: - # print "---" - # print len( top1_bq_T ), len( top1_bq_C ), len( top2_bq_T ), len( top2_bq_C ) - # print self.lnL_homo_major, self.lnL_homo_minor, self.lnL_heter_noAS, self.lnL_heter_AS - # print self.BIC_homo_major, self.BIC_homo_minor, self.BIC_heter_noAS, self.BIC_heter_AS - - if self.top1allele != self.ref_allele and self.n_reads[ self.top2allele ] == 0: - # in this case, there is no top2 nt (or socalled minor - # allele) in either treatment or control, we should assume - # it's a 1/1 genotype. We will take 1/1 if it passes BIC - # test (deltaBIC >=2), and will skip this loci if it can't - # pass the test. - - self.deltaBIC = min( self.BIC_heter_noAS, self.BIC_heter_AS, self.BIC_homo_minor ) - self.BIC_homo_major - if self.deltaBIC < 2: - self.filterout = True - return - - self.type = "homo" - self.GT = "1/1" - - self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 - self.PL_01 = -10.0 * max( self.lnL_heter_noAS, self.lnL_heter_AS ) / LN10 - self.PL_11 = -10.0 * self.lnL_homo_major / LN10 - - self.PL_00 = max( 0, self.PL_00 - self.PL_11 ) - self.PL_01 = max( 0, self.PL_01 - self.PL_11 ) - self.PL_11 = 0 - - self.GQ = min( self.PL_00, self.PL_01 ) - self.alt_allele = self.top1allele - else: - # assign GQ, GT, and type - if self.ref_allele != self.top1allele and self.BIC_homo_major + 2 <= self.BIC_homo_minor and self.BIC_homo_major + 2 <= self.BIC_heter_noAS and self.BIC_homo_major + 2 <= self.BIC_heter_AS: - self.type = "homo" - self.deltaBIC = min( self.BIC_heter_noAS, self.BIC_heter_AS, self.BIC_homo_minor ) - self.BIC_homo_major - self.GT = "1/1" - self.alt_allele = self.top1allele - - self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 - self.PL_01 = -10.0 * max( self.lnL_heter_noAS, self.lnL_heter_AS ) / LN10 - self.PL_11 = -10.0 * self.lnL_homo_major / LN10 - - self.PL_00 = self.PL_00 - self.PL_11 - self.PL_01 = self.PL_01 - self.PL_11 - self.PL_11 = 0 - - self.GQ = min( self.PL_00, self.PL_01 ) - - elif self.BIC_heter_noAS + 2 <= self.BIC_homo_major and self.BIC_heter_noAS + 2 <= self.BIC_homo_minor and self.BIC_heter_noAS + 2 <= self.BIC_heter_AS : - self.type = "heter_noAS" - self.deltaBIC = min( self.BIC_homo_major, self.BIC_homo_minor ) - self.BIC_heter_noAS - - self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 - self.PL_01 = -10.0 * self.lnL_heter_noAS / LN10 - self.PL_11 = -10.0 * self.lnL_homo_major / LN10 - - self.PL_00 = self.PL_00 - self.PL_01 - self.PL_11 = self.PL_11 - self.PL_01 - self.PL_01 = 0 - - self.GQ = min( self.PL_00, self.PL_11 ) - - elif self.BIC_heter_AS + 2 <= self.BIC_homo_major and self.BIC_heter_AS + 2 <= self.BIC_homo_minor and self.BIC_heter_AS + 2 <= self.BIC_heter_noAS: - self.type = "heter_AS" - self.deltaBIC = min( self.BIC_homo_major, self.BIC_homo_minor ) - self.BIC_heter_AS - - self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 - self.PL_01 = -10.0 * self.lnL_heter_AS / LN10 - self.PL_11 = -10.0 * self.lnL_homo_major / LN10 - - self.PL_00 = self.PL_00 - self.PL_01 - self.PL_11 = self.PL_11 - self.PL_01 - self.PL_01 = 0 - - self.GQ = min( self.PL_00, self.PL_11 ) - - elif self.BIC_heter_AS + 2 <= self.BIC_homo_major and self.BIC_heter_AS + 2 <= self.BIC_homo_minor: - # can't decide if it's noAS or AS - self.type = "heter_unsure" - self.deltaBIC = min( self.BIC_homo_major, self.BIC_homo_minor ) - max( self.BIC_heter_AS, self.BIC_heter_noAS ) - - self.PL_00 = -10.0 * self.lnL_homo_minor / LN10 - self.PL_01 = -10.0 * max( self.lnL_heter_noAS, self.lnL_heter_AS ) / LN10 - self.PL_11 = -10.0 * self.lnL_homo_major / LN10 - - self.PL_00 = self.PL_00 - self.PL_01 - self.PL_11 = self.PL_11 - self.PL_01 - self.PL_01 = 0 - - self.GQ = min( self.PL_00, self.PL_11 ) - - elif self.ref_allele == self.top1allele and self.BIC_homo_major < self.BIC_homo_minor and self.BIC_homo_major < self.BIC_heter_noAS and self.BIC_homo_major < self.BIC_heter_AS: - self.type = "homo_ref" - # we do not calculate GQ if type is homo_ref - self.GT = "0/0" - self.filterout = True - else: - self.type="unsure" - self.filterout = True - - if self.type.startswith( "heter" ): - if self.ref_allele == self.top1allele: - self.alt_allele = self.top2allele - self.GT = "0/1" - elif self.ref_allele == self.top2allele: - self.alt_allele = self.top1allele - self.GT = "0/1" - else: - self.alt_allele = self.top1allele+b','+self.top2allele - self.GT = "1/2" - # strand bias filter, uncomment following if wish to debug - # calculate SB score - #print "calculate SB score for ", self.ref_pos, "a/b/c/d:", self.n_strand[ 0 ][ self.top1allele ], self.n_strand[ 0 ][ self.top2allele ], self.n_strand[ 1 ][ self.top1allele ], self.n_strand[ 1 ][ self.top2allele ] - #SBscore = self.SB_score_ChIP( self.n_strand[ 0 ][ self.top1allele ], self.n_strand[ 0 ][ self.top2allele ], self.n_strand[ 1 ][ self.top1allele ], self.n_strand[ 1 ][ self.top2allele ] ) - #SBscore = 0 - #if SBscore >= 1: - # print "disgard variant at", self.ref_pos, "type", self.type - # self.filterout = True - - # if self.ref_allele == self.top1allele: - # self.n_strand[ 0 ][ self.top1allele ] + self.n_strand[ 1 ][ self.top1allele ] - # if and self.n_strand[ 0 ][ self.top2allele ] == 0 or self.n_strand[ 1 ][ self.top2allele ] == 0: - # self.filterout = True - # print self.ref_pos - - - # self.deltaBIC = self.deltaBIC - - tmp_mutation_type = [] - for tmp_alt in self.alt_allele.split(b','): - if tmp_alt == b'*': - tmp_mutation_type.append( "Deletion" ) - elif len( tmp_alt ) > 1: - tmp_mutation_type.append( "Insertion" ) - else: - tmp_mutation_type.append( "SNV" ) - self.mutation_type = ",".join( tmp_mutation_type ) - return - - cdef float SB_score_ChIP( self, int a, int b, int c, int d ): - """ calculate score for filtering variants with strange strand biases. - - a: top1/major allele plus strand - b: top2/minor allele plus strand - c: top1/major allele minus strand - d: top2/minor allele minus strand - - Return a float value so that if this value >= 1, the variant will be filtered out. - """ - cdef: - float score - double p - double p1_l, p1_r - double p2_l, p2_r - double top2_sb, top1_sb - - if a+b == 0 or c+d == 0: - # if major allele and minor allele both bias to the same strand, allow it - return 0.0 - - # Rule: - # if there is bias in top2 allele then bias in top1 allele should not be significantly smaller than it. - # or there is no significant bias (0.5) in top2 allele. - - #print a, b, c, d - p1_l = binomial_cdf( a, (a+c), 0.5, lower=True ) # alternative: less than 0.5 - p1_r = binomial_cdf( c, (a+c), 0.5, lower=True ) # greater than 0.5 - p2_l = binomial_cdf( b, (b+d), 0.5, lower=True ) # alternative: less than 0.5 - p2_r = binomial_cdf( d, (b+d), 0.5, lower=True ) # greater than 0.5 - #print p1_l, p1_r, p2_l, p2_r - - if (p1_l < 0.05 and p2_r < 0.05) or (p1_r < 0.05 and p2_l < 0.05): - # we reject loci where the significant biases are inconsistent between top1 and top2 alleles. - return 1.0 - else: - # if b<=2 and d=0 or b=0 and d<=2 -- highly possible FPs - #if ( b<=2 and d==0 or b==0 and d<=2 ): - # return 1 - # can't decide - return 0.0 - - cdef float SB_score_ATAC( self, int a, int b, int c, int d ): - """ calculate score for filtering variants with strange strand biases. - - ATAC-seq version - - a: top1/major allele plus strand - b: top2/minor allele plus strand - c: top1/major allele minus strand - d: top2/minor allele minus strand - - Return a float value so that if this value >= 1, the variant will be filtered out. - """ - cdef: - float score - double p - double p1_l, p1_r - double p2_l, p2_r - double top2_sb, top1_sb - - if a+b == 0 or c+d == 0: - # if major allele and minor allele both bias to the same strand, allow it - return 0.0 - - # Rule: - # if there is bias in top2 allele then bias in top1 allele should not be significantly smaller than it. - # or there is no significant bias (0.5) in top2 allele. - - #print a, b, c, d - p1_l = binomial_cdf( a, (a+c), 0.5, lower=True ) # alternative: less than 0.5 - p1_r = binomial_cdf( c, (a+c), 0.5, lower=True ) # greater than 0.5 - p2_l = binomial_cdf( b, (b+d), 0.5, lower=True ) # alternative: less than 0.5 - p2_r = binomial_cdf( d, (b+d), 0.5, lower=True ) # greater than 0.5 - #print p1_l, p1_r, p2_l, p2_r - - if (p1_l < 0.05 and p2_r < 0.05) or (p1_r < 0.05 and p2_l < 0.05): - # we reject loci where the significant biases are inconsistent between top1 and top2 alleles. - return 1.0 - else: - # can't decide - return 0.0 - - cpdef str to_vcf ( self ): - """Output REF,ALT,QUAL,FILTER,INFO,FORMAT, SAMPLE columns. - """ - cdef: - str vcf_ref, vcf_alt, vcf_qual, vcf_filter, vcf_info, vcf_format, vcf_sample - - vcf_ref = self.ref_allele.decode() - vcf_alt = self.alt_allele.decode() - vcf_qual = "%d" % self.GQ - vcf_filter = "." - vcf_info = (b"M=%s;MT=%s;DPT=%d;DPC=%d;DP1T=%d%s;DP2T=%d%s;DP1C=%d%s;DP2C=%d%s;SB=%d,%d,%d,%d;DBIC=%.2f;BICHOMOMAJOR=%.2f;BICHOMOMINOR=%.2f;BICHETERNOAS=%.2f;BICHETERAS=%.2f;AR=%.2f" % \ - (self.type.encode(), self.mutation_type.encode(), sum( self.n_reads_T.values() ), sum( self.n_reads_C.values() ), - self.n_reads_T[self.top1allele], self.top1allele, self.n_reads_T[self.top2allele], self.top2allele, - self.n_reads_C[self.top1allele], self.top1allele, self.n_reads_C[self.top2allele], self.top2allele, - self.n_strand[ 0 ][ self.top1allele ], self.n_strand[ 0 ][ self.top2allele ], self.n_strand[ 1 ][ self.top1allele ], self.n_strand[ 1 ][ self.top2allele ], - self.deltaBIC, - self.BIC_homo_major, self.BIC_homo_minor, self.BIC_heter_noAS,self.BIC_heter_AS, - self.n_reads_T[self.top1allele]/(self.n_reads_T[self.top1allele]+self.n_reads_T[self.top2allele]) - )).decode() - vcf_format = "GT:DP:GQ:PL" - vcf_sample = "%s:%d:%d:%d,%d,%d" % (self.GT, self.raw_read_depth( opt = "all" ), self.GQ, self.PL_00, self.PL_01, self.PL_11) - return "\t".join( ( vcf_ref, vcf_alt, vcf_qual, vcf_filter, vcf_info, vcf_format, vcf_sample ) ) - - cpdef toVariant ( self ): - cdef: - object v - v = Variant( - self.ref_allele.decode(), - self.alt_allele.decode(), - self.GQ, - '.', - self.type, - self.mutation_type, - self.top1allele.decode(), - self.top2allele.decode(), - sum( self.n_reads_T.values() ), - sum( self.n_reads_C.values() ), - self.n_reads_T[self.top1allele], - self.n_reads_T[self.top2allele], - self.n_reads_C[self.top1allele], - self.n_reads_C[self.top2allele], - self.n_strand[ 0 ][ self.top1allele ], - self.n_strand[ 0 ][ self.top2allele ], - self.n_strand[ 1 ][ self.top1allele ], - self.n_strand[ 1 ][ self.top2allele ], - self.deltaBIC, - self.BIC_homo_major, - self.BIC_homo_minor, - self.BIC_heter_noAS, - self.BIC_heter_AS, - self.n_reads_T[self.top1allele]/(self.n_reads_T[self.top1allele]+self.n_reads_T[self.top2allele]), - self.GT, - self.raw_read_depth( opt = "all" ), - self.PL_00, - self.PL_01, - self.PL_11 ) - return v diff --git a/MACS3/Signal/RACollection.pxd b/MACS3/Signal/RACollection.pxd new file mode 100644 index 00000000..ce14c068 --- /dev/null +++ b/MACS3/Signal/RACollection.pxd @@ -0,0 +1,61 @@ +cdef extern from "fml.h": + ctypedef struct bseq1_t: + int l_seq + char *seq + char *qual # NULL-terminated strings; length expected to match $l_seq + + ctypedef struct magopt_t: + int flag, min_ovlp, min_elen, min_ensr, min_insr, max_bdist, max_bdiff, max_bvtx, min_merge_len, trim_len, trim_depth + float min_dratio1, max_bcov, max_bfrac + + ctypedef struct fml_opt_t: + int n_threads # number of threads; don't use multi-threading for small data sets + int ec_k # k-mer length for error correction; 0 for auto estimate + int min_cnt, max_cnt # both occ threshold in ec and tip threshold in cleaning lie in [min_cnt,max_cnt] + int min_asm_ovlp # min overlap length during assembly + int min_merge_len # during assembly, don't explicitly merge an overlap if shorter than this value + magopt_t mag_opt # graph cleaning options + + ctypedef struct fml_ovlp_t: + unsigned int len_, from_, id_, to_ + #unit32_t from # $from and $to: 0 meaning overlapping 5'-end; 1 overlapping 3'-end + #unsigned int id + #unsigned int to # $id: unitig number + + ctypedef struct fml_utg_t: + int len # length of sequence + int nsr # number of supporting reads + char *seq # unitig sequence + char *cov # cov[i]-33 gives per-base coverage at i + int n_ovlp[2] # number of 5'-end [0] and 3'-end [1] overlaps + fml_ovlp_t *ovlp # overlaps, of size n_ovlp[0]+n_ovlp[1] + + void fml_opt_init(fml_opt_t *opt) + fml_utg_t* fml_assemble(const fml_opt_t *opt, int n_seqs, bseq1_t *seqs, int *n_utg) + void fml_utg_destroy(int n_utg, fml_utg_t *utg) + void fml_utg_print(int n_utgs, const fml_utg_t *utg) + bseq1_t *bseq_read(const char *fn, int *n) + +# --- end of fermi-lite functions --- + +# --- smith-waterman alignment functions --- + +cdef extern from "swalign.h": + ctypedef struct seq_pair_t: + char *a + unsigned int alen + char *b + unsigned int blen + ctypedef struct align_t: + seq_pair_t *seqs + char *markup; + int start_a + int start_b + int end_a + int end_b + int matches + int gaps + double score + align_t *smith_waterman(seq_pair_t *problem) + void destroy_seq_pair(seq_pair_t *pair) + void destroy_align(align_t *ali) diff --git a/MACS3/Signal/RACollection.py b/MACS3/Signal/RACollection.py new file mode 100644 index 00000000..6f8ca04b --- /dev/null +++ b/MACS3/Signal/RACollection.py @@ -0,0 +1,906 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-22 16:26:57 Tao Liu> + +"""Module for ReadAlignment collection + +Copyright (c) 2024 Tao Liu + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file COPYING included +with the distribution). + +@status: experimental +@version: $Revision$ +@author: Tao Liu +@contact: vladimir.liu@gmail.com +""" +# ------------------------------------ +# python modules +# ------------------------------------ +from collections import Counter +from operator import itemgetter +from copy import copy + +from MACS3.Signal.ReadAlignment import ReadAlignment +from MACS3.Signal.PosReadsInfo import PosReadsInfo +from MACS3.Signal.UnitigRACollection import UnitigRAs, UnitigCollection +from MACS3.IO.PeakIO import PeakIO + +import cython +from cython.cimports.cpython import bool +# from cython.cimports.cpython.mem import PyMem_Malloc, PyMem_Free + +from cython.cimports.libc.stdlib import malloc, free + +# ------------------------------------ +# constants +# ------------------------------------ +__version__ = "Parser $Revision$" +__author__ = "Tao Liu " +__doc__ = "All Parser classes" + +__DNACOMPLEMENT__ = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@TBGDEFCHIJKLMNOPQRSAUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff' # A trans table to convert A to T, C to G, G to C, and T to A. + +__CIGARCODE__ = "MIDNSHP=X" + +# ------------------------------------ +# Misc functions +# ------------------------------------ + +# ------------------------------------ +# Classes +# ------------------------------------ + + +@cython.cclass +class RACollection: + """A collection of ReadAlignment objects and the corresponding + PeakIO. + + """ + chrom: bytes + peak: PeakIO # A PeakIO object + RAlists: list # contain ReadAlignment lists for treatment (0) and control (1) + left: cython.long # left position of peak + right: cython.long # right position of peak + length: cython.long # length of peak + RAs_left: cython.long # left position of all RAs in the collection + RAs_right: cython.long # right position of all RAs in the collection + sorted: bool # if sorted by lpos + peak_refseq: bytes # reference sequence in peak region b/w left and right + peak_refseq_ext: bytes # reference sequence in peak region with extension on both sides b/w RAs_left and RAs_right + + def __init__(self, chrom: bytes, peak: PeakIO, RAlist_T: list, RAlist_C: list = []): + """Create RACollection by: object taking: + + 1. peak: a PeakIO indicating: object the peak region. + + 2. RAlist: a python of: list ReadAlignment objects containing + all the reads overlapping the peak region. If no RAlist_C + given, it will be []. + + """ + if len(RAlist_T) == 0: + # no reads, return None + raise Exception("No reads from ChIP sample to construct RAcollection!") + self.chrom = chrom + self.peak = peak + # print(len(RAlist_T),"\n") + # print(len(RAlist_C),"\n") + self.RAlists = [RAlist_T, RAlist_C] + self.left = peak["start"] + self.right = peak["end"] + self.length = self.right - self.left + if RAlist_T: + self.RAs_left = RAlist_T[0]["lpos"] # initial assignment of RAs_left + self.RAs_right = RAlist_T[-1]["rpos"] # initial assignment of RAs_right + self.sort() # it will set self.sorted = True + else: + self.RAs_left = -1 + self.RAs_right = -1 + # check RAs_left and RAs_right + for ra in RAlist_T: + if ra["lpos"] < self.RAs_left: + self.RAs_left = ra["lpos"] + if ra["rpos"] > self.RAs_right: + self.RAs_right = ra["rpos"] + + for ra in RAlist_C: + if ra["lpos"] < self.RAs_left: + self.RAs_left = ra["lpos"] + if ra["rpos"] > self.RAs_right: + self.RAs_right = ra["rpos"] + (self.peak_refseq, self.peak_refseq_ext) = self.__get_peak_REFSEQ() + + def __getitem__(self, keyname): + if keyname == "chrom": + return self.chrom + elif keyname == "left": + return self.left + elif keyname == "right": + return self.right + elif keyname == "RAs_left": + return self.RAs_left + elif keyname == "RAs_right": + return self.RAs_right + elif keyname == "length": + return self.length + elif keyname == "count": + return len(self.RAlists[0]) + len(self.RAlists[1]) + elif keyname == "count_T": + return len(self.RAlists[0]) + elif keyname == "count_C": + return len(self.RAlists[1]) + elif keyname == "peak_refseq": + return self.peak_refseq + elif keyname == "peak_refseq_ext": + return self.peak_refseq_ext + else: + raise KeyError("Unavailable key:", keyname) + + def __getstate__(self): + #return {"chrom":self.chrom, "peak":self.peak, "RAlists":self.RAlists, + # "left":self.left, "right":self.right, "length": self.length, + # "RAs_left":self.RAs_left, "RAs_right":self.RAs_right} + return (self.chrom, self.peak, self.RAlists, self.left, self.right, + self.length, self.RAs_left, self.RAs_right, self.peak_refseq, + self.peak_refseq_ext) + + def __setstate__(self, state): + (self.chrom, self.peak, self.RAlists, self.left, self.right, + self.length, self.RAs_left, self.RAs_right, self.peak_refseq, + self.peak_refseq_ext) = state + + @cython.ccall + def sort(self): + """Sort RAs according to lpos. Should be used after realignment. + + """ + if self.RAlists[0]: + self.RAlists[0].sort(key=itemgetter("lpos")) + if self.RAlists[1]: + self.RAlists[1].sort(key=itemgetter("lpos")) + self.sorted = True + return + + @cython.ccall + def remove_outliers(self, percent: cython.int = 5): + """ Remove outliers with too many n_edits. The outliers with + n_edits in top p% will be removed. + + Default: remove top 5% of reads that have too many differences + with reference genome. + """ + n_edits_list: list + ralist: list + read: ReadAlignment # ReadAlignment object + highest_n_edits: cython.int + new_RAlist: list + i: cython.int + + n_edits_list = [] + for ralist in self.RAlists: + for read in ralist: + n_edits_list.append(read["n_edits"]) + n_edits_list.sort() + highest_n_edits = n_edits_list[int(len(n_edits_list) * (1 - percent * .01))] + + for i in (range(len(self.RAlists))): + new_RAlist = [] + for read in self.RAlists[i]: + if read["n_edits"] <= highest_n_edits: + new_RAlist.append(read) + self.RAlists[i] = new_RAlist + + return + + @cython.ccall + def n_edits_sum(self) -> cython.int: + """ + """ + n_edits_list: list + ralist: list + read: ReadAlignment + c: cython.int + # highest_n_edits: cython.int + + n_edits_list = [] + + for ralist in self.RAlists: + for read in ralist: + n_edits_list.append(read["n_edits"]) + + n_edits_list.sort() + # print (n_edits_list) + c = Counter(n_edits_list) + return c + # print(c) + + @cython.cfunc + def __get_peak_REFSEQ(self) -> tuple: + """Get the reference sequence within the peak region. + + """ + peak_refseq: bytearray + # i: cython.int + # prev_r: cython.long #remember the previous filled right end + start: cython.long + end: cython.long + # ind: cython.long + # ind_r: cython.long + # read: ReadAlignment + # read_refseq_ext: bytearray + # read_refseq: bytearray + + start = min(self.RAs_left, self.left) + end = max(self.RAs_right, self.right) + # print ("left",start,"right",end) + peak_refseq_ext = bytearray(b'N' * (end - start)) + + # for treatment. + peak_refseq_ext = self.__fill_refseq(peak_refseq_ext, + self.RAlists[0]) + # and control if available. + if self.RAlists[1]: + peak_refseq_ext = self.__fill_refseq(peak_refseq_ext, + self.RAlists[1]) + + # trim + peak_refseq = peak_refseq_ext[self.left - start: self.right - start] + return (bytes(peak_refseq), bytes(peak_refseq_ext)) + + @cython.cfunc + def __fill_refseq(self, + seq: bytearray, + ralist: list) -> bytearray: + """Fill refseq sequence of whole peak with refseq sequences of + each read in ralist. + + """ + prev_r: cython.long # previous right position of last + # filled + ind: cython.long + ind_r: cython.long + start: cython.long + # end: cython.long + read: ReadAlignment + read_refseq: bytearray + + start = min(self.RAs_left, self.left) + + # print(len(ralist),"\n") + prev_r = ralist[0]["lpos"] + + for i in range(len(ralist)): + read = ralist[i] + if read["lpos"] > prev_r: + read = ralist[i - 1] + read_refseq = read.get_REFSEQ() + ind = read["lpos"] - start + ind_r = ind + read["rpos"] - read["lpos"] + seq[ind: ind_r] = read_refseq + prev_r = read["rpos"] + # last + read = ralist[-1] + read_refseq = read.get_REFSEQ() + ind = read["lpos"] - start + ind_r = ind + read["rpos"] - read["lpos"] + seq[ind: ind_r] = read_refseq + return seq + + @cython.ccall + def get_PosReadsInfo_ref_pos(self, + ref_pos: cython.long, + ref_nt: bytes, + Q: cython.int = 20): + """Generate a PosReadsInfo for: object a given reference genome + position. + + Return a PosReadsInfo object. + + """ + s: bytearray + bq: bytearray + strand: cython.int + ra: ReadAlignment + # bq_list_t: list = [] + # bq_list_c: list = [] + i: cython.int + pos: cython.int + tip: bool + posreadsinfo_p: PosReadsInfo + + posreadsinfo_p = PosReadsInfo(ref_pos, ref_nt) + + # Treatment group + for i in range(len(self.RAlists[0])): + ra = self.RAlists[0][i] + if ra["lpos"] <= ref_pos and ra["rpos"] > ref_pos: + (s, bq, strand, tip, pos) = ra.get_variant_bq_by_ref_pos(ref_pos) + posreadsinfo_p.add_T(i, bytes(s), bq[0], strand, tip, Q=Q) + + # Control group + for i in range(len(self.RAlists[1])): + ra = self.RAlists[1][i] + if ra["lpos"] <= ref_pos and ra["rpos"] > ref_pos: + (s, bq, strand, tip, pos) = ra.get_variant_bq_by_ref_pos(ref_pos) + posreadsinfo_p.add_C(i, bytes(s), bq[0], strand, Q=Q) + + return posreadsinfo_p + + @cython.ccall + def get_FASTQ(self) -> bytearray: + """Get FASTQ file for all reads in RACollection. + + """ + ra: ReadAlignment + fastq_text: bytearray + + fastq_text = bytearray(b"") + + for ra in self.RAlists[0]: + fastq_text += ra.get_FASTQ() + + for ra in self.RAlists[1]: + fastq_text += ra.get_FASTQ() + + return fastq_text + + @cython.cfunc + def fermi_assemble(self, + fermiMinOverlap: cython.int, + opt_flag: cython.int = 0x80) -> list: + """A wrapper function to call Fermi unitig building functions. + """ + opt: cython.pointer(fml_opt_t) + # c: cython.int + n_seqs: cython.int + n_utg: cython.pointer(cython.int) + seqs: cython.pointer(bseq1_t) + utg: cython.pointer(fml_utg_t) + p: fml_utg_t + + # unitig_k: cython.int + # merge_min_len: cython.int + tmps: bytes + tmpq: bytes + # ec_k: cython.int = -1 + l: cython.long + cseq: cython.pointer(cython.char) + cqual: cython.pointer(cython.char) + i: cython.int + j: cython.int + # tmpunitig: bytes + # unitig: bytes # final unitig + unitig_list: list # contain of: list sequences in format: bytes + # n: cython.pointer(cython.int) + + n_seqs = len(self.RAlists[0]) + len(self.RAlists[1]) + + # prn_seqs: cython.int + + # prepare seq and qual, note, we only extract SEQ according to the + + # strand of reference sequence. + seqs = cython.cast(cython.pointer(bseq1_t), + malloc(n_seqs * cython.sizeof(bseq1_t))) # we rely on fermi-lite to free this mem + + i = 0 + for ra in self.RAlists[0]: + tmps = ra["SEQ"] + tmpq = ra["QUAL"] + l = len(tmps) + # we rely on fermi-lite to free this mem + cseq = cython.cast(cython.pointer(cython.char), + malloc((l+1)*cython.sizeof(cython.char))) + # we rely on fermi-lite to free this mem + cqual = cython.cast(cython.pointer(cython.char), + malloc((l+1)*cython.sizeof(cython.char))) + for j in range(l): + cseq[j] = tmps[j] + cqual[j] = tmpq[j] + 33 + cseq[l] = b'\x00' + cqual[l] = b'\x00' + + seqs[i].seq = cseq + seqs[i].qual = cqual + seqs[i].l_seq = len(tmps) + i += 1 + + # print "@",ra["readname"].decode() + # prcseq: cython.int.decode() + # print "+" + # prcqual: cython.int.decode() + + for ra in self.RAlists[1]: + tmps = ra["SEQ"] + tmpq = ra["QUAL"] + l = len(tmps) + # we rely on fermi-lite to free this mem + cseq = cython.cast(cython.pointer(cython.char), + malloc((l+1)*cython.sizeof(cython.char))) + # we rely on fermi-lite to free this mem + cqual = cython.cast(cython.pointer(cython.char), + malloc((l+1)*cython.sizeof(cython.char))) + for j in range(l): + cseq[j] = tmps[j] + cqual[j] = tmpq[j] + 33 + cseq[l] = b'\x00' + cqual[l] = b'\x00' + + seqs[i].seq = cseq + seqs[i].qual = cqual + seqs[i].l_seq = len(tmps) + i += 1 + # print "@",ra["readname"].decode() + # prcseq: cython.int.decode() + # print "+" + # prcqual: cython.int.decode() + + # if self.RAlists[1]: + # unitig_k=int(min(self.RAlists[0][0]["l"],self.RAlists[1][0]["l"])*fermiOverlapMinRatio) + + # merge_min_len=int(min(self.RAlists[0][0]["l"],self.RAlists[1][0]["l"])*0.5) + # else: + # unitig_k = int(self.RAlists[0][0]["l"]*fermiOverlapMinRatio) + + # merge_min_len=int(self.RAlists[0][0]["l"]*0.5) + #fermiMinOverlap = int(self.RAlists[0][0]["l"]*fermiOverlapMinRatio) + + # minimum overlap to merge, default 0 + # merge_min_len= max(25, int(self.RAlists[0][0]["l"]*0.5)) + # merge_min_len= int(self.RAlists[0][0]["l"]*0.5) + + # opt = cython.cast(cython.pointer(fml_opt_t), + # PyMem_Malloc(cython.sizeof(fml_opt_t))) + # n_utg = cython.cast(cython.pointer(cython.int), + # PyMem_Malloc(cython.sizeof(int))) + + opt = cython.cast(cython.pointer(fml_opt_t), + malloc(cython.sizeof(fml_opt_t))) + n_utg = cython.cast(cython.pointer(cython.int), + malloc(cython.sizeof(int))) + + fml_opt_init(opt) + # k-mer length for error correction (0 for auto; -1 to disable) + # opt.ec_k = 0 + + # min overlap length during initial assembly + opt.min_asm_ovlp = fermiMinOverlap + + # minimum length to merge, during assembly, don't explicitly merge an overlap if shorter than this value + # opt.min_merge_len = merge_min_len + + # there are more 'options' for mag clean: + # flag, min_ovlp, min_elen, min_ensr, min_insr, max_bdist, max_bdiff, max_bvtx, min_merge_len, trim_len, trim_depth, min_dratio1, max_bcov, max_bfrac + # min_elen (300) will be adjusted + # min_ensr (4), min_insr (3) will be computed + # min_merge_len (0) will be updated using opt.min_merge_len + + # We can adjust: flag (0x40|0x80), min_ovlp (0), min_dratio1 (0.7), max_bdiff (50), max_bdist (512), max_bvtx (64), trim_len (0), trim_depth (6), max_bcov (10.), max_bfrac (0.15) + + # 0x20: MAG_F_AGGRESSIVE pop variant bubbles + # 0x40: MAG_F_POPOPEN aggressive tip trimming + # 0x80: MAG_F_NO_SIMPL skip bubble simplification + opt.mag_opt.flag = opt_flag + + # mag_opt.min_ovlp + #opt.mag_opt.min_ovlp = fermiMinOverlap + + # drop an overlap if its length is below maxOvlpLen*FLOAT + #opt.mag_opt.min_dratio1 = 0.5 + + # retain a bubble if one side is longer than the other side by >INT-bp + #opt.mag_opt.max_bdiff = 10#merge_min_len + + # trim_len: + # trim_depth: Parameter used to trim the open end/tip. If trim_len == 0, do nothing + + # max_bdist: + # max_bvtx: Parameter used to simply bubble while 0x80 flag is set. + #opt.mag_opt.max_bdist = 1024 + #opt.mag_opt.max_bvtx = 128 + + # max_bcov: + # max_bfrac: Parameter used when aggressive bubble removal is not used. Bubble will be removed if its average coverage lower than max_bcov and fraction (cov1/(cov1+cov2)) is lower than max_bfrac + #opt.mag_opt.max_bcov = 10. + #opt.mag_opt.max_bfrac = 0.01 + + utg = fml_assemble(opt, n_seqs, seqs, n_utg) + # get results + unitig_list = [] + for i in range(n_utg[0]): + p = utg[i] + if (p.len < 0): + continue + # unitig = b'' + # for j in range(p.len): + # unitig += [b'A',b'C',b'G',b'T',b'N'][int(p.seq[j]) - 1] + # unitig_list.append(unitig) + unitig_list.append(p.seq) + + fml_utg_destroy(n_utg[0], utg) + + # PyMem_Free(opt) + # PyMem_Free(n_utg) + free(opt) + free(n_utg) + + return unitig_list + + @cython.cfunc + def align_unitig_to_REFSEQ(self, unitig_list: list) -> tuple: + """Note: we use smith waterman, but we don't use linear gap + penalty at this time. + + Also, if unitig is mapped to - strand, we will revcomp the + unitig. So the unitig_will: list be changed in this case. + """ + unitig: bytes + problem: seq_pair_t + results: cython.pointer(align_t) + # tmp: cython.pointer(cython.char) + target: bytes + reference: bytes + target_aln_f: bytes + target_aln_r: bytes + reference_aln_f: bytes + reference_aln_r: bytes + markup_aln_f: bytes + markup_aln_r: bytes + score_f: cython.double + score_r: cython.double + target_alns: list = [] + reference_alns: list = [] + markup_alns: list = [] + aln_scores: list = [] + i: cython.int + + reference = copy(self.peak_refseq_ext+b'\x00') + + for i in range(len(unitig_list)): + unitig = unitig_list[i] + target = copy(unitig + b'\x00') + # we use swalign.c for local alignment (without affine gap + # penalty). Will revise later. + problem.a = target + problem.alen = len(unitig) + problem.b = reference + problem.blen = len(self.peak_refseq_ext) + results = smith_waterman(cython.address(problem)) + target_aln_f = results.seqs.a + reference_aln_f = results.seqs.b + markup_aln_f = results.markup + score_f = results.score + free(results.seqs.a) + free(results.seqs.b) + free(results.markup) + free(results) + # end of local alignment + + # try reverse complement + target = copy(unitig[::-1] + b'\x00') + target = target.translate(__DNACOMPLEMENT__) + problem.a = target + problem.alen = len(unitig) + problem.b = reference + problem.blen = len(self.peak_refseq_ext) + results = smith_waterman(cython.address(problem)) + target_aln_r = results.seqs.a + reference_aln_r = results.seqs.b + markup_aln_r = results.markup + score_r = results.score + free(results.seqs.a) + free(results.seqs.b) + free(results.markup) + free(results) + # end of local alignment + + if score_f > score_r: + target_alns.append(target_aln_f) + reference_alns.append(reference_aln_f) + markup_alns.append(markup_aln_f) + aln_scores.append(score_f) + else: + target_alns.append(target_aln_r) + reference_alns.append(reference_aln_r) + markup_alns.append(markup_aln_r) + aln_scores.append(score_r) + # we will revcomp unitig + unitig = unitig[::-1] + unitig_list[i] = unitig.translate(__DNACOMPLEMENT__) + + return (target_alns, reference_alns, aln_scores, markup_alns) + + @cython.cfunc + def verify_alns(self, unitig_list, unitig_alns, reference_alns, aln_scores, markup_alns, min_score_100: cython.float = 150): + """Remove aln/unitig if it contains too many edits in a small region + + default min score is 150, which means under 2/-3/-5/-2 scoring schema, there are 10 mismatches within 100bps region. + """ + i: cython.int + + for i in range(len(unitig_list)-1, -1, -1): + # pri: cython.int, aln_scores[i] + # prunitig_alns: cython.int[i] + # prmarkup_alns: cython.int[i] + # prreference_alns: cython.int[i] + if aln_scores[i] * 100 / len(markup_alns[i]) < min_score_100: + unitig_list.pop(i) + unitig_alns.pop(i) + reference_alns.pop(i) + aln_scores.pop(i) + markup_alns.pop(i) + return + + @cython.cfunc + def filter_unitig_with_bad_aln(self, unitig_list: list, + target_alns: list, + reference_alns: list, + gratio: float = 0.25) -> tuple: + """Remove unitigs that has too much gaps (both on target and + reference) during alignments. + + """ + pass + + @cython.cfunc + def remap_RAs_w_unitigs(self, unitig_list: list) -> list: + """Remap RAs to unitigs, requiring perfect match. + + Return RAlists_T, RAlists_C, unmapped_racollection. + """ + RAlists_T: list = [] # lists of of: list RAs of ChIP mapped to each unitig + RAlists_C: list = [] + unmapped_RAlist_T: list = [] # of: list RAs of ChIP unmappable to unitigs + unmapped_RAlist_C: list = [] + # RACollection unmapped_ra_collection + flag: cython.int = 0 + i: cython.int + tmp_ra: ReadAlignment + tmp_ra_seq: bytes + unitig: bytes + + for i in range(len(unitig_list)): + RAlists_T.append([]) # for each unitig, there is another of: list RAs + RAlists_C.append([]) + + # assign RAs to unitigs + + for tmp_ra in self.RAlists[0]: + flag = 0 + tmp_ra_seq = tmp_ra["SEQ"] + for i in range(len(unitig_list)): + unitig = unitig_list[i] + if tmp_ra_seq in unitig: + flag = 1 + RAlists_T[i].append(tmp_ra) + break + if flag == 0: + unmapped_RAlist_T.append(tmp_ra) + # print "unmapped:", tmp_ra["SEQ"] + + for tmp_ra in self.RAlists[1]: + flag = 0 + tmp_ra_seq = tmp_ra["SEQ"] + for i in range(len(unitig_list)): + unitig = unitig_list[i] + if tmp_ra_seq in unitig: + flag = 1 + RAlists_C[i].append(tmp_ra) + break + if flag == 0: + unmapped_RAlist_C.append(tmp_ra) + # print "unmapped:", tmp_ra["SEQ"] + + # if unmapped_RAlist_T: + # unmapped_ra_collection = RACollection(self.chrom, self.peak, unmapped_RAlist_T, unmapped_RAlist_C) + return [RAlists_T, RAlists_C, unmapped_RAlist_T, unmapped_RAlist_C] + + @cython.cfunc + def add_to_unitig_list(self, unitig_list, unitigs_2nd) -> list: + """ + """ + i: cython.int + j: cython.int + flag: cython.int + u0: bytes + u1: bytes + new_unitig_list: list + + new_unitig_list = [] + + for i in range(len(unitigs_2nd)): + # initial value: can't be found in unitig_list + flag = 0 + u0 = unitigs_2nd[i] + for j in range(len(unitig_list)): + u1 = unitig_list[j] + if u1.find(u0) != -1: + flag = 1 + break + u1 = u1[::-1].translate(__DNACOMPLEMENT__) + if u1.find(u0) != -1: + flag = 1 + break + if not flag: + new_unitig_list.append(u0) + new_unitig_list.extend(unitig_list) + return new_unitig_list + + @cython.ccall + def build_unitig_collection(self, fermiMinOverlap): + """unitig_and: list tuple_alns are in the same order! + + return UnitigCollection object. + + """ + start: cython.long + end: cython.long + unitigs_2nd: list + # u: bytes + # target_alns: list + reference_alns: list + aln_scores: list + markup_alns: list + # target_alns_2nd: list + # reference_alns_2nd: list + # aln_scores_2nd: list + RAlists_T: list = [] # lists of of: list RAs of ChIP mapped to each unitig + RAlists_C: list = [] + unmapped_RAlist_T: list = [] + unmapped_RAlist_C: list = [] + # tmp_unitig_seq: bytes + tmp_reference_seq: bytes + tmp_unitig_aln: bytes + tmp_reference_aln: bytes + + i: cython.int + j: cython.int + left_padding_ref: cython.long + right_padding_ref: cython.long + left_padding_unitig: cython.long + right_padding_unitig: cython.long + ura_list: list = [] + unmapped_ra_collection: RACollection + # flag: cython.int = 0 + # n_unmapped: cython.int + n_unitigs_0: cython.int + n_unitigs_1: cython.int + + # first round of assembly + # print (" First round to assemble unitigs") + unitig_list = self.fermi_assemble(fermiMinOverlap, opt_flag=0x80) + if len(unitig_list) == 0: + return 0 + + n_unitigs_0 = -1 + n_unitigs_1 = len(unitig_list) + # print " # of Unitigs:", n_unitigs_1 + # print " Map reads to unitigs" + (unitig_alns, reference_alns, aln_scores, markup_alns) = self.align_unitig_to_REFSEQ(unitig_list) + + self.verify_alns(unitig_list, + unitig_alns, + reference_alns, + aln_scores, + markup_alns) + if len(unitig_list) == 0: + # if stop here, it raises a flag that the region may + # contain too many mismapped reads, we return -1 + return -1 + # print (" # of Unitigs:", n_unitigs_1) + + # assign RAs to unitigs + [RAlists_T, RAlists_C, unmapped_RAlist_T, unmapped_RAlist_C] = self.remap_RAs_w_unitigs(unitig_list) + # prunmapped_ra_collection: cython.int.get_FASTQ().decode() + + # n_unmapped = len(unmapped_RAlist_T) + len(unmapped_RAlist_C) + + while len(unmapped_RAlist_T) > 0 and n_unitigs_1 != n_unitigs_0: + # if there are unmapped reads AND we can get more unitigs + # from last round of assembly, do assembly again + + # print (" # of RAs not mapped, will be assembled again:", n_unmapped) + n_unitigs_0 = n_unitigs_1 + # another round of assembly + unmapped_ra_collection = RACollection(self.chrom, + self.peak, + unmapped_RAlist_T, + unmapped_RAlist_C) + unitigs_2nd = unmapped_ra_collection.fermi_assemble(fermiMinOverlap, + opt_flag=0x80) + + if unitigs_2nd: + unitig_list = self.add_to_unitig_list(unitig_list, unitigs_2nd) + n_unitigs_1 = len(unitig_list) + # print " # of Unitigs:", n_unitigs_1 + # print " Map reads to unitigs" + (unitig_alns, reference_alns, aln_scores, markup_alns) = self.align_unitig_to_REFSEQ(unitig_list) + self.verify_alns(unitig_list, + unitig_alns, + reference_alns, + aln_scores, + markup_alns) + [RAlists_T, RAlists_C, unmapped_RAlist_T, unmapped_RAlist_C] = self.remap_RAs_w_unitigs(unitig_list) + # n_unmapped = len(unmapped_RAlist_T) + len(unmapped_RAlist_C) + # else: + # for r in unmapped_RAlist_T: + # prr: cython.int.get_FASTQ().decode().lstrip() + + # print (" # of RAs not mapped, will be assembled again with 1/2 of fermiMinOverlap:", n_unmapped) + # another round of assembly + unmapped_ra_collection = RACollection(self.chrom, + self.peak, + unmapped_RAlist_T, + unmapped_RAlist_C) + unitigs_2nd = unmapped_ra_collection.fermi_assemble(fermiMinOverlap/2, + opt_flag=0x80) + + if unitigs_2nd: + unitig_list = self.add_to_unitig_list(unitig_list, unitigs_2nd) + n_unitigs_1 = len(unitig_list) + # print " # of Unitigs:", n_unitigs_1 + # print " Map reads to unitigs" + (unitig_alns, reference_alns, aln_scores, markup_alns) = self.align_unitig_to_REFSEQ(unitig_list) + self.verify_alns(unitig_list, + unitig_alns, + reference_alns, + aln_scores, + markup_alns) + [RAlists_T, RAlists_C, unmapped_RAlist_T, unmapped_RAlist_C] = self.remap_RAs_w_unitigs(unitig_list) + # n_unmapped = len(unmapped_RAlist_T) + len(unmapped_RAlist_C) + # else: + # for r in unmapped_RAlist_T: + # prr: cython.int.get_FASTQ().decode().lstrip() + if len(unitig_list) == 0: + raise Exception("Shouldn't reach here") + # print (" # of Unitigs:", n_unitigs_1) + + if len(unitig_list) == 0: + return None + # print (" Final round: # of Unitigs:", len(unitig_list)) + # print (" Final round: # of RAs not mapped:", n_unmapped) + + start = min(self.left, self.RAs_left) + end = max(self.right, self.RAs_right) + + # create UnitigCollection + for i in range(len(unitig_list)): + #b'---------------------------AAATAATTTTATGTCCTTCAGTACAAAAAGCAGTTTCAACTAAAACCCAGTAACAAGCTAGCAATTCCTTTTAAATGGTGCTACTTCAAGCTGCAGCCAGGTAGCTTTTTATTACAAAAAATCCCACAGGCAGCCACTAGGTGGCAGTAACAGGCTTTTGCCAGCGGCTCCAGTCAGCATGGCTTGACTGTGTGCTGCAGAAACTTCTTAAATCGTCTGTGTTTGGGACTCGTGGGGCCCCACAGGGCTTTACAAGGGCTTTTTAATTTCCAAAAACATAAAACAAAAAAA--------------' + #b'GATATAAATAGGATGTTATGAGTTTTCAAATAATTTTATGTCCTTCAGTACAAAAAGCAGTTTCAACTAAAACCCAGTAACAAGCTAGCAATTCCTTTTAAATGGTGCTACTTCAAGCTGCAGCCAGGTAGCTTTTTATTACAAAAA-TCCCACAGGCAGCCACTAGGTGGCAGTAACAGGCTTTTGCCAGCGGCTCCAGTCAGCATGGCTTGACTGTGTGCTGCAGAAACTTCTTAAATCGTCTGTGTTTGGGACTCGTGGGGCCCCACAGGGCTTTACAAGGGCTTTTTAATTTCCAAAAACATAAAACAAAAAAAAATACAAATGTATT' + tmp_unitig_aln = unitig_alns[i] + tmp_reference_aln = reference_alns[i] + # tmp_unitig_seq = tmp_unitig_aln.replace(b'-',b'') + tmp_reference_seq = tmp_reference_aln.replace(b'-', b'') + + # prtmp_unitig_aln: cython.int + # prtmp_reference_aln: cython.int + # prtmp_unitig_seq: cython.int + # prtmp_reference_aln: cython.int + + # find the position on self.peak_refseq_ext + left_padding_ref = self.peak_refseq_ext.find(tmp_reference_seq) # this number of nts should be skipped on refseq_ext from left + right_padding_ref = len(self.peak_refseq_ext) - left_padding_ref - len(tmp_reference_seq) # this number of nts should be skipped on refseq_ext from right + + # now, decide the lpos and rpos on reference of this unitig + # first, trim left padding '-' + left_padding_unitig = len(tmp_unitig_aln) - len(tmp_unitig_aln.lstrip(b'-')) + right_padding_unitig = len(tmp_unitig_aln) - len(tmp_unitig_aln.rstrip(b'-')) + + tmp_lpos = start + left_padding_ref + tmp_rpos = end - right_padding_ref + + for j in range(left_padding_unitig): + if tmp_reference_aln[j] != b'-': + tmp_lpos += 1 + for j in range(1, right_padding_unitig + 1): + if tmp_reference_aln[-j] != b'-': + tmp_rpos -= 1 + + tmp_unitig_aln = tmp_unitig_aln[left_padding_unitig:(len(tmp_unitig_aln)-right_padding_unitig)] + tmp_reference_aln = tmp_reference_aln[left_padding_unitig:(len(tmp_reference_aln)-right_padding_unitig)] + + ura_list.append(UnitigRAs(self.chrom, tmp_lpos, tmp_rpos, tmp_unitig_aln, tmp_reference_aln, [RAlists_T[i], RAlists_C[i]])) + + return UnitigCollection(self.chrom, self.peak, ura_list) diff --git a/MACS3/Signal/RACollection.pyx b/MACS3/Signal/RACollection.pyx deleted file mode 100644 index c0650dee..00000000 --- a/MACS3/Signal/RACollection.pyx +++ /dev/null @@ -1,898 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2021-03-10 23:39:52 Tao Liu> - -"""Module for SAPPER BAMParser class - -Copyright (c) 2017 Tao Liu - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file COPYING included -with the distribution). - -@status: experimental -@version: $Revision$ -@author: Tao Liu -@contact: tliu4@buffalo.edu -""" -# ------------------------------------ -# python modules -# ------------------------------------ -import struct -from collections import Counter -from operator import itemgetter -from copy import copy - -from MACS3.Signal.ReadAlignment import ReadAlignment -from MACS3.Signal.PosReadsInfo import PosReadsInfo -from MACS3.Signal.UnitigRACollection import UnitigRAs, UnitigCollection -from MACS3.IO.PeakIO import PeakIO - -from cpython cimport bool -from cpython.mem cimport PyMem_Malloc, PyMem_Realloc, PyMem_Free - -import numpy as np -cimport numpy as np -from numpy cimport uint32_t, uint64_t, int32_t, int64_t - -from libc.stdlib cimport malloc, free, realloc - -cdef extern from "stdlib.h": - ctypedef unsigned int size_t - size_t strlen(char *s) - #void *malloc(size_t size) - void *calloc(size_t n, size_t size) - #void free(void *ptr) - int strcmp(char *a, char *b) - char * strcpy(char *a, char *b) - long atol(char *bytes) - int atoi(char *bytes) - - -# --- fermi-lite functions --- -#define MAG_F_AGGRESSIVE 0x20 // pop variant bubbles (not default) -#define MAG_F_POPOPEN 0x40 // aggressive tip trimming (default) -#define MAG_F_NO_SIMPL 0x80 // skip bubble simplification (default) - -cdef extern from "fml.h": - ctypedef struct bseq1_t: - int32_t l_seq - char *seq - char *qual # NULL-terminated strings; length expected to match $l_seq - - ctypedef struct magopt_t: - int flag, min_ovlp, min_elen, min_ensr, min_insr, max_bdist, max_bdiff, max_bvtx, min_merge_len, trim_len, trim_depth - float min_dratio1, max_bcov, max_bfrac - - ctypedef struct fml_opt_t: - int n_threads # number of threads; don't use multi-threading for small data sets - int ec_k # k-mer length for error correction; 0 for auto estimate - int min_cnt, max_cnt # both occ threshold in ec and tip threshold in cleaning lie in [min_cnt,max_cnt] - int min_asm_ovlp # min overlap length during assembly - int min_merge_len # during assembly, don't explicitly merge an overlap if shorter than this value - magopt_t mag_opt # graph cleaning options - - ctypedef struct fml_ovlp_t: - uint32_t len_, from_, id_, to_ - #unit32_t from # $from and $to: 0 meaning overlapping 5'-end; 1 overlapping 3'-end - #uint32_t id - #uint32_t to # $id: unitig number - - ctypedef struct fml_utg_t: - int32_t len # length of sequence - int32_t nsr # number of supporting reads - char *seq # unitig sequence - char *cov # cov[i]-33 gives per-base coverage at i - int n_ovlp[2] # number of 5'-end [0] and 3'-end [1] overlaps - fml_ovlp_t *ovlp # overlaps, of size n_ovlp[0]+n_ovlp[1] - - void fml_opt_init(fml_opt_t *opt) - fml_utg_t* fml_assemble(const fml_opt_t *opt, int n_seqs, bseq1_t *seqs, int *n_utg) - void fml_utg_destroy(int n_utg, fml_utg_t *utg) - void fml_utg_print(int n_utgs, const fml_utg_t *utg) - bseq1_t *bseq_read(const char *fn, int *n) - -# --- end of fermi-lite functions --- - -# --- smith-waterman alignment functions --- - -cdef extern from "swalign.h": - ctypedef struct seq_pair_t: - char *a - unsigned int alen - char *b - unsigned int blen - ctypedef struct align_t: - seq_pair_t *seqs - char *markup; - int start_a - int start_b - int end_a - int end_b - int matches - int gaps - double score - align_t *smith_waterman(seq_pair_t *problem) - void destroy_seq_pair(seq_pair_t *pair) - void destroy_align(align_t *ali) - -# ------------------------------------ -# constants -# ------------------------------------ -__version__ = "Parser $Revision$" -__author__ = "Tao Liu " -__doc__ = "All Parser classes" - -__DNACOMPLEMENT__ = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@TBGDEFCHIJKLMNOPQRSAUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff' # A trans table to convert A to T, C to G, G to C, and T to A. - -__CIGARCODE__ = "MIDNSHP=X" - -# ------------------------------------ -# Misc functions -# ------------------------------------ - -# ------------------------------------ -# Classes -# ------------------------------------ - -cdef class RACollection: - """A collection of ReadAlignment objects and the corresponding - PeakIO. - - """ - cdef: - bytes chrom - object peak # A PeakIO object - list RAlists # contain ReadAlignment lists for treatment (0) and control (1) - long left # left position of peak - long right # right position of peak - long length # length of peak - long RAs_left # left position of all RAs in the collection - long RAs_right # right position of all RAs in the collection - bool sorted # if sorted by lpos - bytes peak_refseq # reference sequence in peak region b/w left and right - bytes peak_refseq_ext # reference sequence in peak region with extension on both sides b/w RAs_left and RAs_right - - def __init__ ( self, chrom, peak, RAlist_T, RAlist_C=[] ): - """Create RACollection object by taking: - - 1. peak: a PeakIO object indicating the peak region. - - 2. RAlist: a python list of ReadAlignment objects containing - all the reads overlapping the peak region. If no RAlist_C - given, it will be []. - - """ - if len(RAlist_T) == 0: - # no reads, return None - raise Exception("No reads from ChIP sample to construct RAcollection!") - self.chrom = chrom - self.peak = peak - #print(len(RAlist_T),"\n") - #print(len(RAlist_C),"\n") - self.RAlists = [ RAlist_T, RAlist_C ] - self.left = peak["start"] - self.right = peak["end"] - self.length = self.right - self.left - if RAlist_T: - self.RAs_left = RAlist_T[0]["lpos"] # initial assignment of RAs_left - self.RAs_right = RAlist_T[-1]["rpos"] # initial assignment of RAs_right - self.sort() # it will set self.sorted = True - else: - self.RAs_left = -1 - self.RAs_right = -1 - # check RAs_left and RAs_right - for ra in RAlist_T: - if ra[ "lpos" ] < self.RAs_left: - self.RAs_left = ra[ "lpos" ] - if ra[ "rpos" ] > self.RAs_right: - self.RAs_right = ra[ "rpos" ] - - for ra in RAlist_C: - if ra[ "lpos" ] < self.RAs_left: - self.RAs_left = ra[ "lpos" ] - if ra[ "rpos" ] > self.RAs_right: - self.RAs_right = ra[ "rpos" ] - (self.peak_refseq, self.peak_refseq_ext) = self.__get_peak_REFSEQ() - - def __getitem__ ( self, keyname ): - if keyname == "chrom": - return self.chrom - elif keyname == "left": - return self.left - elif keyname == "right": - return self.right - elif keyname == "RAs_left": - return self.RAs_left - elif keyname == "RAs_right": - return self.RAs_right - elif keyname == "length": - return self.length - elif keyname == "count": - return len( self.RAlists[ 0 ] )+ len( self.RAlists[ 1 ] ) - elif keyname == "count_T": - return len( self.RAlists[ 0 ] ) - elif keyname == "count_C": - return len( self.RAlists[ 1 ] ) - elif keyname == "peak_refseq": - return self.peak_refseq - elif keyname == "peak_refseq_ext": - return self.peak_refseq_ext - else: - raise KeyError("Unavailable key:", keyname) - - def __getstate__ ( self ): - #return {"chrom":self.chrom, "peak":self.peak, "RAlists":self.RAlists, - # "left":self.left, "right":self.right, "length": self.length, - # "RAs_left":self.RAs_left, "RAs_right":self.RAs_right} - return (self.chrom, self.peak, self.RAlists, self.left, self.right, self.length, self.RAs_left, self.RAs_right, self.peak_refseq, self.peak_refseq_ext) - - def __setstate__ ( self, state ): - (self.chrom, self.peak, self.RAlists, self.left, self.right, self.length, self.RAs_left, self.RAs_right, self.peak_refseq, self.peak_refseq_ext) = state - - cpdef sort ( self ): - """Sort RAs according to lpos. Should be used after realignment. - - """ - if self.RAlists[ 0 ]: - self.RAlists[ 0 ].sort(key=itemgetter("lpos")) - if self.RAlists[ 1 ]: - self.RAlists[ 1 ].sort(key=itemgetter("lpos")) - self.sorted = True - return - - cpdef remove_outliers ( self, int percent = 5 ): - """ Remove outliers with too many n_edits. The outliers with - n_edits in top p% will be removed. - - Default: remove top 5% of reads that have too many differences - with reference genome. - """ - cdef: - list n_edits_list - object read # ReadAlignment object - int highest_n_edits - list new_RAlist - int i - - n_edits_list = [] - for ralist in self.RAlists: - for read in ralist: - n_edits_list.append( read["n_edits"] ) - n_edits_list.sort() - highest_n_edits = n_edits_list[ int( len( n_edits_list ) * (1 - percent * .01) ) ] - - for i in ( range(len(self.RAlists)) ): - new_RAlist = [] - for read in self.RAlists[ i ]: - if read["n_edits"] <= highest_n_edits: - new_RAlist.append( read ) - self.RAlists[ i ] = new_RAlist - - return - - cpdef n_edits_sum ( self ): - """ - """ - cdef: - list n_edits_list - object read - int highest_n_edits - - n_edits_list = [] - - for ralist in self.RAlists: - for read in ralist: - n_edits_list.append( read["n_edits"] ) - - n_edits_list.sort() - # print ( n_edits_list ) - c = Counter( n_edits_list ) - #print( c ) - - cdef tuple __get_peak_REFSEQ ( self ): - """Get the reference sequence within the peak region. - - """ - cdef: - bytearray peak_refseq - int i - long prev_r #remember the previous filled right end - long start - long end - long ind, ind_r - object read - bytearray read_refseq_ext - bytearray read_refseq - - start = min( self.RAs_left, self.left ) - end = max( self.RAs_right, self.right ) - #print ("left",start,"right",end) - peak_refseq_ext = bytearray( b'N' * ( end - start ) ) - - # for treatment. - peak_refseq_ext = self.__fill_refseq ( peak_refseq_ext, self.RAlists[0] ) - # and control if available. - if self.RAlists[1]: - peak_refseq_ext = self.__fill_refseq ( peak_refseq_ext, self.RAlists[1] ) - - # trim - peak_refseq = peak_refseq_ext[ self.left - start: self.right - start ] - return ( bytes( peak_refseq ), bytes( peak_refseq_ext ) ) - - cdef bytearray __fill_refseq ( self, bytearray seq, list ralist ): - """Fill refseq sequence of whole peak with refseq sequences of - each read in ralist. - - """ - cdef: - long prev_r # previous right position of last - # filled - long ind, ind_r - long start, end - object read - bytearray read_refseq - - start = min( self.RAs_left, self.left ) - - #print(len(ralist),"\n") - prev_r = ralist[0]["lpos"] - - for i in range( len( ralist ) ): - read = ralist[ i ] - if read[ "lpos" ] > prev_r: - read = ralist[ i - 1 ] - read_refseq = read.get_REFSEQ() - ind = read["lpos"] - start - ind_r = ind + read["rpos"] - read["lpos"] - seq[ ind: ind_r ] = read_refseq - prev_r = read[ "rpos" ] - # last - read = ralist[ -1 ] - read_refseq = read.get_REFSEQ() - ind = read["lpos"] - start - ind_r = ind + read["rpos"] - read["lpos"] - seq[ ind: ind_r ] = read_refseq - return seq - - cpdef get_PosReadsInfo_ref_pos ( self, long ref_pos, bytes ref_nt, int Q=20 ): - """Generate a PosReadsInfo object for a given reference genome - position. - - Return a PosReadsInfo object. - - """ - cdef: - bytearray s - bytearray bq - int strand - object ra - list bq_list_t = [] - list bq_list_c = [] - int i - int pos - bool tip - - posreadsinfo_p = PosReadsInfo( ref_pos, ref_nt ) - - #Treatment group - for i in range( len( self.RAlists[ 0 ] ) ): - ra = self.RAlists[ 0 ][ i ] - if ra[ "lpos" ] <= ref_pos and ra[ "rpos" ] > ref_pos: - ( s, bq, strand, tip, pos) = ra.get_variant_bq_by_ref_pos( ref_pos ) - posreadsinfo_p.add_T( i, bytes( s ), bq[ 0 ], strand, tip, Q=Q ) - - #Control group - for i in range( len( self.RAlists[ 1 ] ) ): - ra = self.RAlists[ 1 ][ i ] - if ra[ "lpos" ] <= ref_pos and ra[ "rpos" ] > ref_pos: - ( s, bq, strand, tip, pos ) = ra.get_variant_bq_by_ref_pos( ref_pos ) - posreadsinfo_p.add_C( i, bytes( s ), bq[ 0 ], strand, Q=Q ) - - return posreadsinfo_p - - cpdef bytearray get_FASTQ ( self ): - """Get FASTQ file for all reads in RACollection. - - """ - cdef: - object ra - bytearray fastq_text - - fastq_text = bytearray(b"") - - for ra in self.RAlists[0]: - fastq_text += ra.get_FASTQ() - - for ra in self.RAlists[1]: - fastq_text += ra.get_FASTQ() - - return fastq_text - - cdef list fermi_assemble( self, int fermiMinOverlap, int opt_flag = 0x80 ): - """A wrapper function to call Fermi unitig building functions. - """ - cdef: - fml_opt_t *opt - int c, n_seqs - int * n_utg - bseq1_t *seqs - fml_utg_t *utg - fml_utg_t p - - int unitig_k, merge_min_len - bytes tmps - bytes tmpq - int ec_k = -1 - int64_t l - char * cseq - char * cqual - int i, j - bytes tmpunitig - bytes unitig #final unitig - list unitig_list # contain list of sequences in bytes format - int * n - - n_seqs = len(self.RAlists[0]) + len(self.RAlists[1]) - - # print n_seqs - - # prepare seq and qual, note, we only extract SEQ according to the + - # strand of reference sequence. - seqs = malloc( n_seqs * sizeof(bseq1_t) ) # we rely on fermi-lite to free this mem - - i = 0 - for ra in self.RAlists[0]: - tmps = ra["SEQ"] - tmpq = ra["QUAL"] - l = len(tmps) - cseq = malloc( (l+1)*sizeof(char))# we rely on fermi-lite to free this mem - cqual = malloc( (l+1)*sizeof(char))# we rely on fermi-lite to free this mem - for j in range(l): - cseq[ j ] = tmps[ j ] - cqual[ j ] = tmpq[ j ] + 33 - cseq[ l ] = b'\x00' - cqual[ l ]= b'\x00' - - seqs[ i ].seq = cseq - seqs[ i ].qual = cqual - seqs[ i ].l_seq = len(tmps) - i += 1 - - # print "@",ra["readname"].decode() - # print cseq.decode() - # print "+" - # print cqual.decode() - - for ra in self.RAlists[1]: - tmps = ra["SEQ"] - tmpq = ra["QUAL"] - l = len(tmps) - cseq = malloc( (l+1)*sizeof(char))# we rely on fermi-lite to free this mem - cqual = malloc( (l+1)*sizeof(char))# we rely on fermi-lite to free this mem - for j in range(l): - cseq[ j ] = tmps[ j ] - cqual[ j ] = tmpq[ j ] + 33 - cseq[ l ] = b'\x00' - cqual[ l ]= b'\x00' - - seqs[ i ].seq = cseq - seqs[ i ].qual = cqual - seqs[ i ].l_seq = len(tmps) - i += 1 - # print "@",ra["readname"].decode() - # print cseq.decode() - # print "+" - # print cqual.decode() - - # if self.RAlists[1]: - # unitig_k=int(min(self.RAlists[0][0]["l"],self.RAlists[1][0]["l"])*fermiOverlapMinRatio) - - # merge_min_len=int(min(self.RAlists[0][0]["l"],self.RAlists[1][0]["l"])*0.5) - # else: - # unitig_k = int(self.RAlists[0][0]["l"]*fermiOverlapMinRatio) - - # merge_min_len=int(self.RAlists[0][0]["l"]*0.5) - #fermiMinOverlap = int(self.RAlists[0][0]["l"]*fermiOverlapMinRatio) - - # minimum overlap to merge, default 0 - # merge_min_len= max( 25, int(self.RAlists[0][0]["l"]*0.5) ) - # merge_min_len= int(self.RAlists[0][0]["l"]*0.5) - - opt = PyMem_Malloc( sizeof(fml_opt_t) ) - n_utg = PyMem_Malloc( sizeof(int) ) - - fml_opt_init(opt) - # k-mer length for error correction (0 for auto; -1 to disable) - #opt.ec_k = 0 - - # min overlap length during initial assembly - opt.min_asm_ovlp = fermiMinOverlap - - # minimum length to merge, during assembly, don't explicitly merge an overlap if shorter than this value - # opt.min_merge_len = merge_min_len - - # there are more 'options' for mag clean: - # flag, min_ovlp, min_elen, min_ensr, min_insr, max_bdist, max_bdiff, max_bvtx, min_merge_len, trim_len, trim_depth, min_dratio1, max_bcov, max_bfrac - # min_elen (300) will be adjusted - # min_ensr (4), min_insr (3) will be computed - # min_merge_len (0) will be updated using opt.min_merge_len - - # We can adjust: flag (0x40|0x80), min_ovlp (0), min_dratio1 (0.7), max_bdiff (50), max_bdist (512), max_bvtx (64), trim_len (0), trim_depth (6), max_bcov (10.), max_bfrac (0.15) - - # 0x20: MAG_F_AGGRESSIVE pop variant bubbles - # 0x40: MAG_F_POPOPEN aggressive tip trimming - # 0x80: MAG_F_NO_SIMPL skip bubble simplification - opt.mag_opt.flag = opt_flag - - # mag_opt.min_ovlp - #opt.mag_opt.min_ovlp = fermiMinOverlap - - # drop an overlap if its length is below maxOvlpLen*FLOAT - #opt.mag_opt.min_dratio1 = 0.5 - - # retain a bubble if one side is longer than the other side by >INT-bp - #opt.mag_opt.max_bdiff = 10#merge_min_len - - # trim_len: - # trim_depth: Parameter used to trim the open end/tip. If trim_len == 0, do nothing - - # max_bdist: - # max_bvtx: Parameter used to simply bubble while 0x80 flag is set. - #opt.mag_opt.max_bdist = 1024 - #opt.mag_opt.max_bvtx = 128 - - # max_bcov: - # max_bfrac: Parameter used when aggressive bubble removal is not used. Bubble will be removed if its average coverage lower than max_bcov and fraction (cov1/(cov1+cov2)) is lower than max_bfrac - #opt.mag_opt.max_bcov = 10. - #opt.mag_opt.max_bfrac = 0.01 - - utg = fml_assemble(opt, n_seqs, seqs, n_utg) - # get results - unitig_list = [] - for i in range( n_utg[0] ): - p = utg[ i ] - if (p.len < 0): - continue - #unitig = b'' - #for j in range( p.len ): - # unitig += [b'A',b'C',b'G',b'T',b'N'][int(p.seq[j]) - 1] - #unitig_list.append( unitig ) - unitig_list.append( p.seq ) - - fml_utg_destroy(n_utg[0], utg) - - PyMem_Free( opt ) - PyMem_Free( n_utg ) - - return unitig_list - - cdef tuple align_unitig_to_REFSEQ ( self, list unitig_list ): - """Note: we use smith waterman, but we don't use linear gap - penalty at this time. - - Also, if unitig is mapped to - strand, we will revcomp the - unitig. So the unitig_list will be changed in this case. - """ - - cdef: - bytes unitig - seq_pair_t problem - align_t * results - char * tmp - bytes target - bytes reference - bytes target_aln_f, target_aln_r - bytes reference_aln_f, reference_aln_r - bytes markup_aln_f, markup_aln_r - double score_f, score_r - list target_alns = [] - list reference_alns = [] - list markup_alns = [] - list aln_scores = [] - int i - - reference = copy(self.peak_refseq_ext+b'\x00') - - for i in range( len(unitig_list) ): - unitig = unitig_list[ i ] - target = copy(unitig + b'\x00') - # we use swalign.c for local alignment (without affine gap - # penalty). Will revise later. - problem.a = target - problem.alen = len( unitig ) - problem.b = reference - problem.blen = len( self.peak_refseq_ext ) - results = smith_waterman( &problem ) - target_aln_f = results.seqs.a - reference_aln_f = results.seqs.b - markup_aln_f = results.markup - score_f = results.score - free( results.seqs.a ) - free( results.seqs.b ) - free( results.markup ) - free( results ) - # end of local alignment - - # try reverse complement - target = copy(unitig[::-1] + b'\x00') - target = target.translate( __DNACOMPLEMENT__ ) - problem.a = target - problem.alen = len( unitig ) - problem.b = reference - problem.blen = len( self.peak_refseq_ext ) - results = smith_waterman( &problem ) - target_aln_r = results.seqs.a - reference_aln_r = results.seqs.b - markup_aln_r = results.markup - score_r = results.score - free( results.seqs.a ) - free( results.seqs.b ) - free( results.markup ) - free( results ) - # end of local alignment - - if score_f > score_r: - target_alns.append( target_aln_f ) - reference_alns.append( reference_aln_f ) - markup_alns.append( markup_aln_f ) - aln_scores.append( score_f ) - else: - target_alns.append( target_aln_r ) - reference_alns.append( reference_aln_r ) - markup_alns.append( markup_aln_r ) - aln_scores.append( score_r ) - # we will revcomp unitig - unitig = unitig[::-1] - unitig_list[ i ] = unitig.translate( __DNACOMPLEMENT__ ) - - return ( target_alns, reference_alns, aln_scores, markup_alns ) - - cdef verify_alns( self, unitig_list, unitig_alns, reference_alns, aln_scores, markup_alns, float min_score_100 = 150 ): - """Remove aln/unitig if it contains too many edits in a small region - - default min score is 150, which means under 2/-3/-5/-2 scoring schema, there are 10 mismatches within 100bps region. - """ - cdef: - int i - for i in range( len( unitig_list )-1, -1, -1 ): - #print i, aln_scores[ i ] - #print unitig_alns[ i ] - #print markup_alns[ i ] - #print reference_alns[ i ] - if aln_scores[ i ] * 100 /len( markup_alns[ i ] ) < min_score_100: - unitig_list.pop( i ) - unitig_alns.pop( i ) - reference_alns.pop( i ) - aln_scores.pop( i ) - markup_alns.pop( i ) - return - - - cdef tuple filter_unitig_with_bad_aln ( self, list unitig_list, list target_alns, list reference_alns, float gratio = 0.25 ): - """Remove unitigs that has too much gaps (both on target and reference) during alignments. - """ - pass - - cdef list remap_RAs_w_unitigs ( self, list unitig_list ): - """Remap RAs to unitigs, requiring perfect match. - - Return RAlists_T, RAlists_C, unmapped_racollection. - """ - cdef: - list RAlists_T = [] # lists of list of RAs of ChIP mapped to each unitig - list RAlists_C = [] - list unmapped_RAlist_T = [] # list of RAs of ChIP unmappable to unitigs - list unmapped_RAlist_C = [] - #RACollection unmapped_ra_collection - int flag = 0 - int i - object tmp_ra - bytes tmp_ra_seq, unitig - - for i in range( len(unitig_list) ): - RAlists_T.append([]) # for each unitig, there is another list of RAs - RAlists_C.append([]) - - # assign RAs to unitigs - - for tmp_ra in self.RAlists[0]: - flag = 0 - tmp_ra_seq = tmp_ra["SEQ"] - for i in range( len(unitig_list) ): - unitig = unitig_list[ i ] - if tmp_ra_seq in unitig: - flag = 1 - RAlists_T[ i ].append( tmp_ra ) - break - if flag == 0: - unmapped_RAlist_T.append( tmp_ra ) - #print "unmapped:", tmp_ra["SEQ"] - - for tmp_ra in self.RAlists[1]: - flag = 0 - tmp_ra_seq = tmp_ra["SEQ"] - for i in range( len(unitig_list) ): - unitig = unitig_list[ i ] - if tmp_ra_seq in unitig: - flag = 1 - RAlists_C[ i ].append( tmp_ra ) - break - if flag == 0: - unmapped_RAlist_C.append( tmp_ra ) - #print "unmapped:", tmp_ra["SEQ"] - - #if unmapped_RAlist_T: - #unmapped_ra_collection = RACollection( self.chrom, self.peak, unmapped_RAlist_T, unmapped_RAlist_C ) - return [ RAlists_T, RAlists_C, unmapped_RAlist_T, unmapped_RAlist_C ] - - cdef list add_to_unitig_list ( self, unitig_list, unitigs_2nd ): - """ - """ - cdef: - int i,j - int flag - bytes u0, u1 - list new_unitig_list - - new_unitig_list = [] - - for i in range( len(unitigs_2nd) ): - flag = 0 # initial value: can't be found in unitig_list - u0 = unitigs_2nd[ i ] - for j in range( len( unitig_list ) ): - u1 = unitig_list[ j ] - if u1.find( u0 ) != -1: - flag = 1 - break - u1 = u1[::-1].translate(__DNACOMPLEMENT__) - if u1.find( u0 ) != -1: - flag = 1 - break - if not flag: - new_unitig_list.append( u0 ) - new_unitig_list.extend( unitig_list ) - return new_unitig_list - - - cpdef object build_unitig_collection ( self, fermiMinOverlap ): - """unitig_list and tuple_alns are in the same order! - - return UnitigCollection object. - - """ - cdef: - long start, end - list unitigs_2nd - bytes u - list target_alns, reference_alns, aln_scores, markup_alns - list target_alns_2nd, reference_alns_2nd, aln_scores_2nd - list RAlists_T = [] # lists of list of RAs of ChIP mapped to each unitig - list RAlists_C = [] - list unmapped_RAlist_T = [] - list unmapped_RAlist_C = [] - bytes tmp_unitig_seq, tmp_reference_seq - bytes tmp_unitig_aln, tmp_reference_aln, - int i, j - long left_padding_ref, right_padding_ref - long left_padding_unitig, right_padding_unitig - list ura_list = [] - RACollection unmapped_ra_collection - int flag = 0 - int n_unmapped, n_unitigs_0, n_unitigs_1 - - # first round of assembly - # print (" First round to assemble unitigs") - unitig_list = self.fermi_assemble( fermiMinOverlap, opt_flag = 0x80 ) - if len(unitig_list) == 0: - return 0 - - n_unitigs_0 = -1 - n_unitigs_1 = len( unitig_list ) - #print " # of Unitigs:", n_unitigs_1 - #print " Map reads to unitigs" - ( unitig_alns, reference_alns, aln_scores, markup_alns) = self.align_unitig_to_REFSEQ( unitig_list ) - - self.verify_alns( unitig_list, unitig_alns, reference_alns, aln_scores, markup_alns ) - if len(unitig_list) == 0: - # if stop here, it raises a flag that the region may contain too many mismapped reads, we return -1 - return -1 - # print (" # of Unitigs:", n_unitigs_1) - - # assign RAs to unitigs - [ RAlists_T, RAlists_C, unmapped_RAlist_T, unmapped_RAlist_C ] = self.remap_RAs_w_unitigs( unitig_list ) - #print unmapped_ra_collection.get_FASTQ().decode() - - n_unmapped = len( unmapped_RAlist_T ) + len( unmapped_RAlist_C ) - - while len( unmapped_RAlist_T ) > 0 and n_unitigs_1 != n_unitigs_0: - # if there are unmapped reads AND we can get more unitigs - # from last round of assembly, do assembly again - - # print (" # of RAs not mapped, will be assembled again:", n_unmapped) - n_unitigs_0 = n_unitigs_1 - # another round of assembly - unmapped_ra_collection = RACollection( self.chrom, self.peak, unmapped_RAlist_T, unmapped_RAlist_C ) - unitigs_2nd = unmapped_ra_collection.fermi_assemble( fermiMinOverlap, opt_flag = 0x80 ) - - if unitigs_2nd: - unitig_list = self.add_to_unitig_list ( unitig_list, unitigs_2nd ) - n_unitigs_1 = len( unitig_list ) - #print " # of Unitigs:", n_unitigs_1 - #print " Map reads to unitigs" - ( unitig_alns, reference_alns, aln_scores, markup_alns ) = self.align_unitig_to_REFSEQ( unitig_list ) - self.verify_alns( unitig_list, unitig_alns, reference_alns, aln_scores, markup_alns ) - [ RAlists_T, RAlists_C, unmapped_RAlist_T, unmapped_RAlist_C ] = self.remap_RAs_w_unitigs( unitig_list ) - n_unmapped = len( unmapped_RAlist_T ) + len( unmapped_RAlist_C ) - #else: - # for r in unmapped_RAlist_T: - # print r.get_FASTQ().decode().lstrip() - - # print (" # of RAs not mapped, will be assembled again with 1/2 of fermiMinOverlap:", n_unmapped) - # another round of assembly - unmapped_ra_collection = RACollection( self.chrom, self.peak, unmapped_RAlist_T, unmapped_RAlist_C ) - unitigs_2nd = unmapped_ra_collection.fermi_assemble( fermiMinOverlap/2, opt_flag = 0x80 ) - - if unitigs_2nd: - unitig_list = self.add_to_unitig_list ( unitig_list, unitigs_2nd ) - n_unitigs_1 = len( unitig_list ) - #print " # of Unitigs:", n_unitigs_1 - #print " Map reads to unitigs" - ( unitig_alns, reference_alns, aln_scores, markup_alns ) = self.align_unitig_to_REFSEQ( unitig_list ) - self.verify_alns( unitig_list, unitig_alns, reference_alns, aln_scores, markup_alns ) - [ RAlists_T, RAlists_C, unmapped_RAlist_T, unmapped_RAlist_C ] = self.remap_RAs_w_unitigs( unitig_list ) - n_unmapped = len( unmapped_RAlist_T ) + len( unmapped_RAlist_C ) - #else: - # for r in unmapped_RAlist_T: - # print r.get_FASTQ().decode().lstrip() - if len(unitig_list) == 0: - raise Exception("Shouldn't reach here") - # print (" # of Unitigs:", n_unitigs_1) - - if len(unitig_list) == 0: - return None - #print (" Final round: # of Unitigs:", len(unitig_list)) - #print (" Final round: # of RAs not mapped:", n_unmapped) - - start = min( self.left, self.RAs_left ) - end = max( self.right, self.RAs_right ) - - # create UnitigCollection - for i in range( len( unitig_list ) ): - #b'---------------------------AAATAATTTTATGTCCTTCAGTACAAAAAGCAGTTTCAACTAAAACCCAGTAACAAGCTAGCAATTCCTTTTAAATGGTGCTACTTCAAGCTGCAGCCAGGTAGCTTTTTATTACAAAAAATCCCACAGGCAGCCACTAGGTGGCAGTAACAGGCTTTTGCCAGCGGCTCCAGTCAGCATGGCTTGACTGTGTGCTGCAGAAACTTCTTAAATCGTCTGTGTTTGGGACTCGTGGGGCCCCACAGGGCTTTACAAGGGCTTTTTAATTTCCAAAAACATAAAACAAAAAAA--------------' - #b'GATATAAATAGGATGTTATGAGTTTTCAAATAATTTTATGTCCTTCAGTACAAAAAGCAGTTTCAACTAAAACCCAGTAACAAGCTAGCAATTCCTTTTAAATGGTGCTACTTCAAGCTGCAGCCAGGTAGCTTTTTATTACAAAAA-TCCCACAGGCAGCCACTAGGTGGCAGTAACAGGCTTTTGCCAGCGGCTCCAGTCAGCATGGCTTGACTGTGTGCTGCAGAAACTTCTTAAATCGTCTGTGTTTGGGACTCGTGGGGCCCCACAGGGCTTTACAAGGGCTTTTTAATTTCCAAAAACATAAAACAAAAAAAAATACAAATGTATT' - tmp_unitig_aln = unitig_alns[ i ] - tmp_reference_aln = reference_alns[ i ] - tmp_unitig_seq = tmp_unitig_aln.replace(b'-',b'') - tmp_reference_seq = tmp_reference_aln.replace(b'-',b'') - - # print tmp_unitig_aln - # print tmp_reference_aln - # print tmp_unitig_seq - # print tmp_reference_aln - - # find the position on self.peak_refseq_ext - left_padding_ref = self.peak_refseq_ext.find( tmp_reference_seq ) # this number of nts should be skipped on refseq_ext from left - right_padding_ref = len(self.peak_refseq_ext) - left_padding_ref - len(tmp_reference_seq) # this number of nts should be skipped on refseq_ext from right - - #now, decide the lpos and rpos on reference of this unitig - #first, trim left padding '-' - left_padding_unitig = len(tmp_unitig_aln) - len(tmp_unitig_aln.lstrip(b'-')) - right_padding_unitig = len(tmp_unitig_aln) - len(tmp_unitig_aln.rstrip(b'-')) - - tmp_lpos = start + left_padding_ref - tmp_rpos = end - right_padding_ref - - for j in range( left_padding_unitig ): - if tmp_reference_aln[ j ] != b'-': - tmp_lpos += 1 - for j in range( 1, right_padding_unitig + 1 ): - if tmp_reference_aln[ -j ] != b'-': - tmp_rpos -= 1 - - tmp_unitig_aln = tmp_unitig_aln[ left_padding_unitig:(len(tmp_unitig_aln)-right_padding_unitig)] - tmp_reference_aln = tmp_reference_aln[ left_padding_unitig:(len(tmp_reference_aln)-right_padding_unitig)] - - ura_list.append( UnitigRAs( self.chrom, tmp_lpos, tmp_rpos, tmp_unitig_aln, tmp_reference_aln, [RAlists_T[i], RAlists_C[i]] ) ) - - return UnitigCollection( self.chrom, self.peak, ura_list ) diff --git a/MACS3/Signal/ReadAlignment.pyx b/MACS3/Signal/ReadAlignment.py similarity index 54% rename from MACS3/Signal/ReadAlignment.pyx rename to MACS3/Signal/ReadAlignment.py index d5376350..b174ba9e 100644 --- a/MACS3/Signal/ReadAlignment.pyx +++ b/MACS3/Signal/ReadAlignment.py @@ -1,6 +1,6 @@ # cython: language_level=3 # cython: profile=True -# Time-stamp: <2021-03-10 16:21:51 Tao Liu> +# Time-stamp: <2024-10-22 15:19:55 Tao Liu> """Module for SAPPER ReadAlignment class @@ -12,18 +12,19 @@ # ------------------------------------ # python modules # ------------------------------------ -from cpython cimport bool - -cdef extern from "stdlib.h": - ctypedef unsigned int size_t - size_t strlen(char *s) - void *malloc(size_t size) - void *calloc(size_t n, size_t size) - void free(void *ptr) - int strcmp(char *a, char *b) - char * strcpy(char *a, char *b) - long atol(char *bytes) - int atoi(char *bytes) +import cython +from cython.cimports.cpython import bool + +# cdef extern from "stdlib.h": +# ctypedef unsigned int size_t +# size_t strlen(char *s) +# void *malloc(size_t size) +# void *calloc(size_t n, size_t size) +# void free(void *ptr) +# int strcmp(char *a, char *b) +# char * strcpy(char *a, char *b) +# long atol(char *bytes) +# int atoi(char *bytes) # ------------------------------------ # constants @@ -53,31 +54,35 @@ # ------------------------------------ # Classes # ------------------------------------ -cdef class ReadAlignment: - cdef: - bytes readname - bytes chrom - int lpos - int rpos - int strand # strand information. 0 means forward strand, 1 means reverse strand. - bytes binaryseq - bytes binaryqual - int l # length of read - tuple cigar # each item contains op_l|op - bytes MD - int n_edits # number of edits; higher the number, - # more differences with reference - bytes SEQ # sequence of read regarding to + strand - bytes QUAL # quality of read regarding to + strand - - def __init__ ( self, - bytes readname, - bytes chrom, int lpos, int rpos, - int strand, - bytes binaryseq, - bytes binaryqual, - tuple cigar, - bytes MD ): + + +@cython.cclass +class ReadAlignment: + readname: bytes + chrom: bytes + lpos: cython.int + rpos: cython.int + # strand information. 0 means forward strand, 1 means reverse strand. + strand: cython.int + binaryseq: bytes + binaryqual: bytes + l: cython.int # length of read + cigar: tuple # each item contains op_l|op + MD: bytes + # number of edits; higher the number, more differences with reference + n_edits: cython.int + SEQ: bytes # sequence of read regarding to + strand + QUAL: bytes # quality of read regarding to + strand + + def __init__(self, + readname: bytes, + chrom: bytes, + lpos: cython.int, rpos: cython.int, + strand: cython.int, + binaryseq: bytes, + binaryqual: bytes, + cigar: tuple, + MD: bytes): self.readname = readname self.chrom = chrom self.lpos = lpos @@ -85,34 +90,36 @@ def __init__ ( self, self.strand = strand self.binaryseq = binaryseq self.binaryqual = binaryqual - self.l = len( binaryqual ) + self.l = len(binaryqual) self.cigar = cigar self.MD = MD self.n_edits = self.get_n_edits() (self.SEQ, self.QUAL) = self.__get_SEQ_QUAL() - cdef int get_n_edits( self ): + @cython.cfunc + def get_n_edits(self) -> cython.int: """The number is from self.cigar and self.MD. """ - cdef: - int n_edits - int i, cigar_op, cigar_op_l - char c - + n_edits: cython.int + i: cython.int + cigar_op: cython.int + cigar_op_l: cython.int + c: cython.char + n_edits = 0 for i in self.cigar: # only count insertion or softclip cigar_op = i & 15 cigar_op_l = i >> 4 - if cigar_op in [ 1, 4 ]: # count Insertion or Softclip + if cigar_op in [1, 4]: # count Insertion or Softclip n_edits += cigar_op_l - + for c in self.MD: - if (c > 64 and c < 91): # either deletion in query or mismatch + if (c > 64 and c < 91): # either deletion in query or mismatch n_edits += 1 return n_edits - def __str__ ( self ): + def __str__(self): c = self.chrom.decode() n = self.readname.decode() if self.strand: @@ -121,7 +128,7 @@ def __str__ ( self ): s = "+" return f"{c}\t{self.lpos}\t{self.rpos}\t{n}\t{self.l}\t{s}" - def __getitem__ ( self, keyname ): + def __getitem__(self, keyname): if keyname == "readname": return self.readname elif keyname == "chrom": @@ -151,142 +158,128 @@ def __getitem__ ( self, keyname ): else: raise KeyError("No such key", keyname) - def __getstate__ ( self ): - return ( self.readname, self.chrom, self.lpos, self.rpos, self.strand, self.binaryseq, self.binaryqual, self.l, self.cigar, self.MD, self.n_edits, self.SEQ, self.QUAL ) - - def __setstate__ ( self, state ): - ( self.readname, self.chrom, self.lpos, self.rpos, self.strand, self.binaryseq, self.binaryqual, self.l, self.cigar, self.MD, self.n_edits, self.SEQ, self.QUAL ) = state - - # cpdef bytearray get_SEQ ( self ): - # """Convert binary seq to ascii seq. - - # Rule: for each byte, 1st base in the highest 4bit; 2nd in the lowest 4bit. "=ACMGRSVTWYHKDBN" -> [0,15] + def __getstate__(self): + return (self.readname, self.chrom, self.lpos, self.rpos, self.strand, + self.binaryseq, self.binaryqual, self.l, self.cigar, + self.MD, self.n_edits, self.SEQ, self.QUAL) - # Note: In BAM, if a sequence is mapped to reverse strand, the - # reverse complement seq is written in SEQ field. So the return - # value of this function will not be the original one if the - # read is mapped to - strand. - # """ - # cdef: - # char c - # bytearray seq + def __setstate__(self, state): + (self.readname, self.chrom, self.lpos, self.rpos, self.strand, + self.binaryseq, self.binaryqual, self.l, self.cigar, self.MD, + self.n_edits, self.SEQ, self.QUAL) = state - # seq = bytearray(b"") - # for c in self.binaryseq: - # # high - # seq.append( __BAMDNACODE__[c >> 4 & 15] ) - # # low - # seq.append( __BAMDNACODE__[c & 15] ) - # if seq[-1] == b"=": - # # trim the last '=' if it exists - # seq = seq[:-1] - # return seq + @cython.cfunc + def __get_SEQ_QUAL(self) -> tuple: + """Get of: tuple (SEQ, QUAL). - cdef tuple __get_SEQ_QUAL ( self ): - """Get tuple of (SEQ, QUAL). - - Rule: for each byte, 1st base in the highest 4bit; 2nd in the lowest 4bit. "=ACMGRSVTWYHKDBN" -> [0,15] + Rule: for each byte, 1st base in the highest 4bit; 2nd in the + lowest 4bit. "=ACMGRSVTWYHKDBN" -> [0,15] Note: In BAM, if a sequence is mapped to reverse strand, the reverse complement seq is written in SEQ field. So the return value of this function will not be the original one if the read is mapped to - strand. If you need to original one, do reversecomp for SEQ and reverse QUAL. + """ - cdef: - int i - char c - bytearray seq - bytearray qual + i: cython.int + c: cython.char + seq: bytearray + qual: bytearray seq = bytearray(b"") qual = bytearray(b"") - for i in range( len(self.binaryseq) ): - c = self.binaryseq[ i ] + for i in range(len(self.binaryseq)): + c = self.binaryseq[i] # high - seq.append( __BAMDNACODE__[c >> 4 & 15] ) + seq.append(__BAMDNACODE__[c >> 4 & 15]) # low - seq.append( __BAMDNACODE__[c & 15] ) + seq.append(__BAMDNACODE__[c & 15]) + + for i in range(len(self.binaryqual)): + # qual is the -10log10 p or phred score. + qual.append(self.binaryqual[i]) - for i in range( len( self.binaryqual ) ): - # qual is the -10log10 p or phred score. - qual.append( self.binaryqual[i] ) - if seq[-1] == b"=": # trim the last '=' if it exists seq = seq[:-1] - assert len( seq ) == len( qual ), Exception("Lengths of seq and qual are not consistent!") + assert len(seq) == len(qual), Exception("Lengths of seq and qual are not consistent!") # Example on how to get original SEQ and QUAL: - #if self.strand: + # if self.strand: # seq.reverse() # #compliment - # seq = seq.translate( __DNACOMPLEMENT__ ) + # seq = seq.translate(__DNACOMPLEMENT__) # qual.reverse() - return ( bytes(seq), bytes(qual) ) + return (bytes(seq), bytes(qual)) - - cpdef bytes get_FASTQ ( self ): + @cython.ccall + def get_FASTQ(self) -> bytes: """Get FASTQ format text. """ - cdef: - bytes seq - bytearray qual + seq: bytes + qual: bytearray seq = self.SEQ qual = bytearray(self.QUAL) - for i in range( len( self.QUAL ) ): - # qual is the -10log10 p or phred score, to make FASTQ, we have to add 33 - qual[ i ] += 33 - + for i in range(len(self.QUAL)): + # qual is the -10log10 p or phred score, to make FASTQ, we + # have to add 33 + qual[i] += 33 + # reverse while necessary if self.strand: seq = self.SEQ[::-1] - #compliment - seq = seq.translate( __DNACOMPLEMENT__ ) + # compliment + seq = seq.translate(__DNACOMPLEMENT__) qual = qual[::-1] else: seq = self.SEQ return b"@" + self.readname + b"\n" + seq + b"\n+\n" + qual + b"\n" - cpdef bytearray get_REFSEQ ( self ): + @cython.ccall + def get_REFSEQ(self) -> bytearray: """Fetch reference sequence, using self.MD and self.cigar """ - cdef: - char c - bytearray seq, refseq - int i, cigar_op, cigar_op_l - bytearray MD_op - int ind - bool flag_del # flag for deletion event in query - - seq = bytearray(self.SEQ) # we start with read seq then make modifications + c: cython.char + seq: bytearray + i: cython.int + cigar_op: cython.int + cigar_op_l: cython.int + MD_op: bytearray + ind: cython.int + flag_del: bool # flag for deletion event in query + + # we start with read seq then make modifications + seq = bytearray(self.SEQ) # 2-step proces - # First step: use CIGAR to edit SEQ to remove S (softclip) and I (insert) + # First step: use CIGAR to edit SEQ to remove S (softclip) and + # I (insert) # __CIGARCODE__ = "MIDNSHP=X" # let ind be the index in SEQ ind = 0 for i in self.cigar: cigar_op = i & 15 cigar_op_l = i >> 4 - if cigar_op in [2, 5, 6]: # do nothing for Deletion (we will - # put sequence back in step 2), - # Hardclip and Padding + if cigar_op in [2, 5, 6]: + # do nothing for Deletion (we will + # put sequence back in step 2), + # Hardclip and Padding pass - elif cigar_op in [0, 7, 8]: # M = X alignment match (match or - # mismatch) - # do nothing and move ind + elif cigar_op in [0, 7, 8]: + # M = X alignment match (match or mismatch) do nothing + # and move ind ind += cigar_op_l - elif cigar_op in [ 1, 4 ]: # Remove for Insertion or Softclip - seq[ ind : ind + cigar_op_l ] = b'' + elif cigar_op in [1, 4]: # Remove for Insertion or Softclip + seq[ind: ind + cigar_op_l] = b'' - # now the seq should be at the same length as rpos-lpos + # now the seq should be at the same length as rpos-lpos # Second step: use MD string to edit SEQ to put back 'deleted # seqs' and modify mismatches @@ -305,12 +298,12 @@ def __setstate__ ( self, state ): # right, a mismatch should only be 1 letter surrounded # by digits. ind += int(MD_op) - seq[ ind ] = c + seq[ind] = c ind += 1 # reset MD_op MD_op = bytearray(b'') elif (c > 64 and c < 91) and flag_del: - seq[ ind:ind ] = [c,] + seq[ind:ind] = [c,] ind += 1 elif c == 94: # means Deletion in query. Now, insert a sequnce into @@ -321,62 +314,74 @@ def __setstate__ ( self, state ): MD_op = bytearray(b'') else: raise Exception("Don't understand this operator in MD: %c" % c) - #print( seq.decode() ) + # print(seq.decode()) return seq - - cpdef get_base_by_ref_pos ( self, long ref_pos ): + + @cython.ccall + def get_base_by_ref_pos(self, ref_pos: cython.long): """Get base by ref position. """ - cdef: - int relative_pos, p + relative_pos: cython.int + p: cython.int + assert self.lpos <= ref_pos and self.rpos > ref_pos, Exception("Given position out of alignment location") relative_pos = ref_pos - self.lpos - p = self.relative_ref_pos_to_relative_query_pos( relative_pos ) + p = self.relative_ref_pos_to_relative_query_pos(relative_pos) if p == -1: # located in a region deleted in query return None else: - return __BAMDNACODE__[ (self.binaryseq[p//2] >> ((1-p%2)*4) ) & 15 ] + return __BAMDNACODE__[(self.binaryseq[p//2] >> ((1-p % 2)*4)) & 15] - cpdef get_bq_by_ref_pos ( self, long ref_pos ): + @cython.ccall + def get_bq_by_ref_pos(self, ref_pos: cython.long): """Get base quality by ref position. Base quality is in Phred scale. Returned value is the raw Phred-scaled base quality. """ - cdef: - int relative_pos, p + relative_pos: cython.int + p: cython.int + assert self.lpos <= ref_pos and self.rpos > ref_pos, Exception("Given position out of alignment location") relative_pos = ref_pos - self.lpos - p = self.relative_ref_pos_to_relative_query_pos( relative_pos ) + p = self.relative_ref_pos_to_relative_query_pos(relative_pos) if p == -1: # located in a region deleted in query return None else: return self.binaryqual[p] - cpdef tuple get_base_bq_by_ref_pos ( self, long ref_pos ): - """Get base and base quality by ref position. Base quality is in Phred scale. + @cython.ccall + def get_base_bq_by_ref_pos(self, ref_pos: cython.long) -> tuple: + """Get base and base quality by ref position. Base quality is + in Phred scale. Returned bq is the raw Phred-scaled base quality. + """ - cdef: - int relative_pos, p + relative_pos: cython.int + p: cython.int + assert self.lpos <= ref_pos and self.rpos > ref_pos, Exception("Given position out of alignment location") relative_pos = ref_pos - self.lpos - p = self.relative_ref_pos_to_relative_query_pos( relative_pos ) + p = self.relative_ref_pos_to_relative_query_pos(relative_pos) if p == -1: # located in a region deleted in query return None else: - return ( __BAMDNACODE__[ (self.binaryseq[p//2] >> ((1-p%2)*4) ) & 15 ], self.binaryqual[p] ) + return (__BAMDNACODE__[(self.binaryseq[p//2] >> ((1-p % 2)*4)) & 15], + self.binaryqual[p]) - cpdef tuple get_variant_bq_by_ref_pos ( self, long ref_pos ): - """Get any variants (different with reference) and base quality by ref position. + @cython.ccall + def get_variant_bq_by_ref_pos(self, + ref_pos: cython.long) -> tuple: + """Get any variants (different with reference) and base + quality by ref position. - variants will be + variants will be 1) =, if identical @@ -390,40 +395,43 @@ def __setstate__ ( self, state ): Base quality is the raw Phred-scaled base quality. """ - cdef: - int i, m, n - int res, p, op, op_l - int pos - bool tip - bytearray refseq - bytes p_refseq, p_seq - bytearray seq_array - bytearray bq_array + i: cython.int + m: cython.int + n: cython.int + res: cython.int + p: cython.int + op: cython.int + op_l: cython.int + pos: cython.int + tip: bool + refseq: bytearray + p_refseq: bytes + seq_array: bytearray + bq_array: bytearray assert self.lpos <= ref_pos and self.rpos > ref_pos, Exception("Given position out of alignment location") res = ref_pos - self.lpos # residue p = 0 - refseq = self.get_REFSEQ() - p_refseq = refseq[ res ] + # p_refseq = refseq[res] # -- CIGAR CODE -- - #OP BAM Description - #M 0 alignment match (can be a sequence match or mismatch) insertion to the reference - #I 1 insertion to the reference - #D 2 deletion from the reference - #N 3 skipped region from the reference - #S 4 soft clipping (clipped sequences present in SEQ) - #H 5 hard clipping (clipped sequences NOT present in SEQ) - #P 6 padding (silent deletion from padded reference) - #= 7 sequence match - #X 8 sequence mismatch - - seq_array = bytearray( b'' ) - bq_array = bytearray( b'' ) - - for m in range( len(self.cigar) ): - i = self.cigar[ m ] + # OP BAM Description + # M 0 alignment match (can be a sequence match or mismatch) insertion to the reference + # I 1 insertion to the reference + # D 2 deletion from the reference + # N 3 skipped region from the reference + # S 4 soft clipping (clipped sequences present in SEQ) + # H 5 hard clipping (clipped sequences NOT present in SEQ) + # P 6 padding (silent deletion from padded reference) + # = 7 sequence match + # X 8 sequence mismatch + + seq_array = bytearray(b'') + bq_array = bytearray(b'') + + for m in range(len(self.cigar)): + i = self.cigar[m] op = i & 15 op_l = i >> 4 if op in [0, 7, 8]: # M = X alignment match (match or mismatch) @@ -432,98 +440,101 @@ def __setstate__ ( self, state ): p += res # find the position, now get the ref pos = p - seq_array.append( __BAMDNACODE__[ (self.binaryseq[ p//2 ] >> ( (1-p%2)*4 ) ) & 15 ] ) - bq_array.append( self.binaryqual[ p ] ) + seq_array.append(__BAMDNACODE__[(self.binaryseq[p//2] >> ((1-p % 2)*4)) & 15]) + bq_array.append(self.binaryqual[p]) break elif res == op_l - 1: p += res pos = p - seq_array.append( __BAMDNACODE__[ (self.binaryseq[ p//2 ] >> ( (1-p%2)*4 ) ) & 15 ] ) - bq_array.append( self.binaryqual[ p ] ) + seq_array.append(__BAMDNACODE__[(self.binaryseq[p//2] >> ((1-p % 2)*4)) & 15]) + bq_array.append(self.binaryqual[p]) # now add any insertion later on # get next cigar - if m + 1 == len( self.cigar ): + if m + 1 == len(self.cigar): break - i = self.cigar[ m + 1 ] + i = self.cigar[m + 1] op = i & 15 op_l = i >> 4 - if op == 1: #insertion - for n in range( op_l ): + if op == 1: # insertion + for n in range(op_l): p += 1 - seq_array.append( __BAMDNACODE__[ (self.binaryseq[ p//2 ] >> ( (1-p%2)*4 ) ) & 15 ] ) - bq_array.append( self.binaryqual[ p ] ) - #print self.SEQ, seq_array + seq_array.append(__BAMDNACODE__[(self.binaryseq[p//2] >> ((1-p % 2)*4)) & 15]) + bq_array.append(self.binaryqual[p]) + # prself: cython.int.SEQ, seq_array break else: # go to the next cigar code p += op_l res -= op_l - elif op in [ 2, 3 ]: # D N + elif op in [2, 3]: # D N if res < op_l: # find the position, however ... - # position located in a region in reference that not exists in query + # position located in a region in reference that + # not exists in query pos = p - seq_array.append( b'*' ) - bq_array.append( 93 ) #assign 93 for deletion + seq_array.append(b'*') + bq_array.append(93) # assign 93 for deletion break else: # go to the next cigar code res -= op_l - elif op == 1 : # Insertion + elif op == 1: # Insertion p += op_l # if res == 0: # no residue left, so return a chunk of inserted sequence # print "shouldn't run this code" # # first, add the insertion point - # seq_array = bytearray( b'~' ) - # bq_array.append( self.binaryqual[ p ] ) + # seq_array = bytearray(b'~') + # bq_array.append(self.binaryqual[p]) # # then add the inserted seq - # for i in range( op_l ): + # for i in range(op_l): # p += 1 - # seq_array.append( __BAMDNACODE__[ (self.binaryseq[ p//2 ] >> ( (1-p%2)*4 ) ) & 15 ] ) - # bq_array.append( self.binaryqual[ p ] ) + # seq_array.append(__BAMDNACODE__[(self.binaryseq[p//2] >> ((1-p%2)*4)) & 15] ) + # bq_array.append(self.binaryqual[p]) # break # else: # p += op_l - elif op == 4 : # Softclip. If it's Softclip, we'd better not return the extra seq + elif op == 4: # Softclip. If it's Softclip, we'd better not return the extra seq p += op_l if pos == 0 or pos == self.l - 1: tip = True else: tip = False - - return ( seq_array, bq_array, self.strand, tip, pos ) + return (seq_array, bq_array, self.strand, tip, pos) # last position ? - #raise Exception("Not expected to see this") + # raise Exception("Not expected to see this") - cdef int relative_ref_pos_to_relative_query_pos ( self, long relative_ref_pos ): + @cython.cfunc + def relative_ref_pos_to_relative_query_pos(self, + relative_ref_pos: cython.long) -> cython.int: """Convert relative pos on ref to pos on query. """ - cdef: - int p, res, op, op_l + p: cython.int + res: cython.int + op: cython.int + op_l: cython.int + p = 0 res = relative_ref_pos - + for i in self.cigar: op = i & 15 op_l = i >> 4 - if op in [0, 7, 8]: # M = X alignment match (match or mismatch) + if op in [0, 7, 8]: + # M = X alignment match (match or mismatch) if res < op_l: p += res return p else: p += op_l res -= op_l - elif op in [ 2, 3 ]: # D N + elif op in [2, 3]: # D N if res < op_l: - # position located in a region in reference that not exists in query + # position located in a region in reference that + # not exists in query return -1 else: res -= op_l - elif op in [ 1, 4 ]: # I + elif op in [1, 4]: # I p += op_l return p - - -### End ### - diff --git a/MACS3/Signal/UnitigRACollection.py b/MACS3/Signal/UnitigRACollection.py new file mode 100644 index 00000000..0c3f31e8 --- /dev/null +++ b/MACS3/Signal/UnitigRACollection.py @@ -0,0 +1,324 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-22 17:14:11 Tao Liu> + +"""Module + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file COPYING included +with the distribution). +""" +# ------------------------------------ +# python modules +# ------------------------------------ +from operator import itemgetter + +from MACS3.Signal.ReadAlignment import ReadAlignment +from MACS3.Signal.PosReadsInfo import PosReadsInfo +from MACS3.IO.PeakIO import PeakIO + +import cython +from cython.cimports.cpython import bool + +# ------------------------------------ +# constants +# ------------------------------------ +__version__ = "Parser $Revision$" +__author__ = "Tao Liu " +__doc__ = "All Parser classes" + +__DNACOMPLEMENT__ = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@TBGDEFCHIJKLMNOPQRSAUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff' # A trans table to convert A to T, C to G, G to C, and T to A. + +__CIGARCODE__ = "MIDNSHP=X" + +# ------------------------------------ +# Misc functions +# ------------------------------------ + +# ------------------------------------ +# Classes +# ------------------------------------ + + +@cython.cclass +class UnitigRAs: + """ + """ + RAlists: list # [RAlists_T, RAlists_C] + seq: bytes + unitig_aln: bytes + reference_aln: bytes + chrom: bytes + lpos: cython.long + rpos: cython.long + unitig_length: cython.long + reference_length: cython.long + aln_length: cython.long + + def __init__(self, + chrom: bytes, + lpos: cython.long, + rpos: cython.long, + unitig_aln: bytes, + reference_aln: bytes, + RAlists: list): + assert len(unitig_aln) == len(reference_aln), Exception("aln on unitig and reference should be the same length!") + self.chrom = chrom + self.lpos = lpos + self.rpos = rpos + self.unitig_aln = unitig_aln + self.reference_aln = reference_aln + self.RAlists = RAlists + # fill in other information + self.seq = self.unitig_aln.replace(b'-', b'') + self.unitig_length = len(self.seq) + self.reference_length = rpos - lpos + self.aln_length = len(unitig_aln) + + def __getitem__(self, keyname): + if keyname == "chrom": + return self.chrom + elif keyname == "lpos": + return self.lpos + elif keyname == "rpos": + return self.rpos + elif keyname == "seq": + return self.seq + elif keyname == "unitig_aln": + return self.unitig_aln + elif keyname == "reference_aln": + return self.reference_aln + elif keyname == "unitig_length": + return self.unitig_length + elif keyname == "reference_length": + return self.reference_length + elif keyname == "aln_length": + return self.aln_length + elif keyname == "count": + return len(self.RAlists[0]) + len(self.RAlists[1]) + else: + raise KeyError("Unavailable key:", keyname) + + def __getstate__(self): + return (self.RAlists, self.seq, self.unitig_aln, self.reference_aln, + self.chrom, self.lpos, self.rpos, self.unitig_length, + self.reference_length, self.aln_length) + + def __setstate__(self, state): + (self.RAlists, self.seq, self.unitig_aln, self.reference_aln, + self.chrom, self.lpos, self.rpos, self.unitig_length, + self.reference_length, self.aln_length) = state + + @cython.ccall + def get_variant_bq_by_ref_pos(self, + ref_pos: cython.long) -> tuple: + """ + return (s, bq_list_t, bq_list_c, strand_list_t, strand_list_c) + """ + i: cython.long + index_aln: cython.long + index_unitig: cython.long + residue: cython.long + ra: ReadAlignment + s: bytes + bq_list_t: list = [] + bq_list_c: list = [] + strand_list_t: list = [] + strand_list_c: list = [] + tip_list_t: list = [] + pos_list_t: list = [] + pos_list_c: list = [] + ra_seq: bytes + ra_pos: cython.long + l_read: cython.int + + # b'TTATTAGAAAAAAT' find = 2 + # b'AAAAATCCCACAGG' + # b'TTTTATTAGAAAAAATCCCACAGGCAGCCACTAGGTGGCAGTAACAGGCTTTTGCCAGCGGCTCCAGTCAGCATGGCTTGACTGTGTGCT' + # b'TTTTATTACAAAAA-TCCCACAGGCAGCCACTAGGTGGCAGTAACAGGCTTTTGCCAGCGGCTCCAGTCAGCATGGCTTGACTGTGTGCT' lpos=100 + # | | | + # genome 108 113 120 + # aln 8 13 21 + # unitig 8 13 21 + # ref 8 13 20 + # read1 6 11 + # read2 3 11 + # find the position + residue = ref_pos - self.lpos + 1 + index_aln = 0 + for i in range(self.aln_length): + if self.reference_aln[i] != 45: # 45 means b'-' + residue -= 1 + if residue == 0: + break + index_aln += 1 + + # index_aln should be the position on aln + s = self.unitig_aln[index_aln:index_aln+1] + # find the index on unitig + index_unitig = len(self.unitig_aln[:index_aln+1].replace(b'-', b'')) + + if s == b'-': # deletion + for ra in self.RAlists[0]: + ra_seq = ra["SEQ"] + l_read = ra["l"] + ra_pos = index_unitig - self.seq.find(ra_seq) - 1 + if ra_pos == 0 or ra_pos == l_read - 1: + tip_list_t.append(True) + else: + tip_list_t.append(False) + bq_list_t.append(93) + strand_list_t.append(ra["strand"]) + pos_list_t.append(ra_pos) + for ra in self.RAlists[1]: + ra_seq = ra["SEQ"] + ra_pos = index_unitig - self.seq.find(ra_seq) - 1 + bq_list_c.append(93) + strand_list_c.append(ra["strand"]) + pos_list_c.append(ra_pos) + return (bytearray(b'*'), bq_list_t, bq_list_c, strand_list_t, + strand_list_c, tip_list_t, pos_list_t, pos_list_c) + + if index_aln < self.aln_length - 1: + for i in range(index_aln + 1, self.aln_length): + if self.reference_aln[i] == 45: # insertion detected, 45 means b'-' + s += self.unitig_aln[i:i+1] # we extend the s string to contain the inserted seq + else: + break + + for ra in self.RAlists[0]: # treatment + ra_seq = ra["SEQ"] + l_read = ra["l"] + ra_pos = index_unitig - self.seq.find(ra_seq) - 1 + if ra_pos < l_read and ra_pos >= 0: + pos_list_t.append(ra_pos) + if ra_pos == 0 or ra_pos == l_read - 1: + tip_list_t.append(True) + else: + tip_list_t.append(False) + bq_list_t.append(ra["binaryqual"][ra_pos]) + strand_list_t.append(ra["strand"]) + + for ra in self.RAlists[1]: # control + ra_seq = ra["SEQ"] + l_read = ra["l"] + ra_pos = index_unitig - self.seq.find(ra_seq) - 1 + if ra_pos < l_read and ra_pos >= 0: + pos_list_c.append(ra_pos) + bq_list_c.append(ra["binaryqual"][ra_pos]) + strand_list_c.append(ra["strand"]) + + return (bytearray(s), bq_list_t, bq_list_c, strand_list_t, + strand_list_c, tip_list_t, pos_list_t, pos_list_c) + + +@cython.cclass +class UnitigCollection: + """A collection of ReadAlignment objects and the corresponding + PeakIO. + + """ + chrom: bytes + peak: PeakIO # A PeakIO object + URAs_list: list + left: cython.long # left position of peak + right: cython.long # right position of peak + length: cython.long # length of peak + URAs_left: cython.long # left position of all RAs in the collection + URAs_right: cython.long # right position of all RAs in the collection + is_sorted: bool # if sorted by lpos + + def __init__(self, + chrom: bytes, + peak: PeakIO, + URAs_list: list = []): + self.chrom = chrom + self.peak = peak + self.URAs_list = URAs_list + self.left = peak["start"] + self.right = peak["end"] + self.length = self.right - self.left + self.URAs_left = URAs_list[0]["lpos"] # initial assignment of RAs_left + self.URAs_right = URAs_list[-1]["rpos"] # initial assignment of RAs_right + self.sort() # it will set self.is_sorted = True + # check RAs_left and RAs_right + for ura in URAs_list: + if ura["lpos"] < self.URAs_left: + self.URAs_left = ura["lpos"] + if ura["rpos"] > self.URAs_right: + self.URAs_right = ura["rpos"] + + def __getitem__(self, keyname): + if keyname == "chrom": + return self.chrom + elif keyname == "left": + return self.left + elif keyname == "right": + return self.right + elif keyname == "URAs_left": + return self.URAs_left + elif keyname == "URAs_right": + return self.URAs_right + elif keyname == "length": + return self.length + elif keyname == "count": + return len(self.URAs_list) + elif keyname == "URAs_list": + return self.URAs_list + else: + raise KeyError("Unavailable key:", keyname) + + def __getstate__(self): + return (self.chrom, self.peak, self.URAs_list, self.left, self.right, + self.length, self.URAs_left, self.URAs_right, self.is_sorted) + + def __setstate__(self, state): + (self.chrom, self.peak, self.URAs_list, self.left, self.right, + self.length, self.URAs_left, self.URAs_right, self.is_sorted) = state + + @cython.ccall + def sort(self): + """Sort RAs according to lpos. Should be used after realignment. + + """ + self.URAs_list.sort(key=itemgetter("lpos")) + self.is_sorted = True + return + + @cython.ccall + def get_PosReadsInfo_ref_pos(self, + ref_pos: cython.long, + ref_nt: bytes, + Q: cython.int = 20): + """Generate a PosReadsInfo for: object a given reference genome + position. + + Return a PosReadsInfo object. + + """ + s: bytearray + bq_list_t: list + bq_list_c: list + strand_list_t: list + strand_list_c: list + tip_list_t: list + pos_list_t: list + pos_list_c: list + ura: object + i: cython.int + posreadsinfo_p: PosReadsInfo + + posreadsinfo_p = PosReadsInfo(ref_pos, ref_nt) + for i in range(len(self.URAs_list)): + ura = self.URAs_list[i] + if ura["lpos"] <= ref_pos and ura["rpos"] > ref_pos: + (s, bq_list_t, bq_list_c, strand_list_t, strand_list_c, + tip_list_t, pos_list_t, pos_list_c) = ura.get_variant_bq_by_ref_pos(ref_pos) + for i in range(len(bq_list_t)): + posreadsinfo_p.add_T(i, bytes(s), bq_list_t[i], + strand_list_t[i], tip_list_t[i], Q=Q) + for i in range(len(bq_list_c)): + posreadsinfo_p.add_C(i, bytes(s), bq_list_c[i], + strand_list_c[i], Q=Q) + + return posreadsinfo_p diff --git a/MACS3/Signal/UnitigRACollection.pyx b/MACS3/Signal/UnitigRACollection.pyx deleted file mode 100644 index 33051ffd..00000000 --- a/MACS3/Signal/UnitigRACollection.pyx +++ /dev/null @@ -1,309 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2022-02-18 11:44:57 Tao Liu> - -"""Module for SAPPER BAMParser class - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file COPYING included -with the distribution). -""" -# ------------------------------------ -# python modules -# ------------------------------------ -from collections import Counter -from operator import itemgetter -from copy import copy - -from MACS3.Signal.ReadAlignment import ReadAlignment -from MACS3.Signal.PosReadsInfo import PosReadsInfo -from MACS3.IO.PeakIO import PeakIO - -from cpython cimport bool - -import numpy as np -cimport numpy as np -from numpy cimport uint32_t, uint64_t, int32_t, int64_t - -cdef extern from "stdlib.h": - ctypedef unsigned int size_t - size_t strlen(char *s) - void *malloc(size_t size) - void *calloc(size_t n, size_t size) - void free(void *ptr) - int strcmp(char *a, char *b) - char * strcpy(char *a, char *b) - long atol(char *bytes) - int atoi(char *bytes) - -# ------------------------------------ -# constants -# ------------------------------------ -__version__ = "Parser $Revision$" -__author__ = "Tao Liu " -__doc__ = "All Parser classes" - -__DNACOMPLEMENT__ = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@TBGDEFCHIJKLMNOPQRSAUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff' # A trans table to convert A to T, C to G, G to C, and T to A. - -__CIGARCODE__ = "MIDNSHP=X" - -# ------------------------------------ -# Misc functions -# ------------------------------------ - -# ------------------------------------ -# Classes -# ------------------------------------ - -cdef class UnitigRAs: - """ - """ - cdef: - list RAlists # [RAlists_T, RAlists_C] - bytes seq - bytes unitig_aln - bytes reference_aln - bytes chrom - long lpos - long rpos - long unitig_length - long reference_length - long aln_length - - def __init__ ( self, bytes chrom, long lpos, long rpos, bytes unitig_aln, bytes reference_aln, list RAlists ): - assert len( unitig_aln )==len( reference_aln ), Exception("aln on unitig and reference should be the same length!") - self.chrom = chrom - self.lpos = lpos - self.rpos = rpos - self.unitig_aln = unitig_aln - self.reference_aln = reference_aln - self.RAlists = RAlists - # fill in other information - self.seq = self.unitig_aln.replace(b'-',b'') - self.unitig_length = len( self.seq ) - self.reference_length = rpos - lpos - self.aln_length = len( unitig_aln ) - - def __getitem__ ( self, keyname ): - if keyname == "chrom": - return self.chrom - elif keyname == "lpos": - return self.lpos - elif keyname == "rpos": - return self.rpos - elif keyname == "seq": - return self.seq - elif keyname == "unitig_aln": - return self.unitig_aln - elif keyname == "reference_aln": - return self.reference_aln - elif keyname == "unitig_length": - return self.unitig_length - elif keyname == "reference_length": - return self.reference_length - elif keyname == "aln_length": - return self.aln_length - elif keyname == "count": - return len( self.RAlists[0] ) + len( self.RAlists[1] ) - else: - raise KeyError("Unavailable key:", keyname) - - def __getstate__ ( self ): - return (self.RAlists, self.seq, self.unitig_aln, self.reference_aln, self.chrom, self.lpos, self.rpos, self.unitig_length, self.reference_length, self.aln_length ) - - def __setstate__ ( self, state ): - (self.RAlists, self.seq, self.unitig_aln, self.reference_aln, self.chrom, self.lpos, self.rpos, self.unitig_length, self.reference_length, self.aln_length ) = state - - - cpdef tuple get_variant_bq_by_ref_pos( self, long ref_pos ): - """ - - return ( s, bq_list_t, bq_list_c, strand_list_t, strand_list_c ) - """ - cdef: - long i - long index_aln - long index_unitig - long residue - object ra - bytes s - list bq_list_t = [] - list bq_list_c = [] - list strand_list_t = [] - list strand_list_c = [] - list tip_list_t = [] - list pos_list_t = [] - list pos_list_c = [] - bytes ra_seq - long ra_pos - int p_seq - int l_read - - # b'TTATTAGAAAAAAT' find = 2 - # b'AAAAATCCCACAGG' - #b'TTTTATTAGAAAAAATCCCACAGGCAGCCACTAGGTGGCAGTAACAGGCTTTTGCCAGCGGCTCCAGTCAGCATGGCTTGACTGTGTGCT' - #b'TTTTATTACAAAAA-TCCCACAGGCAGCCACTAGGTGGCAGTAACAGGCTTTTGCCAGCGGCTCCAGTCAGCATGGCTTGACTGTGTGCT' lpos=100 - # | | | - #genome 108 113 120 - #aln 8 13 21 - #unitig 8 13 21 - #ref 8 13 20 - #read1 6 11 - #read2 3 11 - # find the position - residue = ref_pos - self.lpos + 1 - index_aln = 0 - for i in range( self.aln_length ): - if self.reference_aln[ i ] != 45: # 45 means b'-' - residue -= 1 - if residue == 0: - break - index_aln += 1 - - # index_aln should be the position on aln - s = self.unitig_aln[ index_aln:index_aln+1 ] - # find the index on unitig - index_unitig = len( self.unitig_aln[:index_aln+1].replace(b'-',b'') ) - - if s == b'-': #deletion - for ra in self.RAlists[ 0 ]: - ra_seq = ra["SEQ"] - l_read = ra["l"] - ra_pos = index_unitig - self.seq.find( ra_seq ) - 1 - if ra_pos == 0 or ra_pos == l_read -1: - tip_list_t.append( True ) - else: - tip_list_t.append( False ) - bq_list_t.append( 93 ) - strand_list_t.append( ra["strand"] ) - pos_list_t.append( ra_pos ) - for ra in self.RAlists[ 1 ]: - ra_seq = ra["SEQ"] - ra_pos = index_unitig - self.seq.find( ra_seq ) - 1 - bq_list_c.append( 93 ) - strand_list_c.append( ra["strand"] ) - pos_list_c.append( ra_pos ) - return ( bytearray(b'*'), bq_list_t, bq_list_c, strand_list_t, strand_list_c, tip_list_t, pos_list_t, pos_list_c ) - - if index_aln < self.aln_length - 1: - for i in range( index_aln + 1, self.aln_length ): - if self.reference_aln[ i ] == 45: #insertion detected, 45 means b'-' - s += self.unitig_aln[ i:i+1 ] # we extend the s string to contain the inserted seq - else: - break - - for ra in self.RAlists[0]: #treatment - ra_seq = ra["SEQ"] - l_read = ra["l"] - ra_pos = index_unitig - self.seq.find( ra_seq ) - 1 - if ra_pos < l_read and ra_pos >= 0: - pos_list_t.append( ra_pos ) - if ra_pos == 0 or ra_pos == l_read -1: - tip_list_t.append( True ) - else: - tip_list_t.append( False ) - bq_list_t.append( ra["binaryqual"][ra_pos] ) - strand_list_t.append( ra["strand"] ) - - for ra in self.RAlists[1]: #control - ra_seq = ra["SEQ"] - l_read = ra["l"] - ra_pos = index_unitig - self.seq.find( ra_seq ) - 1 - if ra_pos < l_read and ra_pos >= 0: - pos_list_c.append( ra_pos ) - bq_list_c.append( ra["binaryqual"][ra_pos] ) - strand_list_c.append( ra["strand"] ) - - return (bytearray(s), bq_list_t, bq_list_c, strand_list_t, strand_list_c, tip_list_t, pos_list_t, pos_list_c ) - -cdef class UnitigCollection: - """A collection of ReadAlignment objects and the corresponding - PeakIO. - - """ - cdef: - bytes chrom - object peak # A PeakIO object - list URAs_list - long left # left position of peak - long right # right position of peak - long length # length of peak - long URAs_left # left position of all RAs in the collection - long URAs_right # right position of all RAs in the collection - bool sorted # if sorted by lpos - - def __init__ ( self, chrom, peak, URAs_list=[] ): - self.chrom = chrom - self.peak = peak - self.URAs_list = URAs_list - self.left = peak["start"] - self.right = peak["end"] - self.length = self.right - self.left - self.URAs_left = URAs_list[ 0 ]["lpos"] # initial assignment of RAs_left - self.URAs_right = URAs_list[-1]["rpos"] # initial assignment of RAs_right - self.sort() # it will set self.sorted = True - # check RAs_left and RAs_right - for ura in URAs_list: - if ura[ "lpos" ] < self.URAs_left: - self.URAs_left = ura[ "lpos" ] - if ura[ "rpos" ] > self.URAs_right: - self.URAs_right = ura[ "rpos" ] - - def __getitem__ ( self, keyname ): - if keyname == "chrom": - return self.chrom - elif keyname == "left": - return self.left - elif keyname == "right": - return self.right - elif keyname == "URAs_left": - return self.URAs_left - elif keyname == "URAs_right": - return self.URAs_right - elif keyname == "length": - return self.length - elif keyname == "count": - return len( self.URAs_list ) - elif keyname == "URAs_list": - return self.URAs_list - else: - raise KeyError("Unavailable key:", keyname) - - def __getstate__ ( self ): - return (self.chrom, self.peak, self.URAs_list, self.left, self.right, self.length, self.URAs_left, self.URAs_right, self.sorted) - - def __setstate__ ( self, state ): - (self.chrom, self.peak, self.URAs_list, self.left, self.right, self.length, self.URAs_left, self.URAs_right, self.sorted) = state - - cpdef sort ( self ): - """Sort RAs according to lpos. Should be used after realignment. - - """ - self.URAs_list.sort(key=itemgetter("lpos")) - self.sorted = True - return - - cpdef object get_PosReadsInfo_ref_pos ( self, long ref_pos, bytes ref_nt, int Q=20 ): - """Generate a PosReadsInfo object for a given reference genome - position. - - Return a PosReadsInfo object. - - """ - cdef: - bytearray s, bq - list bq_list_t, bq_list_c, strand_list_t, strand_list_c, tip_list_t, pos_list_t, pos_list_c - object ura - int i - - posreadsinfo_p = PosReadsInfo( ref_pos, ref_nt ) - for i in range( len( self.URAs_list ) ): - ura = self.URAs_list[ i ] - if ura[ "lpos" ] <= ref_pos and ura[ "rpos" ] > ref_pos: - ( s, bq_list_t, bq_list_c, strand_list_t, strand_list_c, tip_list_t, pos_list_t, pos_list_c ) = ura.get_variant_bq_by_ref_pos( ref_pos ) - for i in range( len(bq_list_t) ): - posreadsinfo_p.add_T( i, bytes(s), bq_list_t[i], strand_list_t[i], tip_list_t[i], Q=Q ) - for i in range( len(bq_list_c) ): - posreadsinfo_p.add_C( i, bytes(s), bq_list_c[i], strand_list_c[i], Q=Q ) - - return posreadsinfo_p diff --git a/MACS3/Signal/VariantStat.py b/MACS3/Signal/VariantStat.py new file mode 100644 index 00000000..b154a03d --- /dev/null +++ b/MACS3/Signal/VariantStat.py @@ -0,0 +1,549 @@ +# cython: language_level=3 +# cython: profile=True +# Time-stamp: <2024-10-22 14:47:22 Tao Liu> + +"""Module for SAPPER BAMParser class + +Copyright (c) 2017 Tao Liu + +This code is free software; you can redistribute it and/or modify it +under the terms of the BSD License (see the file COPYING included +with the distribution). + +@status: experimental +@version: $Revision$ +@author: Tao Liu +@contact: tliu4@buffalo.edu +""" + +# ------------------------------------ +# python modules +# ------------------------------------ +import cython + +import cython.cimports.numpy as cnp +from cython.cimports.cpython import bool + +from math import log1p, exp, log + +LN10 = 2.3025850929940458 +LN10_tenth = 0.23025850929940458 + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.ccall +def CalModel_Homo(top1_bq_T: cnp.ndarray(cython.int, ndim=1), + top1_bq_C: cnp.ndarray(cython.int, ndim=1), + top2_bq_T: cnp.ndarray(cython.int, ndim=1), + top2_bq_C: cnp.ndarray(cython.int, ndim=1)) -> tuple: + """Return (lnL, BIC). + + """ + i: cython.int + lnL: cython.double + BIC: cython.double + + lnL = 0 + # Phred score is Phred = -10log_{10} E, where E is the error rate. + # to get the 1-E: 1-E = 1-exp(Phred/-10*M_LN10) = 1-exp(Phred * -LOG10_E_tenth) + for i in range(top1_bq_T.shape[0]): + lnL += log1p(-exp(-top1_bq_T[i]*LN10_tenth)) + for i in range(top1_bq_C.shape[0]): + lnL += log1p(-exp(-top1_bq_C[i]*LN10_tenth)) + + for i in range(top2_bq_T.shape[0]): + lnL += log(exp(-top2_bq_T[i]*LN10_tenth)) + for i in range(top2_bq_C.shape[0]): + lnL += log(exp(-top2_bq_C[i]*LN10_tenth)) + + BIC = -2*lnL # no free variable, no penalty + return (lnL, BIC) + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.ccall +def CalModel_Heter_noAS(top1_bq_T: cnp.ndarray(cython.int, ndim=1), + top1_bq_C: cnp.ndarray(cython.int, ndim=1), + top2_bq_T: cnp.ndarray(cython.int, ndim=1), + top2_bq_C: cnp.ndarray(cython.int, ndim=1)) -> tuple: + """Return (lnL, BIC) + + k_T + k_C + """ + k_T: cython.int + k_C: cython.int + lnL: cython.double + BIC: cython.double + tn_T: cython.int + tn_C: cython.int + # tn: cython.int # total observed NTs + lnL_T: cython.double + lnL_C: cython.double # log likelihood for treatment and control + + lnL = 0 + BIC = 0 + # for k_T + # total oberseved treatment reads from top1 and top2 NTs + tn_T = top1_bq_T.shape[0] + top2_bq_T.shape[0] + + if tn_T == 0: + raise Exception("Total number of treatment reads is 0!") + else: + (lnL_T, k_T) = GreedyMaxFunctionNoAS(top1_bq_T.shape[0], + top2_bq_T.shape[0], + tn_T, + top1_bq_T, + top2_bq_T) + lnL += lnL_T + BIC += -2*lnL_T + + # for k_C + tn_C = top1_bq_C.shape[0] + top2_bq_C.shape[0] + + if tn_C == 0: + pass + else: + (lnL_C, k_C) = GreedyMaxFunctionNoAS(top1_bq_C.shape[0], + top2_bq_C.shape[0], + tn_C, + top1_bq_C, + top2_bq_C) + lnL += lnL_C + BIC += -2*lnL_C + + # tn = tn_C + tn_T + + # we penalize big model depending on the number of reads/samples + if tn_T == 0: + BIC += log(tn_C) + elif tn_C == 0: + BIC += log(tn_T) + else: + BIC += log(tn_T) + log(tn_C) + + return (lnL, BIC) + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.ccall +def CalModel_Heter_AS(top1_bq_T: cnp.ndarray(cython.int, ndim=1), + top1_bq_C: cnp.ndarray(cython.int, ndim=1), + top2_bq_T: cnp.ndarray(cython.int, ndim=1), + top2_bq_C: cnp.ndarray(cython.int, ndim=1), + max_allowed_ar: cython.float = 0.99) -> tuple: + """Return (lnL, BIC) + + kc + ki + AS_alleleratio + """ + k_T: cython.int + k_C: cython.int + lnL: cython.double + BIC: cython.double + tn_T: cython.int + tn_C: cython.int + # tn: cython.int # total observed NTs + lnL_T: cython.double + lnL_C: cython.double # log likelihood for treatment and control + AS_alleleratio: cython.double # allele ratio + + lnL = 0 + BIC = 0 + + # Treatment + tn_T = top1_bq_T.shape[0] + top2_bq_T.shape[0] + + if tn_T == 0: + raise Exception("Total number of treatment reads is 0!") + else: + (lnL_T, k_T, AS_alleleratio) = GreedyMaxFunctionAS(top1_bq_T.shape[0], + top2_bq_T.shape[0], + tn_T, + top1_bq_T, + top2_bq_T, + max_allowed_ar) + # print ">>>",lnL_T, k_T, AS_alleleratio + lnL += lnL_T + BIC += -2*lnL_T + + # control + tn_C = top1_bq_C.shape[0] + top2_bq_C.shape[0] + + if tn_C == 0: + pass + else: + # We assume control will not have allele preference + (lnL_C, k_C) = GreedyMaxFunctionNoAS(top1_bq_C.shape[0], + top2_bq_C.shape[0], + tn_C, + top1_bq_C, + top2_bq_C) + lnL += lnL_C + BIC += -2*lnL_C + + # we penalize big model depending on the number of reads/samples + if tn_T == 0: + BIC += log(tn_C) + elif tn_C == 0: + BIC += 2 * log(tn_T) + else: + BIC += 2 * log(tn_T) + log(tn_C) + + return (lnL, BIC) + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cfunc +def GreedyMaxFunctionAS(m: cython.int, + n: cython.int, + tn: cython.int, + me: cnp.ndarray(cython.int, ndim=1), + ne: cnp.ndarray(cython.int, ndim=1), + max_allowed_ar: cython.float = 0.99) -> tuple: + """Return lnL, k and alleleratio in tuple. + + Note: I only translate Liqing's C++ code into pyx here. Haven't + done any review. + + """ + dnew: cython.double + dold: cython.double + rold: cython.double + rnew: cython.double + kold: cython.int + knew: cython.int + btemp: bool + k0: cython.int + dl: cython.double + dr: cython.double + d0: cython.double + d1l: cython.double + d1r: cython.double + + assert m+n == tn + btemp = False + if tn == 1: # only 1 read; I don't expect this to be run... + dl = calculate_ln(m, n, tn, me, ne, 0, 0) + dr = calculate_ln(m, n, tn, me, ne, 1, 1) + + if dl > dr: + k = 0 + return (dl, 0, 0) + else: + k = 1 + return (dr, 1, 1) + elif m == 0: # no top1 nt + return (calculate_ln(m, n, tn, me, ne, 0, m, max_allowed_ar), + m, + 1-max_allowed_ar) + # k0 = m + 1 + elif m == tn: # all reads are top1 + return (calculate_ln(m, n, tn, me, ne, 1, m, max_allowed_ar), + m, + max_allowed_ar) + else: + k0 = m + + d0 = calculate_ln(m, n, tn, me, ne, float(k0)/tn, k0, max_allowed_ar) + d1l = calculate_ln(m, n, tn, me, ne, float(k0-1)/tn, k0-1, max_allowed_ar) + d1r = calculate_ln(m, n, tn, me, ne, float(k0+1)/tn, k0+1, max_allowed_ar) + + if d0 > d1l-1e-8 and d0 > d1r-1e-8: + k = k0 + ar = float(k0)/tn + return (d0, k, ar) + elif d1l > d0: + dold = d1l + kold = k0-1 + rold = float(k0-1)/tn + while kold > 1: # disable: when kold=1 still run, than knew=0 is the final run + knew = kold - 1 + rnew = float(knew)/tn + + dnew = calculate_ln(m, + n, + tn, + me, + ne, + rnew, + knew, + max_allowed_ar) + + if (dnew-1e-8 < dold): + btemp = True + break + kold = knew + dold = dnew + rold = rnew + + if btemp: # maximum L value is in [1,m-1]; + k = kold + ar = rold + return (dold, k, ar) + else: # L(k=0) is the max for [0,m-1] + k = kold + ar = rold + return (dold, k, ar) + + elif d1r > d0: + dold = d1r + kold = k0 + 1 + rold = float(k0 + 1)/tn + while kold < tn - 1: # //disable: when kold=tn-1 still run, than knew=tn is the final run + knew = kold + 1 + + rnew = float(knew)/tn + + dnew = calculate_ln(m, + n, + tn, + me, + ne, + rnew, + knew, + max_allowed_ar) + + if dnew - 1e-8 < dold: + btemp = True + break + kold = knew + dold = dnew + rold = rnew + + if btemp: # maximum L value is in [m+1,tn-1] + k = kold + ar = rold + return (dold, k, ar) + else: # L(k=tn) is the max for [m+1,tn] + k = kold + ar = rold + return (dold, k, ar) + else: + raise Exception("error in GreedyMaxFunctionAS") + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cfunc +def GreedyMaxFunctionNoAS(m: cython.int, + n: cython.int, + tn: cython.int, + me: cnp.ndarray(cython.int, ndim=1), + ne: cnp.ndarray(cython.int, ndim=1)) -> tuple: + """Return lnL, and k in tuple. + + Note: I only translate Liqing's C++ code into pyx here. Haven't + done any review. + + """ + dnew: cython.double + dold: cython.double + kold: cython.int + knew: cython.int + btemp: bool + k0: cython.int + bg_r: cython.double + dl: cython.double + dr: cython.double + d0: cython.double + d1l: cython.double + d1r: cython.double + + btemp = False + bg_r = 0.5 + + if tn == 1: + dl = calculate_ln(m, n, tn, me, ne, bg_r, 0) + dr = calculate_ln(m, n, tn, me, ne, bg_r, 1) + if dl > dr: + k = 0 + return (dl, 0) + else: + k = 1 + return (dr, 1) + elif m == 0: # no top1 nt + return (calculate_ln(m, n, tn, me, ne, bg_r, m), m) + # k0 = m + 1 + elif m == tn: # all reads are top1 + return (calculate_ln(m, n, tn, me, ne, bg_r, m), m) + # elif m == 0: + # k0 = m + 1 + # elif m == tn: + # k0 = m - 1 + else: + k0 = m + + d0 = calculate_ln(m, n, tn, me, ne, bg_r, k0) + d1l = calculate_ln(m, n, tn, me, ne, bg_r, k0 - 1) + d1r = calculate_ln(m, n, tn, me, ne, bg_r, k0 + 1) + + if d0 > d1l - 1e-8 and d0 > d1r - 1e-8: + k = k0 + return (d0, k) + elif d1l > d0: + dold = d1l + kold = k0 - 1 + while kold >= 1: # //when kold=1 still run, than knew=0 is the final run + knew = kold - 1 + dnew = calculate_ln(m, n, tn, me, ne, bg_r, knew) + if dnew - 1e-8 < dold: + btemp = True + break + kold = knew + dold = dnew + + if btemp: # //maximum L value is in [1,m-1]; + k = kold + return (dold, k) + else: # //L(k=0) is the max for [0,m-1] + k = kold + return (dold, k) + elif d1r > d0: + dold = d1r + kold = k0 + 1 + while kold <= tn - 1: # //when kold=tn-1 still run, than knew=tn is the final run + knew = kold + 1 + dnew = calculate_ln(m, n, tn, me, ne, bg_r, knew) + if dnew - 1e-8 < dold: + btemp = True + break + kold = knew + dold = dnew + + if btemp: # //maximum L value is in [m+1,tn-1] + k = kold + return (dold, k) + else: # //L(k=tn) is the max for [m+1,tn] + k = kold + return (dold, k) + else: + raise Exception("error in GreedyMaxFunctionNoAS") + + +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cfunc +def calculate_ln(m: cython.int, + n: cython.int, + tn: cython.int, + me: cnp.ndarray(cython.int, ndim=1), + ne: cnp.ndarray(cython.int, ndim=1), + r: cython.double, + k: cython.int, + max_allowed_r: cython.float = 0.99): + """Calculate log likelihood given quality of top1 and top2, the + ratio r and the observed k. + + """ + i: cython.int + lnL: cython.double + e: cython.double + + lnL = 0 + + # r is extremely high or + if r > max_allowed_r or r < 1 - max_allowed_r: + lnL += k*log(max_allowed_r) + (tn-k)*log(1 - max_allowed_r) + else: + lnL += k*log(r) + (tn-k)*log(1-r) + + # it's entirely biased toward 1 allele + if k == 0 or k == tn: + pass + elif k <= tn/2: + for i in range(k): + lnL += log(float(tn-i)/(k-i)) + else: + for i in range(tn-k): + lnL += log(float(tn-i)/(tn-k-i)) + + for i in range(m): + e = exp(- me[i] * LN10_tenth) + lnL += log((1-e)*(float(k)/tn) + e*(1-float(k)/tn)) + + for i in range(n): + e = exp(- ne[i] * LN10_tenth) + lnL += log((1-e)*(1-float(k)/tn) + e*(float(k)/tn)) + + return lnL + + +@cython.ccall +def calculate_GQ(lnL1: cython.double, + lnL2: cython.double, + lnL3: cython.double) -> cython.int: + """GQ1 = -10*log_{10}((L2+L3)/(L1+L2+L3)) + """ + L1: cython.double + L2: cython.double + L3: cython.double + s: cython.double + tmp: cython.double + GQ_score: cython.int + + # L1 = exp(lnL1-lnL1) + L1 = 1 + L2 = exp(lnL2-lnL1) + L3 = exp(lnL3-lnL1) + + # if L1 > 1: + # L1 = 1 + + if L2 > 1: + L2 = 1 + if L3 > 1: + L3 = 1 + # if(L1<1e-110) L1=1e-110; + if L2 < 1e-110: + L2 = 1e-110 + if L3 < 1e-110: + L3 = 1e-110 + + s = L1 + L2 + L3 + tmp = (L2 + L3)/s + if tmp > 1e-110: + GQ_score = (int)(-4.34294*log(tmp)) + else: + GQ_score = 255 + + return GQ_score + + +@cython.ccall +def calculate_GQ_heterASsig(lnL1: cython.double, + lnL2: cython.double) -> cython.int: + """ + """ + L1: cython.double + L2: cython.double + s: cython.double + tmp: cython.double + ASsig_score: cython.int + + # L1=exp(2.7182818,lnL1-lnL1) + L1 = 1 + L2 = exp(lnL2 - lnL1) + + # if L1 > 1: + # L1 = 1 + if L2 > 1: + L2 = 1 + # if L1 < 1e-110: + # L1 = 1e-110 + if L2 < 1e-110: + L2 = 1e-110 + + s = L1 + L2 + tmp = L2/s + if tmp > 1e-110: + ASsig_score = (int)(-4.34294*log(tmp)) + else: + ASsig_score = 255 + + return ASsig_score diff --git a/MACS3/Signal/VariantStat.pyx b/MACS3/Signal/VariantStat.pyx deleted file mode 100644 index be2699ac..00000000 --- a/MACS3/Signal/VariantStat.pyx +++ /dev/null @@ -1,461 +0,0 @@ -# cython: language_level=3 -# cython: profile=True -# Time-stamp: <2020-12-04 18:41:28 Tao Liu> - -"""Module for SAPPER BAMParser class - -Copyright (c) 2017 Tao Liu - -This code is free software; you can redistribute it and/or modify it -under the terms of the BSD License (see the file COPYING included -with the distribution). - -@status: experimental -@version: $Revision$ -@author: Tao Liu -@contact: tliu4@buffalo.edu -""" - -# ------------------------------------ -# python modules -# ------------------------------------ -from cpython cimport bool - -cimport cython - -import numpy as np -cimport numpy as np - -ctypedef np.float32_t float32_t -ctypedef np.int32_t int32_t - -#from libc.math cimport log10, log, exp, M_LN10 #,fabs,log1p -#from libc.math cimport M_LN10 -from math import log1p, exp, log - -LN10 = 2.3025850929940458 -LN10_tenth = 0.23025850929940458 - -@cython.boundscheck(False) # turn off bounds-checking for entire function -@cython.wraparound(False) # turn off negative index wrapping for entire function -cpdef tuple CalModel_Homo( np.ndarray[int32_t, ndim=1] top1_bq_T, np.ndarray[int32_t, ndim=1] top1_bq_C, np.ndarray[int32_t, ndim=1] top2_bq_T, np.ndarray[int32_t, ndim=1] top2_bq_C): - """Return (lnL, BIC). - - """ - cdef: - int i - double lnL, BIC - - lnL=0 - # Phred score is Phred = -10log_{10} E, where E is the error rate. - # to get the 1-E: 1-E = 1-exp( Phred/-10*M_LN10 ) = 1-exp( Phred * -LOG10_E_tenth ) - for i in range( top1_bq_T.shape[0] ): - lnL += log1p( -exp(-top1_bq_T[ i ]*LN10_tenth) ) - for i in range( top1_bq_C.shape[0] ): - lnL += log1p( -exp(-top1_bq_C[ i ]*LN10_tenth) ) - - for i in range( top2_bq_T.shape[0] ): - lnL += log( exp(-top2_bq_T[ i ]*LN10_tenth) ) - for i in range( top2_bq_C.shape[0] ): - lnL += log( exp(-top2_bq_C[ i ]*LN10_tenth) ) - - BIC = -2*lnL # no free variable, no penalty - return (lnL, BIC) - -@cython.boundscheck(False) # turn off bounds-checking for entire function -@cython.wraparound(False) # turn off negative index wrapping for entire function -cpdef tuple CalModel_Heter_noAS( np.ndarray[int32_t, ndim=1] top1_bq_T,np.ndarray[int32_t, ndim=1] top1_bq_C,np.ndarray[int32_t, ndim=1] top2_bq_T,np.ndarray[int32_t, ndim=1] top2_bq_C ): - """Return (lnL, BIC) - - k_T - k_C - """ - cdef: - int k_T, k_C - double lnL, BIC - int i - int tn_T, tn_C, tn # total observed NTs - double lnL_T, lnL_C # log likelihood for treatment and control - - lnL = 0 - BIC = 0 - #for k_T - # total oberseved treatment reads from top1 and top2 NTs - tn_T = top1_bq_T.shape[0] + top2_bq_T.shape[0] - - if tn_T == 0: - raise Exception("Total number of treatment reads is 0!") - else: - ( lnL_T, k_T ) = GreedyMaxFunctionNoAS( top1_bq_T.shape[0], top2_bq_T.shape[0], tn_T, top1_bq_T, top2_bq_T ) - lnL += lnL_T - BIC += -2*lnL_T - - #for k_C - tn_C = top1_bq_C.shape[0] + top2_bq_C.shape[0] - - if tn_C == 0: - pass - else: - ( lnL_C, k_C ) = GreedyMaxFunctionNoAS( top1_bq_C.shape[0], top2_bq_C.shape[0], tn_C, top1_bq_C, top2_bq_C ) - lnL += lnL_C - BIC += -2*lnL_C - - tn = tn_C + tn_T - - # we penalize big model depending on the number of reads/samples - if tn_T == 0: - BIC += log( tn_C ) - elif tn_C == 0: - BIC += log( tn_T ) - else: - BIC += log( tn_T ) + log( tn_C ) - - return ( lnL, BIC ) - - -@cython.boundscheck(False) # turn off bounds-checking for entire function -@cython.wraparound(False) # turn off negative index wrapping for entire function -cpdef tuple CalModel_Heter_AS( np.ndarray[int32_t, ndim=1] top1_bq_T, np.ndarray[int32_t, ndim=1] top1_bq_C, np.ndarray[int32_t, ndim=1] top2_bq_T, np.ndarray[int32_t, ndim=1] top2_bq_C, float max_allowed_ar = 0.99 ): - """Return (lnL, BIC) - - kc - ki - AS_alleleratio - """ - cdef: - int k_T, k_C - double lnL, BIC - int i - int tn_T, tn_C, tn # total observed NTs - double lnL_T, lnL_C # log likelihood for treatment and control - double AS_alleleratio # allele ratio - - lnL = 0 - BIC = 0 - - #assert top2_bq_T.shape[0] + top2_bq_C.shape[0] > 0, "Total number of top2 nt should not be zero while using this function: CalModel_Heter_AS!" - - # Treatment - tn_T = top1_bq_T.shape[0] + top2_bq_T.shape[0] - - if tn_T == 0: - raise Exception("Total number of treatment reads is 0!") - else: - ( lnL_T, k_T, AS_alleleratio ) = GreedyMaxFunctionAS( top1_bq_T.shape[0], top2_bq_T.shape[0], tn_T, top1_bq_T, top2_bq_T, max_allowed_ar) - #print ">>>",lnL_T, k_T, AS_alleleratio - lnL += lnL_T - BIC += -2*lnL_T - - # control - tn_C = top1_bq_C.shape[0] + top2_bq_C.shape[0] - - if tn_C == 0: - pass - else: - # We assume control will not have allele preference - ( lnL_C, k_C ) = GreedyMaxFunctionNoAS ( top1_bq_C.shape[0], top2_bq_C.shape[0], tn_C, top1_bq_C, top2_bq_C) - lnL += lnL_C - BIC += -2*lnL_C - - tn = tn_C + tn_T - - # we penalize big model depending on the number of reads/samples - if tn_T == 0: - BIC += log( tn_C ) - elif tn_C == 0: - BIC += 2 * log( tn_T ) - else: - BIC += 2 * log( tn_T ) + log( tn_C ) - - return (lnL, BIC) - - -@cython.boundscheck(False) # turn off bounds-checking for entire function -@cython.wraparound(False) # turn off negative index wrapping for entire function -cdef tuple GreedyMaxFunctionAS( int m, int n, int tn, np.ndarray[int32_t, ndim=1] me, np.ndarray[int32_t, ndim=1] ne, float max_allowed_ar = 0.99 ): - """Return lnL, k and alleleratio in tuple. - - Note: I only translate Liqing's C++ code into pyx here. Haven't done any review. - """ - cdef: - double dnew, dold, rold, rnew - int kold, knew - bool btemp - int k0 - double dl, dr, d0, d1l, d1r - - assert m+n == tn - btemp = False - if tn == 1: # only 1 read; I don't expect this to be run... - dl=calculate_ln(m,n,tn,me,ne,0,0); - dr=calculate_ln(m,n,tn,me,ne,1,1); - - if dl>dr: - k = 0 - return ( dl, 0, 0 ) - else: - k = 1 - return ( dr, 1, 1 ) - elif m == 0: #no top1 nt - return ( calculate_ln( m, n, tn, me, ne, 0, m, max_allowed_ar ), m, 1-max_allowed_ar ) - #k0 = m + 1 - elif m == tn: #all reads are top1 - return ( calculate_ln( m, n, tn, me, ne, 1, m, max_allowed_ar ), m, max_allowed_ar ) - else: - k0 = m - - d0 = calculate_ln( m, n, tn, me, ne, float(k0)/tn, k0, max_allowed_ar ) - d1l = calculate_ln( m, n, tn, me, ne, float(k0-1)/tn, k0-1, max_allowed_ar ) - d1r = calculate_ln( m, n, tn, me, ne, float(k0+1)/tn, k0+1, max_allowed_ar ) - - if d0 > d1l-1e-8 and d0 > d1r-1e-8: - k = k0 - ar = float(k0)/tn - return ( d0, k, ar ) - elif d1l > d0: - dold = d1l - kold = k0-1 - rold = float(k0-1)/tn - while kold > 1: #disable: when kold=1 still run, than knew=0 is the final run - knew = kold - 1 - rnew = float(knew)/tn - - dnew = calculate_ln( m,n,tn,me,ne,rnew,knew, max_allowed_ar ) - - if(dnew-1e-8 < dold) : - btemp=True - break - kold=knew - dold=dnew - rold=rnew - - if btemp: #maximum L value is in [1,m-1]; - k = kold - ar= rold - return ( dold, k, ar ) - else: #L(k=0) is the max for [0,m-1] - k = kold - ar = rold - return ( dold, k, ar ) - - elif d1r > d0: - dold = d1r - kold = k0 + 1 - rold = float(k0 + 1)/tn - while kold < tn - 1: #//disable: when kold=tn-1 still run, than knew=tn is the final run - knew = kold + 1 - - rnew = float(knew)/tn - - dnew = calculate_ln( m,n,tn,me,ne,rnew,knew, max_allowed_ar ) - - if dnew - 1e-8 < dold: - btemp = True - break - kold = knew - dold = dnew - rold = rnew - - if btemp: #maximum L value is in [m+1,tn-1] - k = kold - ar= rold - return ( dold, k, ar ) - else: #L(k=tn) is the max for [m+1,tn] - k = kold - ar = rold - return ( dold, k, ar ) - else: - raise Exception("error in GreedyMaxFunctionAS") - - -@cython.boundscheck(False) # turn off bounds-checking for entire function -@cython.wraparound(False) # turn off negative index wrapping for entire function -cdef tuple GreedyMaxFunctionNoAS (int m, int n, int tn, np.ndarray[int32_t, ndim=1] me, np.ndarray[int32_t, ndim=1] ne ): - """Return lnL, and k in tuple. - - Note: I only translate Liqing's C++ code into pyx here. Haven't done any review. - """ - cdef: - double dnew, dold - int kold, knew - bool btemp - int k0 - double bg_r, dl, dr, d0, d1l, d1r - - btemp = False - bg_r = 0.5 - - if tn == 1: - dl = calculate_ln( m, n, tn, me, ne, bg_r, 0) - dr= calculate_ln( m, n, tn, me, ne, bg_r, 1) - if dl > dr: - k = 0 - return ( dl, 0 ) - else: - k = 1 - return ( dr, 1 ) - elif m == 0: #no top1 nt - return ( calculate_ln( m, n, tn, me, ne, bg_r, m ), m ) - #k0 = m + 1 - elif m == tn: #all reads are top1 - return ( calculate_ln( m, n, tn, me, ne, bg_r, m ), m ) - #elif m == 0: - # k0 = m + 1 - #elif m == tn: - # k0 = m - 1 - else: - k0 = m - - d0 = calculate_ln( m, n, tn, me, ne, bg_r, k0) - d1l = calculate_ln( m, n, tn, me, ne, bg_r, k0 - 1) - d1r = calculate_ln( m, n, tn, me, ne, bg_r, k0 + 1) - - if d0 > d1l - 1e-8 and d0 > d1r - 1e-8: - k = k0 - return ( d0, k ) - elif d1l > d0: - dold = d1l - kold=k0 - 1 - while kold >= 1: #//when kold=1 still run, than knew=0 is the final run - knew = kold - 1 - dnew = calculate_ln( m, n, tn, me, ne, bg_r, knew ) - if dnew - 1e-8 < dold: - btemp = True - break - kold=knew - dold=dnew - - if btemp: #//maximum L value is in [1,m-1]; - k = kold - return ( dold, k ) - else: #//L(k=0) is the max for [0,m-1] - k = kold - return ( dold, k ) - elif d1r > d0: - dold = d1r - kold = k0 + 1 - while kold <= tn - 1: #//when kold=tn-1 still run, than knew=tn is the final run - knew = kold + 1 - dnew = calculate_ln( m, n, tn, me, ne, bg_r, knew ) - if dnew - 1e-8 < dold: - btemp = True - break - kold = knew - dold = dnew - - if btemp: #//maximum L value is in [m+1,tn-1] - k = kold - return ( dold, k ) - else: #//L(k=tn) is the max for [m+1,tn] - k = kold - return ( dold, k ) - else: - raise Exception("error in GreedyMaxFunctionNoAS") - -@cython.boundscheck(False) # turn off bounds-checking for entire function -@cython.wraparound(False) # turn off negative index wrapping for entire function -cdef calculate_ln( int m, int n, int tn, np.ndarray[int32_t, ndim=1] me, np.ndarray[int32_t, ndim=1] ne, double r, int k, float max_allowed_r = 0.99): - """Calculate log likelihood given quality of top1 and top2, the ratio r and the observed k. - - """ - cdef: - int i - double lnL - double e - - lnL = 0 - - if r > max_allowed_r or r < 1 - max_allowed_r: # r is extremely high or - #print "here1" - lnL += k*log( max_allowed_r ) + (tn-k)*log( 1- max_allowed_r) #-10000 - else: - lnL += k*log( r ) + (tn-k)*log(1-r) - - if k == 0 or k == tn: # it's entirely biased toward 1 allele - #print "here2" - pass - #lnL += k*log( max_allowed_r ) #-10000 - #lnL += -10000 - elif k <= tn/2: - for i in range( k ): - lnL += log(float(tn-i)/(k-i)) - else: - for i in range( tn-k ): - lnL += log(float(tn-i)/(tn-k-i)) - - for i in range( m ): - e = exp( - me[ i ] * LN10_tenth ) - lnL += log((1-e)*(float(k)/tn) + e*(1-float(k)/tn)) - - for i in range( n ): - e = exp( - ne[ i ] * LN10_tenth ) - lnL += log((1-e)*(1-float(k)/tn) + e*(float(k)/tn)) - - #print r,k,lnL - return lnL - -cpdef int calculate_GQ ( double lnL1, double lnL2, double lnL3): - """GQ1 = -10*log_{10}((L2+L3)/(L1+L2+L3)) - - - """ - cdef: - double L1, L2, L3, sum, tmp - int GQ_score - - #L1 = exp(lnL1-lnL1) - L1 = 1 - L2 = exp(lnL2-lnL1) - L3 = exp(lnL3-lnL1) - - #if L1 > 1: - # L1 = 1 - - if L2 > 1: - L2 = 1 - if L3 > 1: - L3 = 1 - #if(L1<1e-110) L1=1e-110; - if L2 < 1e-110: - L2=1e-110 - if L3 < 1e-110: - L3 = 1e-110 - - sum = L1 + L2 + L3 - tmp = ( L2 + L3 )/sum - if tmp > 1e-110: - GQ_score = (int)(-4.34294*log(tmp)) - else: - GQ_score = 255 - - return GQ_score; - -cpdef int calculate_GQ_heterASsig( double lnL1, double lnL2): - """ - """ - cdef: - double L1, L2, sum, tmp - int ASsig_score - - #L1=exp(2.7182818,lnL1-lnL1) - L1 = 1 - L2 = exp( lnL2 - lnL1 ) - - #if L1 > 1: - # L1 = 1 - if L2 > 1: - L2 = 1 - #if L1 < 1e-110: - # L1 = 1e-110 - if L2 < 1e-110: - L2 = 1e-110 - - sum = L1 + L2 - tmp = L2/sum - if tmp > 1e-110: - ASsig_score = (int)(-4.34294*log(tmp)) - else: - ASsig_score = 255 - - return ASsig_score - diff --git a/setup.py b/setup.py index 7248fb2b..96a3f543 100644 --- a/setup.py +++ b/setup.py @@ -139,17 +139,14 @@ def main(): include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.VariantStat", - ["MACS3/Signal/VariantStat.pyx"], - libraries=["m"], + ["MACS3/Signal/VariantStat.py"], include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.ReadAlignment", - ["MACS3/Signal/ReadAlignment.pyx"], - libraries=["m"], - include_dirs=numpy_include_dir, + ["MACS3/Signal/ReadAlignment.py"], extra_compile_args=extra_c_args), Extension("MACS3.Signal.RACollection", - ["MACS3/Signal/RACollection.pyx", + ["MACS3/Signal/RACollection.py", "MACS3/fermi-lite/bfc.c", "MACS3/fermi-lite/bseq.c", "MACS3/fermi-lite/bubble.c", @@ -165,24 +162,19 @@ def main(): "MACS3/fermi-lite/unitig.c", "MACS3/Signal/swalign.c"], libraries=["m", "z"], - include_dirs=numpy_include_dir+["./", - "./MACS3/fermi-lite/", - "./MACS3/Signal/"], + include_dirs=["./", + "./MACS3/fermi-lite/", + "./MACS3/Signal/"], extra_compile_args=extra_c_args+extra_c_args_for_fermi), Extension("MACS3.Signal.UnitigRACollection", - ["MACS3/Signal/UnitigRACollection.pyx"], - libraries=["m"], - include_dirs=numpy_include_dir, + ["MACS3/Signal/UnitigRACollection.py"], extra_compile_args=extra_c_args), Extension("MACS3.Signal.PosReadsInfo", - ["MACS3/Signal/PosReadsInfo.pyx"], - libraries=["m"], + ["MACS3/Signal/PosReadsInfo.py"], include_dirs=numpy_include_dir, extra_compile_args=extra_c_args), Extension("MACS3.Signal.PeakVariants", - ["MACS3/Signal/PeakVariants.pyx"], - libraries=["m"], - include_dirs=numpy_include_dir, + ["MACS3/Signal/PeakVariants.py"], extra_compile_args=extra_c_args), Extension("MACS3.IO.Parser", ["MACS3/IO/Parser.py"],