xpore-2.1/0000755000175000017500000000000014130107110012023 5ustar nileshnileshxpore-2.1/README.md0000644000175000017500000000415614130107110013310 0ustar nileshnilesh![alt text](https://github.com/GoekeLab/xpore/blob/master/figures/xpore_textlogo.png "xPore") [![PyPI version](https://badge.fury.io/py/xpore.svg)](https://badge.fury.io/py/xpore) [![Documentation Status](https://readthedocs.org/projects/xpore/badge/?version=latest)](https://xpore.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://pepy.tech/badge/xpore)](https://pepy.tech/project/xpore) --- xPore is a Python package for identification and quantification of differential RNA modifications from direct RNA sequencing. ### Installation xPore requires [Python3](https://www.python.org). To install the latest release with PyPI (recommended), run ```sh $ pip install xpore ``` --- ### Documentation Please refer to our xPore documentation ([https://xpore.readthedocs.io](https://xpore.readthedocs.io)) for additional information, a quick start guide, and details on the data processing and output file format. xPore is described in details in [Pratanwanich et al. *Nat Biotechnol* (2021)](https://doi.org/10.1038/s41587-021-00949-w) --- ### Release History The current release is xPore v2.1. Please refer to the github release history for previous releases: https://github.com/GoekeLab/xpore/releases --- ### Citing xPore If you use xPore in your research, please cite Ploy N. Pratanwanich, et al. Identification of differential RNA modifications from nanopore direct RNA sequencing with xPore. *Nat Biotechnol* (2021), [https://doi.org/10.1038/s41587-021-00949-w](https://doi.org/10.1038/s41587-021-00949-w). --- ### Getting Help We appreciate your feedback and questions! You can report any error or suggestion related to xPore as an [issue on github](https://github.com/GoekeLab/xpore/issues). If you have questions related to the manuscript, data, or any general comment or suggestion please use the [Discussions](https://github.com/GoekeLab/xpore/discussions). Thank you! --- ### Contact xPore is maintained by [Ploy N. Pratanwanich](https://github.com/ploy-np), [Yuk Kei Wan](https://github.com/yuukiiwa) and [Jonathan Goeke](https://github.com/jonathangoeke) from the Genome Institute of Singapore, A\*STAR. xpore-2.1/setup.py0000644000175000017500000000262714130107110013544 0ustar nileshnilesh"""Setup for the xpore package.""" from setuptools import setup,find_packages __pkg_name__ = 'xpore' with open('README.md') as f: README = f.read() setup( author="Ploy N. Pratanwanich", maintainer_email="naruemon.p@chula.ac.th", name=__pkg_name__, license="MIT", description='xpore is a python package for Nanopore data analysis of differential RNA modifications.', version='v2.1', long_description=README, long_description_content_type='text/markdown', url='https://github.com/GoekeLab/xpore', packages=find_packages(), include_package_data=True, install_requires=[ 'numpy>=1.18.0', 'pandas>=0.25.3', 'scipy>=1.4.1', 'PyYAML', 'h5py>=2.10.0', 'pyensembl>=1.8.5', 'ujson>=4.0.1' ], python_requires=">=3.8", entry_points={'console_scripts': ["xpore={}.scripts.xpore:main".format(__pkg_name__)]}, classifiers=[ # Trove classifiers # (https://pypi.python.org/pypi?%3Aaction=list_classifiers) 'Development Status :: 1 - Planning', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Libraries', 'Topic :: Scientific/Engineering :: Bio-Informatics', 'Intended Audience :: Science/Research', ], ) xpore-2.1/.gitignore0000644000175000017500000000347114130107110014020 0ustar nileshnilesh# Large files #tests/ #db/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # DS store .DS_Store xpore-2.1/.gitattributes0000644000175000017500000000012714130107110014716 0ustar nileshnileshdb/** filter=lfs diff=lfs merge=lfs -text tests/** filter=lfs diff=lfs merge=lfs -text xpore-2.1/LICENSE0000644000175000017500000000205214130107110013027 0ustar nileshnileshMIT License Copyright (c) 2020 Göke Lab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. xpore-2.1/MANIFEST.in0000644000175000017500000000004514130107110013560 0ustar nileshnileshinclude xpore/diffmod/model_kmer.csv xpore-2.1/xpore/0000755000175000017500000000000014130107110013160 5ustar nileshnileshxpore-2.1/xpore/utils/0000755000175000017500000000000014130107110014320 5ustar nileshnileshxpore-2.1/xpore/utils/misc.py0000644000175000017500000000253014130107110015625 0ustar nileshnileshimport numpy as np import os def makedirs(main_dir, sub_dirs=None, opt='depth'): if not os.path.exists(main_dir): os.makedirs(main_dir) filepaths = dict() if sub_dirs is not None: if opt == 'depth': path = main_dir for sub_dir in sub_dirs: path = os.path.join(path, sub_dir) filepaths[sub_dir] = path # if not os.path.exists(path): try: # Use try-catch for the case of multiprocessing. os.makedirs(path) except: pass else: # opt == 'breadth' for sub_dir in sub_dirs: path = os.path.join(main_dir, sub_dir) filepaths[sub_dir] = path # if not os.path.exists(path): try: # Use try-catch for the case of multiprocessing. os.makedirs(path) except: pass return filepaths def str_decode(df): str_df = df.select_dtypes([np.object]) str_df = str_df.stack().str.decode('utf-8').unstack() for col in str_df: df[col] = str_df[col] return df def str_encode(df): str_df = df.select_dtypes([np.object]) str_df = str_df.stack().str.encode('utf-8').unstack() for col in str_df: df[col] = str_df[col] return df xpore-2.1/xpore/utils/stats.py0000644000175000017500000000562614130107110016041 0ustar nileshnileshimport scipy.stats from math import fabs, sqrt, log, erf import numpy as np # from ..exts.statistics import NormalDist def z_test(y1, y2, n1, n2): # two-tailed p1 = y1.mean() p2 = y2.mean() n1 = n1.mean().round() n2 = n2.mean().round() se = np.sqrt(p1*(1-p1)/n1 + p2*(1-p2)/n2) z = (p1 - p2) / se return z, scipy.stats.norm.sf(abs(z))*2 def calc_prob_overlapping(means, variances): sigma1, sigma2 = np.sqrt(variances) mu1, mu2 = means ### the bug causing entry had sigma1 == sigma2 and mu1 == mu2 #print("sigma1: ",sigma1,"sigma2: ",sigma2) #print("mu1: ",mu1,"mu2: ",mu2) if sigma1 != sigma2 and mu1 != mu2: return NormalDist(mu=mu1, sigma=sigma1).overlap(NormalDist(mu=mu2, sigma=sigma2)) else: return np.nan,[np.nan,np.nan,np.nan,np.nan] class NormalDist(object): "Modified from the library statistics.py (Authors)(https://github.com/python/cpython/blob/3.8/Lib/statistics.py) which is under GPL-compatible license." def __init__(self, mu=0.0, sigma=1.0): self._mu = mu self._sigma = sigma def cdf(self, x): return 0.5 * (1.0 + erf((x - self._mu) / (self._sigma * sqrt(2.0)))) def overlap(self, another): """ Compute the overlapping coefficient (OVL) between two normal distributions. Measures the agreement between two normal probability distributions. Returns a value between 0.0 and 1.0 giving the overlapping area in the two underlying probability density functions. """ # See: "The overlapping coefficient as a measure of agreement between # probability distributions and point estimation of the overlap of two # normal densities" -- Henry F. Inman and Edwin L. Bradley Jr # http://dx.doi.org/10.1080/03610928908830127 X, Y = self, another if (Y._sigma, Y._mu) < (X._sigma, X._mu): # sort to assure commutativity X, Y = Y, X X_var, Y_var = X.variance, Y.variance dv = Y_var - X_var dm = fabs(Y._mu - X._mu) if not dv: return 1.0 - erf(dm / (2.0 * X._sigma * sqrt(2.0))) a = X._mu * Y_var - Y._mu * X_var b = X._sigma * Y._sigma * sqrt(dm**2.0 + dv * log(Y_var / X_var)) x1 = (a + b) / dv # intersection point 1 x2 = (a - b) / dv # intersection point 2 p_overlap = 1.0 - (fabs(Y.cdf(x1) - X.cdf(x1)) + fabs(Y.cdf(x2) - X.cdf(x2))) list_cdf_at_intersections = [X.cdf(x1),Y.cdf(x1),X.cdf(x2),Y.cdf(x2)] return p_overlap,list_cdf_at_intersections @property def mean(self): "Arithmetic mean of the normal distribution." return self._mu @property def stdev(self): "Standard deviation of the normal distribution." return self._sigma @property def variance(self): "Square of the standard deviation." return self._sigma ** 2.0 xpore-2.1/xpore/utils/__init__.py0000644000175000017500000000000014130107110016417 0ustar nileshnileshxpore-2.1/xpore/scripts/0000755000175000017500000000000014130107110014647 5ustar nileshnileshxpore-2.1/xpore/scripts/postprocessing.py0000644000175000017500000000261714130107110020311 0ustar nileshnileshimport os def run_postprocessing(diffmod_table_path,out_dir): file=open(diffmod_table_path,"r") header=file.readline() entries=file.readlines() outfile_path=os.path.join(out_dir,"majority_direction_kmer_diffmod.table") outfile=open(outfile_path,"w") outfile.write(header) header=header.strip().split(',') kmer_ind,dir_ind=header.index('kmer'),header.index('mod_assignment') dict={} for ln in entries: l=ln.strip().split(",") if l[kmer_ind] not in dict: dict[l[kmer_ind]]={l[dir_ind]:1} else: if l[dir_ind] not in dict[l[kmer_ind]]: dict[l[kmer_ind]][l[dir_ind]]=1 else: dict[l[kmer_ind]][l[dir_ind]]+=1 for k in dict: if len(dict[k]) > 1: ##consider one modification type per k-mer if dict[k]['higher'] <= dict[k]['lower']: ##choose the majority dict[k]['choose']='lower' else: dict[k]['choose']='higher' else: dict[k]['choose']=list(dict[k].keys())[0] for ln in entries: l=ln.strip().split(",") if l[dir_ind] == dict[l[kmer_ind]]['choose']: outfile.write(ln) outfile.close() def postprocessing(args): diffmod_dir = args.diffmod_dir diffmod_table_path = os.path.join(diffmod_dir,"diffmod.table") run_postprocessing(diffmod_table_path,diffmod_dir) xpore-2.1/xpore/scripts/dataprep.py0000644000175000017500000010231114130107110017017 0ustar nileshnileshimport numpy as np import pandas as pd import os,re import multiprocessing import h5py import csv import ujson from operator import itemgetter from collections import defaultdict from io import StringIO from . import helper from ..utils import misc def index(eventalign_result,pos_start,out_paths,locks): eventalign_result = eventalign_result.set_index(['contig','read_index']) pos_end=pos_start with locks['index'], open(out_paths['index'],'a') as f_index: for index in list(dict.fromkeys(eventalign_result.index)): transcript_id,read_index = index pos_end += eventalign_result.loc[index]['line_length'].sum() try: # sometimes read_index is nan f_index.write('%s,%d,%d,%d\n' %(transcript_id,read_index,pos_start,pos_end)) except: pass pos_start = pos_end def parallel_index(eventalign_filepath,chunk_size,out_dir,n_processes,resume): # Create output paths and locks. out_paths,locks = dict(),dict() for out_filetype in ['index']: out_paths[out_filetype] = os.path.join(out_dir,'eventalign.%s' %out_filetype) locks[out_filetype] = multiprocessing.Lock() # read_names_done = [] # if resume and os.path.exists(out_paths['log']): # read_names_done = [line.rstrip('\n') for line in open(out_paths['log'],'r')] # else: # Create empty files. with open(out_paths['index'],'w') as f: f.write('transcript_id,read_index,pos_start,pos_end\n') # header # Create communication queues. task_queue = multiprocessing.JoinableQueue(maxsize=n_processes * 2) # Create and start consumers. consumers = [helper.Consumer(task_queue=task_queue,task_function=index,locks=locks) for i in range(n_processes)] for p in consumers: p.start() ## Load tasks into task_queue. A task is eventalign information of one read. eventalign_file = open(eventalign_filepath,'r') pos_start = len(eventalign_file.readline()) #remove header chunk_split = None index_features = ['contig','read_index','line_length'] for chunk in pd.read_csv(eventalign_filepath, chunksize=chunk_size,sep='\t'): chunk_complete = chunk[chunk['read_index'] != chunk.iloc[-1]['read_index']] chunk_concat = pd.concat([chunk_split,chunk_complete]) chunk_concat_size = len(chunk_concat.index) ## read the file at where it left off because the file is opened once ## lines = [len(eventalign_file.readline()) for i in range(chunk_concat_size)] chunk_concat['line_length'] = np.array(lines) task_queue.put((chunk_concat[index_features],pos_start,out_paths)) pos_start += sum(lines) chunk_split = chunk[chunk['read_index'] == chunk.iloc[-1]['read_index']] ## the loop above leaves off w/o adding the last read_index to eventalign.index chunk_split_size = len(chunk_split.index) lines = [len(eventalign_file.readline()) for i in range(chunk_split_size)] chunk_split['line_length'] = np.array(lines) task_queue.put((chunk_split[index_features],pos_start,out_paths)) # Put the stop task into task_queue. task_queue = helper.end_queue(task_queue,n_processes) # Wait for all of the tasks to finish. task_queue.join() def t2g(gene_id,fasta_dict,annotation_dict,g2t_mapping,df_eventalign_index,readcount_min): tx_ids = [] t2g_dict = {} transcripts = [tx for tx in annotation_dict if tx in g2t_mapping[gene_id]] n_reads = sum([len(df_eventalign_index.loc[tx]) for tx in transcripts]) if n_reads >= readcount_min: for tx in transcripts: tx_seq = fasta_dict[tx][0] tx_contig = annotation_dict[tx]['chr'] if tx_seq is None: continue for exon_num in range(len(annotation_dict[tx]['exon'])): g_interval=annotation_dict[tx]['exon'][exon_num] tx_interval=annotation_dict[tx]['tx_exon'][exon_num] for g_pos in range(g_interval[0],g_interval[1]+1): # Exclude the rims of exons. dis_from_start = g_pos - g_interval[0] if annotation_dict[tx]['strand'] == "+": tx_pos = tx_interval[0] + dis_from_start elif annotation_dict[tx]['strand'] == "-": tx_pos = tx_interval[1] - dis_from_start if (g_interval[0] <= g_pos < g_interval[0]+2) or (g_interval[1]-2 < g_pos <= g_interval[1]): # Todo: To improve the mapping kmer = 'XXXXX' else: kmer = tx_seq[tx_pos-2:tx_pos+3] t2g_dict[(tx,tx_pos)] = (tx_contig,gene_id,g_pos,kmer) # tx.contig is chromosome. tx_ids += [tx] return n_reads, tx_ids, t2g_dict def combine(events_str): f_string = StringIO(events_str) eventalign_result = pd.read_csv(f_string,delimiter='\t',names=['contig','position','reference_kmer','read_index', 'strand','event_index','event_level_mean','event_stdv','event_length','model_kmer', 'model_mean', 'model_stdv', 'standardized_level', 'start_idx', 'end_idx']) f_string.close() cond_successfully_eventaligned = eventalign_result['reference_kmer'] == eventalign_result['model_kmer'] if cond_successfully_eventaligned.sum() != 0: eventalign_result = eventalign_result[cond_successfully_eventaligned] keys = ['read_index','contig','position','reference_kmer'] # for groupby eventalign_result['length'] = pd.to_numeric(eventalign_result['end_idx'])-pd.to_numeric(eventalign_result['start_idx']) eventalign_result['sum_norm_mean'] = pd.to_numeric(eventalign_result['event_level_mean']) * eventalign_result['length'] eventalign_result = eventalign_result.groupby(keys) sum_norm_mean = eventalign_result['sum_norm_mean'].sum() start_idx = eventalign_result['start_idx'].min() end_idx = eventalign_result['end_idx'].max() total_length = eventalign_result['length'].sum() eventalign_result = pd.concat([start_idx,end_idx],axis=1) eventalign_result['norm_mean'] = (sum_norm_mean/total_length).round(1) eventalign_result.reset_index(inplace=True) # eventalign_result['transcript_id'] = [contig.split('.')[0] for contig in eventalign_result['contig']] #### CHANGE MADE #### eventalign_result['transcript_id'] = [contig for contig in eventalign_result['contig']] #eventalign_result['transcript_id'] = eventalign_result['contig'] eventalign_result['transcriptomic_position'] = pd.to_numeric(eventalign_result['position']) + 2 # the middle position of 5-mers. # eventalign_result = misc.str_encode(eventalign_result) # eventalign_result['read_id'] = [read_name]*len(eventalign_result) # features = ['read_id','transcript_id','transcriptomic_position','reference_kmer','norm_mean','start_idx','end_idx'] # features_dtype = np.dtype([('read_id', 'S36'), ('transcript_id', 'S15'), ('transcriptomic_position', '") if len(entries[1].split("|"))>1: separate_by_pipe=True dict={} for entry in entries: entry=entry.split("\n") if len(entry[0].split()) > 0: id=entry[0].split('.')[0] seq="".join(entry[1:]) dict[id]=[seq] if is_gff > 0: if separate_by_pipe == True: g_id=info[1],split(".")[0] else: g_id=entry[0].split("gene:")[1].split(".")[0] dict[id].append(g_id) return dict def readAnnotation(gtf_or_gff): gtf=open(gtf_or_gff,"r") dict,is_gff={},0 for ln in gtf: if not ln.startswith("#"): ln=ln.strip("\n").split("\t") if is_gff == 0: if ln[-1].startswith("ID") or ln[-1].startswith("Parent"): is_gff = 1 else: is_gff = -1 if is_gff < 0: if ln[2] == "transcript" or ln[2] == "exon": chr,type,start,end=ln[0],ln[2],int(ln[3]),int(ln[4]) attrList=ln[-1].split(";") attrDict={} for k in attrList: p=k.strip().split(" ") if len(p) == 2: attrDict[p[0]]=p[1].strip('\"') ##tx_id=ln[-1].split('; transcript_id "')[1].split('";')[0] ##g_id=ln[-1].split('gene_id "')[1].split('";')[0] tx_id = attrDict["transcript_id"] g_id = attrDict["gene_id"] if tx_id not in dict: dict[tx_id]={'chr':chr,'g_id':g_id,'strand':ln[6]} if type not in dict[tx_id]: if type == "transcript": dict[tx_id][type]=(start,end) else: if type == "exon": if type not in dict[tx_id]: dict[tx_id][type]=[(start,end)] else: dict[tx_id][type].append((start,end)) if is_gff > 0: if ln[2] == "exon" or ln[2] == "mRNA": chr,type,start,end=ln[0],ln[2],int(ln[3]),int(ln[4]) tx_id=ln[-1].split('transcript:')[1].split(';')[0] if ln[2] == "mRNA": type="transcript" if tx_id not in dict: dict[tx_id]={'chr':chr,'strand':ln[6]} if type == "transcript": dict[tx_id][type]=(start,end) if type == "exon": dict[tx_id][type]=[(start,end)] else: if type == "transcript" and type not in dict[tx_id]: dict[tx_id][type]=(start,end) if type == "exon": if type not in dict[tx_id]: dict[tx_id][type]=[(start,end)] else: dict[tx_id][type].append((start,end)) #convert genomic positions to tx positions if is_gff < 0: for id in dict: tx_pos,tx_start=[],0 for pair in dict[id]["exon"]: tx_end=pair[1]-pair[0]+tx_start tx_pos.append((tx_start,tx_end)) tx_start=tx_end+1 dict[id]['tx_exon']=tx_pos else: for id in dict: tx_pos,tx_start=[],0 if dict[id]["strand"] == "-": dict[id]["exon"].sort(key=lambda tup: tup[0], reverse=True) for pair in dict[id]["exon"]: tx_end=pair[1]-pair[0]+tx_start tx_pos.append((tx_start,tx_end)) tx_start=tx_end+1 dict[id]['tx_exon']=tx_pos return (dict,is_gff) def parallel_preprocess_gene(eventalign_filepath,fasta_dict,annotation_dict,is_gff,out_dir,n_processes,readcount_min,readcount_max,resume): # Create output paths and locks. out_paths,locks = dict(),dict() for out_filetype in ['json','index','log','readcount']: out_paths[out_filetype] = os.path.join(out_dir,'data.%s' %out_filetype) locks[out_filetype] = multiprocessing.Lock() # Writing the starting of the files. gene_ids_done = [] if resume and os.path.exists(out_paths['index']): df_index = pd.read_csv(out_paths['index'],sep=',') gene_ids_done = list(df_index['idx'].unique()) else: # with open(out_paths['json'],'w') as f: # f.write('{\n') # f.write('"genes":{') open(out_paths['json'],'w').close() with open(out_paths['index'],'w') as f: f.write('idx,start,end\n') # header with open(out_paths['readcount'],'w') as f: f.write('idx,n_reads\n') # header open(out_paths['log'],'w').close() # Create communication queues. task_queue = multiprocessing.JoinableQueue(maxsize=n_processes * 2) # Create and start consumers. consumers = [helper.Consumer(task_queue=task_queue,task_function=preprocess_gene,locks=locks) for i in range(n_processes)] for p in consumers: p.start() # Get all gene ids and create a dict of eventalign.combine index. # gene_ids = set() if is_gff > 0: ##add g_id from fasta dict entry if gff annotation is used for tx_id in annotation_dict: try: annotation_dict[tx_id]['g_id']=fasta_dict[tx_id][1] except KeyError: continue df_eventalign_index = pd.read_csv(os.path.join(out_dir,'eventalign.index')) df_eventalign_index['transcript_id'] = [tx_id.split('.')[0] for tx_id in df_eventalign_index['transcript_id']] # df_eventalign_index['transcript_id'] = [tx_id for tx_id in df_eventalign_index['transcript_id']] df_eventalign_index.set_index('transcript_id',inplace=True) g2t_mapping = defaultdict(list) for tx_id in set(df_eventalign_index.index): try: ## g_id = ensembl.transcript_by_id(tx_id).gene_id g_id = annotation_dict[tx_id]['g_id'] except KeyError: continue else: # gene_ids = gene_ids.union([g_id]) g2t_mapping[g_id] += [tx_id] # f = open(os.path.join(out_dir,'eventalign.index')) # for ln in f: # tx_id,read_index,pos_start,pos_end = ln.split(',') # tx_id,tx_version = tx_id.split('.') # Based on Ensembl # eventalign_index[tx_id] += [(int(read_index),int(pos_start),int(pos_end))] # tx_ensembl[tx_id] = tx_version # try: # g_id = ensembl.transcript_by_id(tx_id).gene_id # except ValueError: # continue # else: # gene_ids = gene_ids.union([g_id]) # Load tasks into task_queue. gene_ids_processed = [] with open(eventalign_filepath,'r') as eventalign_result: for gene_id in g2t_mapping: if resume and (gene_id in gene_ids_done): continue # mapping a gene <-> transcripts n_reads, tx_ids, t2g_mapping = t2g(gene_id,fasta_dict,annotation_dict,g2t_mapping,df_eventalign_index,readcount_min) # if n_reads >= readcount_min: data_dict = dict() readcount = 0 for tx_id in tx_ids: for _,row in df_eventalign_index.loc[[tx_id]].iterrows(): read_index,pos_start,pos_end = row['read_index'],row['pos_start'],row['pos_end'] eventalign_result.seek(pos_start,0) events_str = eventalign_result.read(pos_end-pos_start) data = combine(events_str) #data = np.genfromtxt(f_string,delimiter=',',dtype=np.dtype([('transcript_id', 'S15'), ('transcriptomic_position', ' 1): data_dict[read_index] = data readcount += 1 if readcount > readcount_max: break if readcount > readcount_max: break if len(data_dict)>=readcount_min: # print(gene_id,len(data_dict)) #len(data_dict) is the number of reads to be processed. task_queue.put((gene_id,data_dict,t2g_mapping,out_paths)) # Blocked if necessary until a free slot is available. gene_ids_processed += [gene_id] # Put the stop task into task_queue. task_queue = helper.end_queue(task_queue,n_processes) # Wait for all of the tasks to finish. task_queue.join() with open(out_paths['log'],'a+') as f: f.write('Total %d genes.\n' %len(gene_ids_processed)) f.write(helper.decor_message('successfully finished')) def preprocess_gene(gene_id,data_dict,t2g_mapping,out_paths,locks): """ Convert transcriptomic to genomic coordinates for a gene. Parameters ---------- gene_id: str Gene ID. data_dict: {tx_id:events_array} Events for each read. t2g_mapping: {(,):()} A dict to map transcriptomic coordinates (transcript id and transcriptomic position) to genomic (gene id and genomic position). db_type: Type of gene-tx mapping either EnsemblRelease or (customised) Genome features: [str] # todo A list of features to collect from the reads that are aligned to each genomic coordinate in the output. Returns ------- dict A dict of all specified features collected for each genomic coordinate. """ # features = ['read_id','transcript_id','transcriptomic_position','reference_kmer','norm_mean','start_idx','end_idx'] # columns in the eventalign file per read. events = [] condition_labels = [] run_labels = [] read_ids = [] genomic_coordinates = [] # Concatenate # if len(data_dict) == 0: # return for read_index,events_per_read in data_dict.items(): # if len(events_per_read) > 0: # ===== transcript to gene coordinates ===== # TODO: to use gtf. # tx_ids = [tx_id.decode('UTF-8').split('.')[0] for tx_id in events_per_read['transcript_id']] tx_ids = [tx_id.split('.')[0] for tx_id in events_per_read['transcript_id']] tx_positions = events_per_read['transcriptomic_position'] genomic_coordinate = list(itemgetter(*zip(tx_ids,tx_positions))(t2g_mapping)) # genomic_coordinates -- np structured array of 'chr','gene_id','genomic_position','kmer' genomic_coordinate = np.array(genomic_coordinate,dtype=np.dtype([('chr','> don't want it. n_reads = len(data_dict) f.write('%s,%d\n' %(gene_id,n_reads)) with locks['log'], open(out_paths['log'],'a') as f: f.write(log_str + '\n') def parallel_preprocess_tx(eventalign_filepath,out_dir,n_processes,readcount_min,readcount_max,resume): # Create output paths and locks. out_paths,locks = dict(),dict() for out_filetype in ['json','index','log','readcount']: out_paths[out_filetype] = os.path.join(out_dir,'data.%s' %out_filetype) locks[out_filetype] = multiprocessing.Lock() # Writing the starting of the files. tx_ids_done = [] if resume and os.path.exists(out_paths['index']): df_index = pd.read_csv(out_paths['index'],sep=',') tx_ids_done = list(df_index['transcript_id'].unique()) else: open(out_paths['json'],'w').close() with open(out_paths['index'],'w') as f: f.write('idx,start,end\n') # header with open(out_paths['readcount'],'w') as f: f.write('idx,n_reads\n') # header open(out_paths['log'],'w').close() # Create communication queues. task_queue = multiprocessing.JoinableQueue(maxsize=n_processes * 2) # Create and start consumers. consumers = [helper.Consumer(task_queue=task_queue,task_function=preprocess_tx,locks=locks) for i in range(n_processes)] for p in consumers: p.start() # Load tasks into task_queue. tx_ids_processed = [] df_eventalign_index = pd.read_csv(os.path.join(out_dir,'eventalign.index')) # df_eventalign_index['transcript_id'] = [tx_id.split('.')[0] for tx_id in df_eventalign_index['transcript_id']] # df_eventalign_index['transcript_id'] = [tx_id for tx_id in df_eventalign_index['transcript_id']] tx_ids = df_eventalign_index['transcript_id'].values.tolist() tx_ids = list(dict.fromkeys(tx_ids)) df_eventalign_index.set_index('transcript_id',inplace=True) with open(eventalign_filepath,'r') as eventalign_result: for tx_id in tx_ids: data_dict = dict() readcount = 0 for _,row in df_eventalign_index.loc[[tx_id]].iterrows(): read_index,pos_start,pos_end = row['read_index'],row['pos_start'],row['pos_end'] eventalign_result.seek(pos_start,0) events_str = eventalign_result.read(pos_end-pos_start) data = combine(events_str) if (data is not None) and (data.size > 1): data_dict[read_index] = data readcount += 1 if readcount > readcount_max: break if readcount>=readcount_min: task_queue.put((tx_id,data_dict,out_paths)) # Blocked if necessary until a free slot is available. tx_ids_processed += [tx_id] # Put the stop task into task_queue. task_queue = helper.end_queue(task_queue,n_processes) # Wait for all of the tasks to finish. task_queue.join() with open(out_paths['log'],'a+') as f: f.write('Total %d transcripts.\n' %len(tx_ids_processed)) f.write(helper.decor_message('successfully finished')) def preprocess_tx(tx_id,data_dict,out_paths,locks): """ Convert transcriptomic to genomic coordinates for a gene. Parameters ---------- tx_id: str Transcript ID. data_dict: {read_id:events_array} Events for each read. features: [str] # todo A list of features to collect from the reads that are aligned to each genomic coordinate in the output. Returns ------- dict A dict of all specified features collected for each genomic coordinate. """ # features = ['read_id','transcript_id','transcriptomic_position','reference_kmer','norm_mean','start_idx','end_idx'] # columns in the eventalign file per read. events = [] condition_labels = [] run_labels = [] read_ids = [] transcriptomic_coordinates = [] # Concatenate if len(data_dict) == 0: return for read_id,events_per_read in data_dict.items(): # print(read_id) events += [events_per_read] events = np.concatenate(events) # Sort and split idx_sorted = np.argsort(events['transcriptomic_position']) unique_positions, index = np.unique(events['transcriptomic_position'][idx_sorted],return_index = True) y_arrays = np.split(events['norm_mean'][idx_sorted], index[1:]) # read_id_arrays = np.split(events['read_id'][idx_sorted], index[1:]) reference_kmer_arrays = np.split(events['reference_kmer'][idx_sorted], index[1:]) # Prepare # print('Reformating the data for each genomic position ...') data = defaultdict(dict) # for each position, make it ready for json dump asserted = True # for key_tuple,y_array,reference_kmer_array in zip(key_tuples,y_arrays,reference_kmer_arrays): for position,y_array,reference_kmer_array in zip(unique_positions,y_arrays,reference_kmer_arrays): position = int(position) if (len(set(reference_kmer_array)) == 1) and ('XXXXX' in set(reference_kmer_array)) or (len(y_array) == 0): continue if 'XXXXX' in set(reference_kmer_array): y_array = y_array[reference_kmer_array != 'XXXXX'] assert len(y_array) == len(reference_kmer_array) - (reference_kmer_array=='XXXXX').sum() reference_kmer_array = reference_kmer_array[reference_kmer_array != 'XXXXX'] try: assert len(set(reference_kmer_array)) == 1 assert list(set(reference_kmer_array))[0].count('N') == 0 ##to weed out the mapped kmers from tx_seq that contain 'N', which is not in diffmod's model_kmer except: asserted = False break kmer = set(reference_kmer_array).pop() data[position] = {kmer: list(np.around(y_array,decimals=2))} # write to file. log_str = '%s: %s.' %(tx_id,asserted) with locks['json'], open(out_paths['json'],'a') as f: pos_start = f.tell() f.write('{') f.write('"%s":' %tx_id) ujson.dump(data, f) f.write('}\n') pos_end = f.tell() with locks['index'], open(out_paths['index'],'a') as f: f.write('%s,%d,%d\n' %(tx_id,pos_start,pos_end)) with locks['readcount'], open(out_paths['readcount'],'a') as f: #todo: repeats no. of tx >> don't want it. n_reads = len(data_dict) f.write('%s,%d\n' %(tx_id,n_reads)) with locks['log'], open(out_paths['log'],'a') as f: f.write(log_str + '\n') # def index_nanopolish(eventalign_filepath,summary_filepath,out_dir,n_processes): # with helper.EventalignFile(eventalign_filepath) as eventalign_file, open(summary_filepath,'r') as summary_file: # reader_summary = csv.DictReader(summary_file, delimiter="\t") # reader_eventalign = csv.DictReader(eventalign_file, delimiter="\t") # row_summary = next(reader_summary) # read_name = row_summary['read_name'] # read_index = row_summary['read_index'] # eventalign_per_read = [] # for row_eventalign in reader_eventalign: # if (row_eventalign['read_index'] == read_index): # eventalign_per_read += [row_eventalign] # else: # # Load a read info to the task queue. # if read_name not in read_names_done: # task_queue.put((read_name,eventalign_per_read,out_paths)) # # Next read. # try: # row_summary = next(reader_summary) # except StopIteration: # no more read. # break # else: # read_index = row_summary['read_index'] # read_name = row_summary['read_name'] # assert row_eventalign['read_index'] == read_index # eventalign_per_read = [row_eventalign] #def check_gene_tx_id_version(gtf_or_gff): # gtf=open(gtf_or_gff,"r") # extra_version_fields=0 # for i in range(25): # ln=gtf.readline().split('\t') # if not ln[0].startswith('#'): # if ln[2] == "transcript" or ln[2] == "exon": # check_transcript_version = len(ln[-1].split('transcript_version')) == 2 # check_gene_version = len(ln[-1].split('gene_version')) == 2 # if check_transcript_version and check_gene_version: # extra_version_fields+=1 # if extra_version_fields > 0: # return True # else: # return False #def mergeGTFtxIDversion(gtf_or_gff,out_dir): # gtf=open(gtf_or_gff,"r") # new_gtf_path=os.path.join(out_dir,'transcript_id_version_merged.gtf') # new_gtf=open(new_gtf_path,"w") # for ln in gtf: # if not ln.startswith("#"): # ln=ln.split("\t") # if ln[2] == "transcript" or ln[2] == "exon": # g_id=ln[-1].split('gene_id "')[1].split('";')[0] # g_ver=ln[-1].split('; gene_version "')[1].split('";')[0] # tx_id=ln[-1].split('; transcript_id "')[1].split('";')[0] # tx_ver=ln[-1].split('; transcript_version "')[1].split('";')[0] # new_gtf.write('\t'.join(ln[:-1])+'\t'+'gene_id "'+g_id+'.'+g_ver+'"; transcript_id "'+tx_id+'.'+tx_ver+'";'+'\n') # new_gtf.close() # return new_gtf_path def dataprep(args): # n_processes = args.n_processes eventalign_filepath = args.eventalign chunk_size = args.chunk_size out_dir = args.out_dir readcount_min = args.readcount_min readcount_max = args.readcount_max resume = args.resume genome = args.genome if genome and (None in [args.gtf_or_gff,args.transcript_fasta]): print('please provide the following') print('- gtf_or_gff') print('- transcript_fasta') else: gtf_or_gff = args.gtf_or_gff transcript_fasta = args.transcript_fasta misc.makedirs(out_dir) #todo: check every level. # (1) For each read, combine multiple events aligned to the same positions, the results from nanopolish eventalign, into a single event per position. if not args.skip_eventalign_indexing: parallel_index(eventalign_filepath,chunk_size,out_dir,n_processes,resume) # (2) Create a .json file, where the info of all reads are stored per position, for modelling. if genome: # merge_transcript_id_version = check_gene_tx_id_version(gtf_or_gff) # if merge_transcript_id_version: # gtf_or_gff = mergeGTFtxIDversion(gtf_or_gff,out_dir) annotation_dict,is_gff = readAnnotation(gtf_or_gff) fasta_dict = readFasta(transcript_fasta,is_gff) parallel_preprocess_gene(eventalign_filepath,fasta_dict,annotation_dict,is_gff,out_dir,n_processes,readcount_min,readcount_max,resume) else: parallel_preprocess_tx(eventalign_filepath,out_dir,n_processes,readcount_min,readcount_max,resume) xpore-2.1/xpore/scripts/xpore.py0000644000175000017500000001132214130107110016355 0ustar nileshnileshimport sys from .dataprep import dataprep from .diffmod import diffmod from .postprocessing import postprocessing def parse_options(argv): """Parses options from the command line """ from argparse import ArgumentParser from xpore import __version__ parser = ArgumentParser(prog='xpore') subparsers = parser.add_subparsers(help='Running modes', metavar='{dataprep, diffmod, postprocessing}') parser.add_argument('-v', '--version', action='version', version='%(prog)s {version}'.format(version=__version__)) ### RUN MODE "DATAPREP" parser_dataprep = subparsers.add_parser('dataprep', help='run mode for preprocessing nanopolish eventalign.txt before differential modification analysis') optional_dataprep = parser_dataprep._action_groups.pop() required_dataprep = parser_dataprep.add_argument_group('required arguments') # Required arguments required_dataprep.add_argument('--eventalign', dest='eventalign', help='eventalign filepath, the output from nanopolish.',required=True) ##required.add_argument('--summary', dest='summary', help='eventalign summary filepath, the output from nanopolish.',required=True) required_dataprep.add_argument('--out_dir', dest='out_dir', help='output directory.',required=True) optional_dataprep.add_argument('--gtf_or_gff', dest='gtf_or_gff', help='GTF or GFF file path.',type=str) optional_dataprep.add_argument('--transcript_fasta', dest='transcript_fasta', help='transcript FASTA path.',type=str) # Optional arguments optional_dataprep.add_argument('--skip_eventalign_indexing', dest='skip_eventalign_indexing', help='skip indexing the eventalign nanopolish output.',default=False,action='store_true') # parser.add_argument('--features', dest='features', help='Signal features to extract.',type=list,default=['norm_mean']) optional_dataprep.add_argument('--genome', dest='genome', help='to run on Genomic coordinates. Without this argument, the program will run on transcriptomic coordinates',default=False,action='store_true') optional_dataprep.add_argument('--n_processes', dest='n_processes', help='number of processes to run.',type=int, default=1) optional_dataprep.add_argument('--chunk_size', dest='chunk_size', help='number of lines from nanopolish eventalign.txt for processing.',type=int, default=1000000) optional_dataprep.add_argument('--readcount_min', dest='readcount_min', help='minimum read counts per gene.',type=int, default=1) optional_dataprep.add_argument('--readcount_max', dest='readcount_max', help='maximum read counts per gene.',type=int, default=1000) optional_dataprep.add_argument('--resume', dest='resume', help='with this argument, the program will resume from the previous run.',default=False,action='store_true') #todo parser_dataprep._action_groups.append(optional_dataprep) parser_dataprep.set_defaults(func=dataprep) ### RUN MODE "DIFFMOD" parser_diffmod = subparsers.add_parser('diffmod', help='run mode for performing differential modification analysis') optional_diffmod = parser_diffmod._action_groups.pop() required_diffmod = parser_diffmod.add_argument_group('required arguments') # Required arguments required_diffmod.add_argument('--config', dest='config', help='YAML configuraion filepath.',required=True) # Optional arguments optional_diffmod.add_argument('--n_processes', dest='n_processes', help='number of processes to run.',type=int,default=1) optional_diffmod.add_argument('--save_models', dest='save_models', help='with this argument, the program will save the model parameters for each id.',default=False,action='store_true') # todo optional_diffmod.add_argument('--resume', dest='resume', help='with this argument, the program will resume from the previous run.',default=False,action='store_true') optional_diffmod.add_argument('--ids', dest='ids', help='gene or transcript ids to model.',default=[],nargs='*') parser_diffmod._action_groups.append(optional_diffmod) parser_diffmod.set_defaults(func=diffmod) ### RUN MODE "POSTPROCESSING" parser_postprocessing = subparsers.add_parser('postprocessing', help='run mode for post-processing diffmod.table, the result table from differential modification analysis.') required_postprocessing = parser_postprocessing.add_argument_group('required arguments') # Required arguments required_postprocessing.add_argument('--diffmod_dir', dest='diffmod_dir', help='diffmod directory path, the output from xpore-diffmod.',required=True) parser_postprocessing.set_defaults(func=postprocessing) return parser.parse_args(argv[1:]) def main(argv=sys.argv): ### get command line options options = parse_options(argv) options.func(options) if __name__ == "__main__": main(sys.argv) xpore-2.1/xpore/scripts/helper.py0000644000175000017500000001132414130107110016501 0ustar nileshnileshimport gzip import multiprocessing import numpy import os import pandas from functools import reduce from collections import defaultdict class EventalignFile: def __init__(self, fn): self._fn = fn self._open() def _open(self): fn = self._fn if os.path.splitext(fn)[1] == '.gz': self._handle = gzip.open(fn) self._decode_method = bytes.decode else: self._handle = open(fn) self._decode_method = str def __enter__(self): return self def __exit__(self, exc_type, exc_value, exc_traceback): self.close() def close(self): self._handle.close() def readline(self): self._handle.readline() def __iter__(self): return self def __next__(self): return self._decode_method(next(self._handle)) def decor_message(text,opt='simple'): text = text.upper() if opt == 'header': return text else: return '--- ' + text + ' ---\n' def end_queue(task_queue,n_processes): for _ in range(n_processes): task_queue.put(None) return task_queue def get_ids(f_index,data_info): #todo df_list = [] for condition_name, run_names in data_info.items(): list_of_set_ids = [] for run_name in run_names: list_of_set_ids += [set(f_index[run_name].keys())] # ids = reduce(lambda x,y: x.intersection(y), list_of_set_ids) ids = reduce(lambda x,y: x.union(y), list_of_set_ids) df_list += [pandas.DataFrame({'ids':list(ids),condition_name:[1]*len(ids)})] df_merged = reduce(lambda left,right: pandas.merge(left,right,on=['ids'], how='outer'), df_list).fillna(0).set_index('ids') return sorted(list(df_merged[df_merged.sum(axis=1) >= 2].index)) # At least two conditions. ## tmp: to remove # def get_gene_ids(config_filepath): # arguments are not used. # import os,pandas # from ..diffmod.configurator import Configurator # # config_filepath = '/ploy_ont_workspace/github/experiments/Release_v1_0/config_manuscript/gmm_HEK293T-KO_HEK293T-WT_HEPG2-WT_K562-WT_A549-WT_MCF7-WT_reps_v01.ini' # config = Configurator(config_filepath) # paths = config.get_paths() # info = config.get_info() # criteria = config.get_criteria() # df_gt_ids = pandas.read_csv('/ploy_ont_workspace/out/Release_v1_0/statCompare/data/mapping_gt_ids.csv') # gene_ids = set(df_gt_ids['g_id'].unique()) # read_count_sum_min,read_count_sum_max = criteria['read_count_min'],criteria['read_count_max'] # df_read_count = {} # for run_name in set(info['run_names']): # read_count_filepath = os.path.join(paths['data_dir'],run_name,'summary','read_count_per_gene.csv') # df_read_count[run_name] = pandas.read_csv(read_count_filepath).set_index('g_id') # for run_name in set(info['run_names']): # df_read_count[run_name].reset_index(inplace=True) # cond = df_read_count[run_name]['n_reads'] >= criteria['read_count_min'] # cond &= df_read_count[run_name]['n_reads'] <= criteria['read_count_max'] # gene_ids = gene_ids.intersection(set(df_read_count[run_name].loc[cond,'g_id'].values)) # gene_ids = sorted(list(gene_ids)) # return gene_ids ## class Consumer(multiprocessing.Process): """ For parallelisation """ def __init__(self,task_queue,task_function,locks=None,result_queue=None): multiprocessing.Process.__init__(self) self.task_queue = task_queue self.locks = locks self.task_function = task_function self.result_queue = result_queue def run(self): proc_name = self.name while True: next_task_args = self.task_queue.get() if next_task_args is None: self.task_queue.task_done() break result = self.task_function(*next_task_args,self.locks) self.task_queue.task_done() if self.result_queue is not None: self.result_queue.put(result) def read_last_line(filepath): # https://stackoverflow.com/questions/3346430/what-is-the-most-efficient-way-to-get-first-and-last-line-of-a-text-file/3346788 if not os.path.exists(filepath): return with open(filepath, "rb") as f: first = f.readline() # Read the first line. if first == b'': return f.seek(-2, os.SEEK_END) # Jump to the second last byte. while f.read(1) != b"\n": # Until EOL is found... f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more. last = f.readline() # Read last line. return last def is_successful(filepath): return read_last_line(filepath) == b'--- SUCCESSFULLY FINISHED ---\n' xpore-2.1/xpore/scripts/diffmod.py0000644000175000017500000001657514130107110016647 0ustar nileshnileshimport numpy as np import pandas import os import multiprocessing import ujson from collections import defaultdict import csv from . import helper from ..diffmod.configurator import Configurator from ..diffmod.gmm import GMM from ..diffmod import io from ..diffmod.statstest import StatsTest def execute(idx, data_dict, data_info, method, criteria, model_kmer, prior_params, out_paths, save_models,locks): """ Run the model on each posiiton across the given idx. """ data = io.load_data(idx,data_dict,min_count=criteria['readcount_min'],max_count=criteria['readcount_max'],pooling=method['pooling']) models = dict() for key,data_at_pos in data.items(): # For each position idx, pos, kmer = key kmer_signal = {'mean':model_kmer.loc[kmer,'model_mean'],'std':model_kmer.loc[kmer,'model_stdv']} kmer_signal['tau'] = 1./(kmer_signal['std']**2) y_mean = data_at_pos['y'].mean() y_tau = 1./(data_at_pos['y'].std()**2) K = 2 if method['pooling']: n_groups = len(data_at_pos['condition_names']) else: n_groups = len(data_at_pos['run_names']) ### Set up priors. priors = {'mu_tau':defaultdict(list),'w':dict()} for k in range(K): priors['mu_tau']['location'] += [kmer_signal['mean']] priors['mu_tau']['lambda'] += [prior_params['mu_tau']['lambda'][k]] priors['mu_tau']['alpha'] += [kmer_signal['tau']] priors['mu_tau']['beta'] += [prior_params['mu_tau']['beta_scale'][k]*1./kmer_signal['tau']] for k,v in priors['mu_tau'].items(): priors['mu_tau'][k] = np.array(v) priors['w']['concentration'] = np.ones([n_groups,K])*1. #GK priors['w']['concentration'][:,0] = float(prior_params['w']['concentration'][0]) priors['w']['concentration'][:,1] = float(prior_params['w']['concentration'][1]) ### ### Fit a model. if method['prefiltering']: pval = StatsTest(data_at_pos).fit(method=method['prefiltering']['method']) if np.isnan(pval) | (pval < method['prefiltering']['threshold']): prefiltering = {method['prefiltering']['method']:pval} models[key] = GMM(method,data_at_pos,priors=priors,kmer_signal=kmer_signal).fit(), prefiltering else: models[key] = GMM(method,data_at_pos,priors=priors,kmer_signal=kmer_signal).fit(), None if save_models & (len(models)>0): #todo: print(out_paths['model_filepath'],idx) io.save_models_to_hdf5(models,out_paths['model_filepath']) if len(models)>0: # Generating the result table. table = io.generate_result_table(models,data_info) with locks['table'], open(out_paths['table'],'a') as f: csv.writer(f,delimiter=',').writerows(table) # # Logging # log_str = '%s: Saving the result table ... Done.' %(idx) # with locks['log'], open(out_paths['log'],'a') as f: # f.write(log_str + '\n') # Logging with locks['log'], open(out_paths['log'],'a') as f: f.write(idx + '\n') def diffmod(args): n_processes = args.n_processes config_filepath = args.config save_models = args.save_models resume = args.resume ids = args.ids config = Configurator(config_filepath) paths = config.get_paths() data_info = config.get_data_info() method = config.get_method() criteria = config.get_criteria() prior_params = config.get_priors() print('Using the signal of unmodified RNA from',paths['model_kmer']) model_kmer = pandas.read_csv(paths['model_kmer']).set_index('model_kmer') ### ### # Get gene ids for modelling # todo # Create output paths and locks. out_paths,locks = dict(),dict() for out_filetype in ['model','table','log']: out_paths[out_filetype] = os.path.join(paths['out_dir'],'diffmod.%s' %out_filetype) locks[out_filetype] = multiprocessing.Lock() # Create communication queues. task_queue = multiprocessing.JoinableQueue(maxsize=n_processes * 2) # Writing the starting of the files. ids_done = [] if resume and os.path.exists(out_paths['log']): ids_done = [line.rstrip('\n') for line in open(out_paths['log'],'r')] else: with open(out_paths['table'],'w') as f: csv.writer(f,delimiter=',').writerow(io.get_result_table_header(data_info,method)) with open(out_paths['log'],'w') as f: f.write(helper.decor_message('diffmod')) # Create and start consumers. consumers = [helper.Consumer(task_queue=task_queue,task_function=execute,locks=locks) for i in range(n_processes)] for p in consumers: p.start() ### Load tasks in to task_queue. ### f_index,f_data = {},{} for condition_name, run_names in data_info.items(): for run_name, dirpath in run_names.items(): # Read index files df_index = pandas.read_csv(os.path.join(dirpath,'data.index'),sep=',') f_index[run_name] = dict(zip(df_index['idx'],zip(df_index['start'],df_index['end']))) # Read readcount files # df_readcount[run_name] = pandas.read_csv(os.path.join(info['dirpath'],'readcount.csv')).groupby('gene_id')['n_reads'].sum() # todo: data.readcount # Open data files f_data[run_name] = open(os.path.join(dirpath,'data.json'),'r') # Load tasks into task_queue. # gene_ids = helper.get_gene_ids(config.filepath) # gene_ids = ['ENSG00000168496','ENSG00000204388','ENSG00000123989','ENSG00000170144'] #test data; todo # gene_ids = ['ENSG00000159111'] if len(ids) == 0: ids = helper.get_ids(f_index,data_info) print(len(ids),'ids to be testing ...') for idx in ids: if resume and (idx in ids_done): continue data_dict = dict() for condition_name, run_names in data_info.items(): for run_name, dirpath in run_names.items(): try: pos_start,pos_end = f_index[run_name][idx] except KeyError: data_dict[(condition_name,run_name)] = None else: # print(idx,run_name,pos_start,pos_end,df_readcount[run_name].loc[idx]) f_data[run_name].seek(pos_start,0) json_str = f_data[run_name].read(pos_end-pos_start) # print(json_str[:50]) # json_str = '{%s}' %json_str # used for old dataprep data_dict[(condition_name,run_name)] = ujson.loads(json_str) # A data dict for each gene. # tmp out_paths['model_filepath'] = os.path.join(paths['models'],'%s.hdf5' %idx) # # if data_dict[run_name][idx] is not None: # todo: remove this line. Fix in dataprep task_queue.put((idx, data_dict, data_info, method, criteria, model_kmer, prior_params, out_paths,save_models)) # Blocked if necessary until a free slot is available. # Put the stop task into task_queue. task_queue = helper.end_queue(task_queue,n_processes) # Wait for all of the tasks to finish. task_queue.join() # Close data files for f in f_data.values(): f.close() with open(out_paths['log'],'a+') as f: f.write(helper.decor_message('successfully finished')) xpore-2.1/xpore/scripts/__init__.py0000644000175000017500000000000014130107110016746 0ustar nileshnileshxpore-2.1/xpore/diffmod/0000755000175000017500000000000014130107110014570 5ustar nileshnileshxpore-2.1/xpore/diffmod/io.py0000644000175000017500000003462514130107110015563 0ustar nileshnileshimport numpy as np import h5py import os from collections import OrderedDict, defaultdict import itertools import scipy.stats from ..utils import stats from .gmm import GMM def get_dummies(x): X = [] labels = sorted(list(set(x))) for label in labels: X += [x == label] # labels = [label.encode('UTF-8') for label in labels] return np.array(X).T, labels def load_data(idx, data_dict, min_count, max_count, pooling=False): """ Parameters ---------- data_dict: dict of ... Data for a gene """ # Create all (pos,kmer) pairs from all runs. position_kmer_pairs = [] for (condition_name,run_name), d_dict in data_dict.items(): # data_dict[run_name][idx][position][kmer] pairs = [] if d_dict is not None: for pos in d_dict[idx].keys(): for kmer in d_dict[idx][pos].keys(): pairs += [(pos,kmer)] position_kmer_pairs += [pairs] position_kmer_pairs = set(position_kmer_pairs[0]).intersection(*position_kmer_pairs) data = OrderedDict() for pos,kmer in position_kmer_pairs: y, read_ids, condition_labels, run_labels = [], [], [], [] n_reads = defaultdict(list) for (condition_name,run_name), d_dict in data_dict.items(): if d_dict is not None: norm_means = d_dict[idx][pos][kmer]#['norm_means'] n_reads_per_run = len(norm_means) # In case of pooling==False, if not enough reads, don't include them. if (not pooling) and ((n_reads_per_run < min_count) or (n_reads_per_run > max_count)): continue # n_reads[condition_name] += [n_reads_per_run] y += norm_means # read_ids += list(data_dict[run_name][idx][pos][kmer]['read_ids'][:]) condition_labels += [condition_name]*n_reads_per_run run_labels += [run_name]*n_reads_per_run y = np.array(y) # read_ids = np.array(read_ids) condition_labels = np.array(condition_labels) run_labels = np.array(run_labels) # Filter those sites that don't have enough reads. if len(y) == 0: # no reads at all. continue conditions_incl = [] unique_condition_names = {condition_name for condition_name,_ in data_dict.keys()} if pooling: # At the modelling step all the reads from the same condition will be combined. for condition_name in unique_condition_names: if (sum(n_reads[condition_name]) >= min_count) and (sum(n_reads[condition_name]) <= max_count): conditions_incl += [condition_name] else: for condition_name in unique_condition_names: if (np.array(n_reads[condition_name]) >= min_count).any() and (np.array(n_reads[condition_name]) <= max_count).any(): conditions_incl += [condition_name] if len(conditions_incl) < 2: continue # Get dummies x, condition_names_dummies = get_dummies(condition_labels) r, run_names_dummies = get_dummies(run_labels) key = (idx, pos, kmer) data[key] = {'y': y, 'x': x, 'r': r, 'condition_names': condition_names_dummies, 'run_names': run_names_dummies, 'y_condition_names': condition_labels, 'y_run_names': run_labels} # , 'read_ids': read_ids return data def save_result_table(table, out_filepath): out_file = h5py.File(out_filepath, 'w') out_file['result'] = table # Structured np array. out_file.close() def save_models_to_hdf5(models, model_filepath): # per gene/transcript """ Save model parameters. Parameters ---------- models Learned models. model_filepath: str Path to save the models. """ model_file = h5py.File(model_filepath, 'w') for model_key, model_tuple in models.items(): idx, position, kmer = model_key model, prefiltering = model_tuple position = str(position) if idx not in model_file: model_file.create_group(idx) model_file[idx].create_group(position) model_file[idx][position].attrs['kmer'] = kmer.encode('UTF-8') # model_file[idx][position].create_group('info') # for key, value in model.info.items(): # model_file[idx][position]['info'][key] = value model_file[idx][position].create_group('nodes') # ['x','y','z','w','mu_tau'] => store only their params for node_name in model.nodes: model_file[idx][position]['nodes'].create_group(node_name) if model.nodes[node_name].data is not None: # Todo: make it optional. value = model.nodes[node_name].data if node_name in ['y_run_names','y_condition_names']: value = [val.encode('UTF-8') for val in value] model_file[idx][position]['nodes'][node_name]['data'] = value if model.nodes[node_name].params is None: continue for param_name, value in model.nodes[node_name].params.items(): if param_name == 'group_names': value = [val.encode('UTF-8') for val in value] model_file[idx][position]['nodes'][node_name][param_name] = value model_file.close() def load_models(model_filepath): # per gene/transcript #Todo: refine. """ Construct a model and load model parameters. Parameters ---------- model_filepath: str Path where the model is stored. Return ------ models Models for each genomic position. """ model_file = h5py.File(model_filepath, 'r') models = {} data = defaultdict(dict) for idx in model_file: for position in model_file[idx]: inits = {'info': None, 'nodes': {'x': {}, 'y': {}, 'w': {}, 'mu_tau': {}, 'z': {}}} kmer = model_file[idx][position].attrs['kmer'] key = (idx, position, kmer) # for k in model_file[idx][position]['info']: # inits['info'] = model_file[idx][position]['info'][k] for node_name, params in model_file[idx][position]['nodes'].items(): for param_name, value in params.items(): if param_name == 'data': data[key][node_name] = value[:] else: inits['nodes'][node_name][param_name] = value[:] # for param_name, value in priors.items(): # inits['nodes'][node_name][param_name] = value[:] models[key] = GMM(data,inits=inits) model_file.close() return models,data # {(idx,position,kmer): GMM obj} def get_ordered_condition_run_names(data_info): unique_condition_names = list(data_info.keys()) unique_condition_names_sorted = sorted(unique_condition_names) unique_run_names_sorted = sum([list(run_names.keys()) for _, run_names in data_info.items()],[]) return unique_condition_names_sorted, unique_run_names_sorted def get_result_table_header(data_info,method): condition_names,run_names = get_ordered_condition_run_names(data_info) ### stats header stats_pairwise = [] for cond1, cond2 in itertools.combinations(condition_names, 2): pair = '_vs_'.join((cond1, cond2)) # cond1 - cond2 stats_pairwise += ['diff_mod_rate_%s' % pair, 'pval_%s' % pair, 'z_score_%s' % pair] stats_one_vs_all = [] for condition_name in condition_names: pair = '%s_vs_all' %condition_name # condition_name - the rest stats_one_vs_all += ['diff_mod_rate_%s' % pair, 'pval_%s' %pair, 'z_score_%s' %pair] ### position header header = ['id', 'position', 'kmer'] # header += ['p_overlap'] # header += ['x_x1', 'y_x1', 'x_x2', 'y_x2'] if method['pooling']: names = condition_names else: names = run_names ### stats header header += stats_pairwise if len(condition_names) > 2: header += stats_one_vs_all ### ### model header for name in names: header += ['mod_rate_%s' % name] for name in names: header += ['coverage_%s' % name] header += ['mu_unmod', 'mu_mod', 'sigma2_unmod', 'sigma2_mod', 'conf_mu_unmod', 'conf_mu_mod','mod_assignment'] ### ### prefiltering header if method['prefiltering']: header += [method['prefiltering']['method']] return header def generate_result_table(models, data_info): # per idx (gene/transcript) """ Generate a table containing learned model parameters and statistic tests. Parameters ---------- models Learned models for individual genomic positions of a gene. group_labels Labels of samples. data_inf Dict Returns ------- table List of tuples. """ ### condition_names,run_names = get_ordered_condition_run_names(data_info) # information from the config file used for modelling. ### ### table = [] for key, (model,prefiltering) in models.items(): idx, position, kmer = key mu = model.nodes['mu_tau'].expected() # K sigma2 = 1./model.nodes['mu_tau'].expected(var='gamma') # K var_mu = model.nodes['mu_tau'].variance(var='normal') # K # mu = model.nodes['y'].params['mean'] # sigma2 = model.nodes['y'].params['variance'] w = model.nodes['w'].expected() # GK N = model.nodes['y'].params['N'].round() # GK N0 = N[:, 0].squeeze() N1 = N[:, 1].squeeze() w0 = w[:, 0].squeeze() coverage = np.sum(model.nodes['y'].params['N'], axis=-1) # GK => G # n_reads per group p_overlap, list_cdf_at_intersections = stats.calc_prob_overlapping(mu, sigma2) model_group_names = model.nodes['x'].params['group_names'] #condition_names if pooling, run_names otherwise. ### Cluster assignment ### conf_mu = [calculate_confidence_cluster_assignment(mu[0],model.kmer_signal),calculate_confidence_cluster_assignment(mu[1],model.kmer_signal)] cluster_idx = {} if conf_mu[0] > conf_mu[1]: cluster_idx['unmod'] = 0 cluster_idx['mod'] = 1 else: cluster_idx['unmod'] = 1 cluster_idx['mod'] = 0 mu_assigned = [mu[cluster_idx['unmod']],mu[cluster_idx['mod']]] sigma2_assigned = [sigma2[cluster_idx['unmod']],sigma2[cluster_idx['mod']]] conf_mu = [conf_mu[cluster_idx['unmod']],conf_mu[cluster_idx['mod']]] w_mod = w[:,cluster_idx['mod']] mod_assignment = [['higher','lower'][(mu[0] 2: ### calculate stats_one_vs_all stats_one_vs_all = [] for cond in condition_names: if model.method['pooling']: cond = [cond] else: cond = list(data_info[cond].keys()) if any(r in model_group_names for r in cond): w_cond1 = w[np.isin(model_group_names, cond), cluster_idx['mod']].flatten() w_cond2 = w[~np.isin(model_group_names, cond), cluster_idx['mod']].flatten() n_cond1 = coverage[np.isin(model_group_names, cond)] n_cond2 = coverage[~np.isin(model_group_names, cond)] z_score, p_ws = stats.z_test(w_cond1, w_cond2, n_cond1, n_cond2) w_mod_mean_diff = np.mean(w_cond1)-np.mean(w_cond2) stats_one_vs_all += [w_mod_mean_diff, p_ws, z_score] else: stats_one_vs_all += [None, None, None] ### w_mod_ordered, coverage_ordered = [], [] # ordered by conditon_names or run_names based on headers. if model.method['pooling']: names = condition_names else: names = run_names for name in names: if name in model_group_names: w_mod_ordered += list(w_mod[np.isin(model_group_names, name)]) coverage_ordered += list(coverage[np.isin(model_group_names, name)]) else: w_mod_ordered += [None] coverage_ordered += [None] ### ### prepare values to write row = [idx, position, kmer] row += stats_pairwise if len(condition_names) > 2: row += stats_one_vs_all # row += [p_overlap] # row += list_cdf_at_intersections row += list(w_mod_ordered) row += list(coverage_ordered) row += mu_assigned + sigma2_assigned + conf_mu + mod_assignment if prefiltering is not None: row += [prefiltering[model.method['prefiltering']['method']]] ### Filtering those positions with a nearly single distribution. cdf_threshold = 0.1 x_x1, y_x1, x_x2, y_x2 = list_cdf_at_intersections is_not_inside = ((y_x1 < cdf_threshold) & (x_x1 < cdf_threshold)) | ((y_x2 < cdf_threshold) & (x_x2 < cdf_threshold)) | (( (1-y_x1) < cdf_threshold) & ((1-x_x1) < cdf_threshold)) | (( (1-y_x2) < cdf_threshold) & ((1-x_x2) < cdf_threshold)) if (p_overlap <= 0.5) and (is_not_inside): table += [tuple(row)] return table def calculate_confidence_cluster_assignment(mu,kmer_signal): cdf = scipy.stats.norm.cdf(kmer_signal['mean'] - abs(kmer_signal['mean']-mu), loc=kmer_signal['mean'], scale=kmer_signal['std']) return cdf*2 xpore-2.1/xpore/diffmod/gmm.py0000644000175000017500000004131414130107110015725 0ustar nileshnileshimport numpy as np from numpy import newaxis import scipy.special import scipy.stats class GMM(object): """ 1D multi-sample 2-Gaussian mixture model. """ def __init__(self, method=None, data={'x': None, 'y': None, 'condition_names': None, 'run_names': None}, inits={'info': None, 'nodes': {'x': None, 'y': None, 'w': None, 'mu_tau': None, 'z': None}}, priors={'mu_tau': None, 'w': None},kmer_signal=None): """ Parameters ---------- method: dict Config file data: dict Sth. inits: dict Sth. priors: Sth. """ self.nodes = dict() self.aux_params = dict() self.K = 2 # modified and unmodified self.method = method self.kmer_signal = kmer_signal self.__init_info(inits['info']) data_node_x = None if (data['x'] is not None) and (data['y'] is not None) and (data['r'] is not None): self.info['n_reads'] = len(data['y']) # inits location_0 = priors['mu_tau']['location'][0] if location_0 < np.percentile(data['y'], q=50): location_1 = np.percentile(data['y'], q=75) else: location_1 = np.percentile(data['y'], q=25) locations = np.array([location_0, location_1]) # print(locations) # kmeans = KMeans(n_clusters=self.K).fit(data['y'][:,newaxis]) # locations = kmeans.cluster_centers_.flatten() inits['nodes']['mu_tau'] = {'location': locations, 'lambda': priors['mu_tau']['lambda'], 'alpha': priors['mu_tau']['alpha'], 'beta': priors['mu_tau']['beta']} if self.method['pooling']: inits['nodes']['x'] = {'group_names': data['condition_names']} data_node_x = data['x'] else: inits['nodes']['x'] = {'group_names': data['run_names']} data_node_x = data['r'] # additional nodes for postprocessing analysis self.nodes['y_condition_names'] = Constant(data=data['y_condition_names']) self.nodes['y_run_names'] = Constant(data=data['y_run_names']) # Define the graph self.nodes['x'] = Constant(data=data_node_x, inits=inits['nodes']['x']) # NG self.n_groups = len(self.nodes['x'].params['group_names']) self.nodes['w'] = Dirichlet(dim=(self.n_groups, self.K), inits=inits['nodes']['w'], priors=priors['w']) self.nodes['mu_tau'] = UnivariateNormalGamma(dim=(self.K), inits=inits['nodes']['mu_tau'], priors=priors['mu_tau']) self.nodes['z'] = Bernoulli(dim=(self.info['n_reads'], self.K), parents={'w': self.nodes['w'], 'x': self.nodes['x']}, inits=inits['nodes']['z']) self.nodes['y'] = UnivariateNormalMixture(parents={'z': self.nodes['z'], 'mu_tau': self.nodes['mu_tau'], 'x': self.nodes['x']}, data=data['y'], inits=inits['nodes']['y']) def __init_info(self, inits): if inits is None: self.info = {'n_reads': 0, 'log_elbos': [], 'converged': False, 'n_iterations': -1, 'convergence_ratio': -1} else: self.info = inits def __compute_log_elbo(self): """ Compute log ELBO. """ log_elbo = self.nodes['y']._log_likelihood() log_elbo += self.nodes['z']._log_prob_prior() log_elbo -= self.nodes['z']._log_prob_posterior() log_elbo += self.nodes['w']._log_prob_prior() log_elbo -= self.nodes['w']._log_prob_posterior() log_elbo += self.nodes['mu_tau']._log_prob_prior(var='normal') log_elbo += self.nodes['mu_tau']._log_prob_prior(var='gamma') log_elbo -= self.nodes['mu_tau']._log_prob_posterior(var='normal') log_elbo -= self.nodes['mu_tau']._log_prob_posterior(var='gamma') return log_elbo def fit(self): """ Fit. """ error = False converged = False # self.__init_params() log_elbo_old = self.__compute_log_elbo() # -np.inf self.nodes['y']._update() for iteration in range(1, self.method['max_iters']): self.nodes['z']._update(children={'y': self.nodes['y']}) # every time z is updated, y needs to be done too. Update z so that z is not randomly drawn. self.nodes['y']._update() self.nodes['w']._update(children=self.nodes['y'].params) self.nodes['mu_tau']._update(children={**self.nodes['y'].params, **{'x': self.nodes['x']}}) log_elbo_new = self.__compute_log_elbo() error = np.round(log_elbo_new - log_elbo_old, 8) < 0 if error: print('ERROR: log ELBO is decreasing ...') print(iteration, log_elbo_old, log_elbo_new, error) error = True break else: self.info['log_elbos'] += [log_elbo_old] diff = (log_elbo_new - log_elbo_old)/np.abs(log_elbo_old) log_elbo_old = log_elbo_new # print('Iteration %d:' %(iteration),'Convergence ratio =',diff) if (diff < self.method['stopping_criteria']): converged = True break self.info['n_iterations'] = iteration self.info['converged'] = converged self.info['convergence_ratio'] = diff return self ################################ ##### Model for each node ###### ################################ class Constant(object): """ Constant node. """ def __init__(self, data=None, inits=None): self.params = inits self.data = data class UnivariateNormalMixture(object): def __init__(self, parents=None, data=None, inits=None): self.parents = parents self.data = data self.params = dict.fromkeys(['N', 'mean', 'variance']) self.__initialise_params(inits) def __initialise_params(self, inits): if inits is not None: self.params = inits def _log_likelihood(self): n_reads = len(self.data) mu_tau_nomal_means = np.repeat(self.parents['mu_tau'].expected(var='normal')[newaxis, :], repeats=n_reads, axis=-2) # N,K mu_tau_normal_variances = np.repeat(self.parents['mu_tau'].variance(var='normal')[newaxis, :], repeats=n_reads, axis=-2) # N,K mu_tau_gamma_means = np.repeat(self.parents['mu_tau'].expected(var='gamma')[newaxis, :], repeats=n_reads, axis=-2) # N,K mu_tau_log_gamma = np.repeat(self.parents['mu_tau'].expected(var='gamma', inner_func='log')[newaxis, :], repeats=n_reads, axis=-2) # N,K y = self.data[..., newaxis] # N,1 log_likelihood = self.parents['z'].expected() * 0.5 * (mu_tau_log_gamma - np.log(2*np.pi) - mu_tau_gamma_means * (y**2 - 2*y * mu_tau_nomal_means + mu_tau_nomal_means**2 + mu_tau_normal_variances)) # N,K log_likelihood *= (np.sum(self.parents['x'].data, axis=-1)[..., newaxis] > 0) # N,K return np.sum(np.sum(log_likelihood, axis=-1), axis=-1) # sum across components and reads respectively => 1 def expected(self, inner_func=None): return self.params['mean'] def variance(self, inner_func=None): return self.params['variance'] def N(self, inner_func=None): return self.params['N'] def _update(self): n_reads = len(self.data) self.params['N'] = np.sum(self.parents['x'].data[..., newaxis] * self.parents['z'].expected()[:, newaxis, :], axis=-3) # sum across reads => G,K N = np.sum(self.params['N'], axis=-2) # sum across groups => K N[N < 1e-30] = 0. # Hack: Avoid overflow in later calculation steps. N_inverse = np.divide(1., N, out=np.zeros_like(N), where=N != 0.) y = self.data[..., newaxis] # N,1 self.params['mean'] = N_inverse * np.sum(self.parents['z'].expected()*y, axis=-2) # sum across reads => K residuals = (np.sum(self.parents['x'].data, axis=-1) > 0)[..., newaxis]*(y - np.repeat(self.params['mean'][newaxis, :], repeats=n_reads, axis=-2)) # N,K self.params['variance'] = N_inverse * np.sum(self.parents['z'].expected()*(residuals**2), axis=-2) # sum across reads => K class Bernoulli(object): def __init__(self, dim, parents=None, data=None, inits=None): self.params = dict() self.params['prob'] = np.full(dim, np.nan) self.params['ln_prob'] = np.full(dim, np.nan) self.__initialise_params(inits) self.parents = parents self.data = data def __initialise_params(self, inits=None): if inits is None: # self.params['prob'][:] = 0.5 # self.params['ln_prob'][:] = np.log(0.5) self.params['prob'] = np.random.random(self.params['prob'].shape) self.params['prob'] /= np.sum(self.params['prob'], axis=1)[:, newaxis] self.params['ln_prob'] = np.log(self.params['prob']) else: self.params = inits def _log_prob_prior(self): res = self.expected() * np.sum(self.parents['x'].data[..., newaxis] * self.parents['w'].expected(inner_func='log')[newaxis, :, :], axis=-2) # sum across groups => N,K return np.sum(np.sum(res, axis=-1), axis=-1) # 1 def _log_prob_posterior(self): res = self.expected() * np.sum(self.parents['x'].data[:, newaxis, :] * self.params['ln_prob'][..., newaxis], axis=-1) # N,K return np.sum(np.sum(res, axis=-1), axis=-1) # 1 def expected(self, inner_func=None): return self.params['prob'] # def variance(self): # return self.params['prob']*(1-self.params['prob']) #scipy.stats.bernoulli.var(self.params['concentration']) # def _update(self, children): # print('#'*5,'Updating z','#'*5) n_reads = len(children['y'].data) mu_tau_nomal_means = np.repeat(children['y'].parents['mu_tau'].expected(var='normal')[newaxis, :], repeats=n_reads, axis=-2) # N,K mu_tau_normal_variances = np.repeat(children['y'].parents['mu_tau'].variance(var='normal')[newaxis, :], repeats=n_reads, axis=-2) # N,K mu_tau_gamma_means = np.repeat(children['y'].parents['mu_tau'].expected(var='gamma')[newaxis, :], repeats=n_reads, axis=-2) # N,K mu_tau_expected_log_gamma = np.repeat(children['y'].parents['mu_tau'].expected(var='gamma', inner_func='log')[newaxis, :], repeats=n_reads, axis=-2) # N,K y = children['y'].data[..., newaxis] # N,1 ln_rho = 0.5*(mu_tau_expected_log_gamma - np.log(2*np.pi) - mu_tau_gamma_means * (y**2 - 2*y * mu_tau_nomal_means + mu_tau_nomal_means**2 + mu_tau_normal_variances)) ln_rho += np.sum(self.parents['x'].data[..., newaxis]*self.parents['w'].expected(inner_func='log')[newaxis, :, :], axis=-2) # sum across groups => N,K ln_rho_diff = ln_rho[:, 1]-ln_rho[:, 0] prob = 1./(1.+np.exp(np.clip(ln_rho_diff, -500, 500))) # N,1 => N self.params['prob'] = np.stack([prob, 1-prob], axis=-1) self.params['ln_prob'] = ln_rho - scipy.special.logsumexp(ln_rho, axis=-1)[..., newaxis] # N,K class Dirichlet(object): def __init__(self, dim, parents=None, data=None, inits=None, priors=None): # dim - [,n_categories] self.priors = dict() self.priors['concentration'] = np.full(dim, np.nan) self.__set_priors(priors) self.params = dict() self.params['concentration'] = np.full(dim, np.nan) self.__initialise_params(inits) self.parents = parents self.data = data def __initialise_params(self, inits=None): if inits is None: self.params['concentration'][:] = self.priors['concentration'][:] else: self.params = inits def __set_priors(self, priors=None): if priors is None: self.priors['concentration'][:] = 1 else: self.priors = priors def _log_prob_prior(self): res = Dirichlet.__log_C(self.priors['concentration']) + np.sum((self.priors['concentration']-1)*self.expected(inner_func='log'), axis=-1) # sum k => G return np.sum(res, axis=-1) # 1 def _log_prob_posterior(self): res = Dirichlet.__log_C(self.params['concentration']) + np.sum((self.params['concentration']-1)*self.expected(inner_func='log'), axis=-1) # sum k => G return np.sum(res, axis=-1) # 1 def expected(self, inner_func=None): if inner_func == 'log': res = scipy.special.digamma(self.params['concentration']) # G,K res -= scipy.special.digamma(np.sum(self.params['concentration'], axis=-1))[..., newaxis] # G,K / G,1 => G,K return res else: return self.params['concentration']/np.sum(self.params['concentration'], axis=-1)[..., newaxis] # G,K def variance(self): # To check, still wrong but unused. return (self.expected()*(1-self.expected())) / (np.sum(self.params['concentration'], axis=-1)[..., newaxis]+1) def _update(self, children): # print('#'*5,'Updating w','#'*5) self.params['concentration'] = self.priors['concentration'] + children['N'] def __log_C(alpha): return scipy.special.gammaln(np.sum(alpha, axis=-1)) - np.sum(scipy.special.gammaln(alpha), axis=-1) class UnivariateNormalGamma(object): def __init__(self, dim, parents=None, data=None, inits=None, priors=None): self.priors = dict.fromkeys(['location', 'lambda', 'alpha', 'beta'], np.full(dim, np.nan)) # alpha = shape, beta=rate self.__set_priors(priors) self.params = dict.fromkeys(['location', 'lambda', 'alpha', 'beta'], np.full(dim, np.nan)) # alpha = shape, beta=rate self.__initialise_params(inits) self.parents = parents self.data = data def __initialise_params(self, inits): if inits is None: for param in self.params: self.params[param] = self.priors[param][:] self.params['location'][:] = np.random.normal(loc=self.priors['location'][:], scale=1) else: self.params = inits def __set_priors(self, priors=None): # if priors is None: # self.priors['location'][:] = 0. # Todo: set to mean # self.priors['lambda'][:] = 1. # self.priors['alpha'][:] = 1. # self.priors['beta'][:] = 1. # else: self.priors = priors def _log_prob_prior(self, var='normal'): if var == 'normal': res = 0.5 * (self.expected(var='gamma', inner_func='log') + np.log(self.priors['lambda']) - np.log(2*np.pi) - self.priors['lambda']*self.expected(var='gamma') * (self.expected(var='normal')**2 + self.variance(var='normal') - 2*self.expected(var='normal')*self.priors['location'] + self.priors['location']**2)) # K else: # gamma res = self.priors['alpha']*np.log(self.priors['beta'])-scipy.special.gammaln(self.priors['alpha'])+(self.priors['alpha']-1)*self.expected(var='gamma', inner_func='log') - self.priors['beta']*self.expected(var='gamma') # K return np.sum(res, axis=-1) # 1 def _log_prob_posterior(self, var='normal'): if var == 'normal': res = 0.5 * (self.expected(var='gamma', inner_func='log') + np.log(self.params['lambda']) - np.log(2*np.pi) - self.params['lambda']*self.expected(var='gamma') * (self.expected(var='normal')**2 + self.variance(var='normal') - 2*self.expected(var='normal')*self.params['location'] + self.params['location']**2)) # K else: # gamma res = self.params['alpha']*np.log(self.params['beta'])-scipy.special.gammaln(self.params['alpha'])+(self.params['alpha']-1)*self.expected(var='gamma', inner_func='log') - self.params['beta']*self.expected(var='gamma') # K return np.sum(res, axis=-1) # 1 def expected(self, var='normal', inner_func=None): if inner_func == 'log': if var == 'gamma': # tau return scipy.special.digamma(self.params['alpha']) - np.log(self.params['beta']) # K else: if var == 'normal': return self.params['location'] else: # gamma return self.params['alpha'] / self.params['beta'] def variance(self, var='normal'): if var == 'normal': return 1./(self.params['lambda']*self.expected(var='gamma')) # else: # gamma # return self.params['alpha']/(self.params['beta']**2) def _update(self, children): # print('#'*5,'Updating mu,tau','#'*5) N = np.sum(children['N'], axis=-2) # sum across groups => K self.params['lambda'] = self.priors['lambda'] + N # K self.params['location'] = (1./self.params['lambda']) * (N*children['mean'] + self.priors['lambda']*self.priors['location']) # K self.params['alpha'] = self.priors['alpha'] + 0.5*N # K self.params['beta'] = self.priors['beta'] + 0.5*(N*children['variance'] + ((self.priors['lambda']*N) / (self.priors['lambda']+N))*(children['mean']-self.priors['location'])**2) # K xpore-2.1/xpore/diffmod/statstest.py0000644000175000017500000000143614130107110017204 0ustar nileshnileshimport scipy.stats import numpy as np class StatsTest(object): def __init__(self,data): if self.__isok(data): self.data = [data['y'][data['x'][:,0]==1],data['y'][data['x'][:,1]==1]] else: self.data = None def fit(self,method): pval = np.nan if self.data is not None: if method == 't-test': stats,_ = scipy.stats.ttest_ind(self.data[0],self.data[1]) df = len(self.data[0]) + len(self.data[1]) - 2 pval = scipy.stats.t.sf(np.abs(stats), df)*2 # also defined as 1 - cdf, but sf is sometimes more accurate. #(1 - scipy.stats.t.cdf(abs(stat), df)) * return pval def __isok(self,data): return data['x'].shape[1] == 2 # Check if only two conditions. xpore-2.1/xpore/diffmod/model_kmer.csv0000644000175000017500000004231314130107110017426 0ustar nileshnileshmodel_kmer,model_mean,model_stdv AAAAA,108.9,2.68 AAAAC,107.75,2.68 AAAAG,101.72,2.68 AAAAT,112.77,2.68 AAACA,99.38,3.45 AAACC,100.0,3.45 AAACG,101.01,3.45 AAACT,106.91,3.45 AAAGA,110.54,4.06 AAAGC,107.69,4.06 AAAGG,108.29,4.06 AAAGT,108.73,4.06 AAATA,114.11,3.11 AAATC,112.2,3.11 AAATG,110.67,3.11 AAATT,115.98,3.11 AACAA,87.82,3.18 AACAC,89.76,3.18 AACAG,86.54,3.18 AACAT,87.88,3.18 AACCA,82.92,2.81 AACCC,84.52,2.81 AACCG,84.37,2.81 AACCT,84.27,2.81 AACGA,79.83,3.18 AACGC,80.65,3.18 AACGG,83.67,3.18 AACGT,80.07,3.18 AACTA,92.91,2.73 AACTC,92.39,2.73 AACTG,92.99,2.73 AACTT,92.87,2.73 AAGAA,122.14,5.87 AAGAC,121.03,5.87 AAGAG,122.1,5.87 AAGAT,124.17,5.87 AAGCA,107.34,3.02 AAGCC,102.84,3.02 AAGCG,109.95,3.02 AAGCT,110.98,3.02 AAGGA,114.01,7.84 AAGGC,115.2,7.84 AAGGG,113.12,7.84 AAGGT,115.61,7.84 AAGTA,119.31,4.03 AAGTC,114.94,4.03 AAGTG,117.29,4.03 AAGTT,119.41,4.03 AATAA,97.17,6.28 AATAC,102.58,6.28 AATAG,99.74,6.28 AATAT,99.12,6.28 AATCA,100.24,5.7 AATCC,103.25,5.7 AATCG,101.47,5.7 AATCT,101.61,5.7 AATGA,85.26,3.87 AATGC,81.8,3.87 AATGG,89.31,3.87 AATGT,82.19,3.87 AATTA,101.02,5.53 AATTC,101.9,5.53 AATTG,100.78,5.53 AATTT,100.95,5.53 ACAAA,82.45,3.02 ACAAC,80.96,3.02 ACAAG,78.46,3.02 ACAAT,84.13,3.02 ACACA,73.59,1.88 ACACC,72.16,1.88 ACACG,74.89,1.88 ACACT,77.76,1.88 ACAGA,96.56,5.09 ACAGC,87.79,5.09 ACAGG,84.2,5.09 ACAGT,91.62,5.09 ACATA,80.76,2.13 ACATC,78.74,2.13 ACATG,79.18,2.13 ACATT,84.22,2.13 ACCAA,74.58,2.11 ACCAC,73.95,2.11 ACCAG,72.33,2.11 ACCAT,74.74,2.11 ACCCA,66.3,2.07 ACCCC,66.66,2.07 ACCCG,67.21,2.07 ACCCT,68.1,2.07 ACCGA,82.82,2.78 ACCGC,77.16,2.78 ACCGG,76.54,2.78 ACCGT,77.73,2.78 ACCTA,69.02,2.35 ACCTC,69.47,2.35 ACCTG,70.75,2.35 ACCTT,72.21,2.35 ACGAA,85.74,3.99 ACGAC,85.84,3.99 ACGAG,84.3,3.99 ACGAT,87.51,3.99 ACGCA,76.33,2.6 ACGCC,74.77,2.6 ACGCG,77.12,2.6 ACGCT,79.97,2.6 ACGGA,96.56,4.98 ACGGC,85.58,4.98 ACGGG,83.39,4.98 ACGGT,89.18,4.98 ACGTA,101.5,3.05 ACGTC,78.18,3.05 ACGTG,78.19,3.05 ACGTT,82.52,3.05 ACTAA,86.71,2.63 ACTAC,86.58,2.63 ACTAG,86.1,2.63 ACTAT,86.6,2.63 ACTCA,81.85,2.85 ACTCC,80.74,2.85 ACTCG,80.88,2.85 ACTCT,83.72,2.85 ACTGA,93.41,2.85 ACTGC,86.11,2.85 ACTGG,87.12,2.85 ACTGT,87.17,2.85 ACTTA,77.86,2.42 ACTTC,80.34,2.42 ACTTG,80.1,2.42 ACTTT,82.38,2.42 AGAAA,128.13,5.56 AGAAC,128.77,5.56 AGAAG,123.66,5.56 AGAAT,129.86,5.56 AGACA,125.56,4.79 AGACC,125.6,4.79 AGACG,127.32,4.79 AGACT,129.81,4.79 AGAGA,127.71,5.69 AGAGC,128.62,5.69 AGAGG,123.33,5.69 AGAGT,128.25,5.69 AGATA,134.1,5.1 AGATC,134.08,5.1 AGATG,133.6,5.1 AGATT,136.89,5.1 AGCAA,115.2,3.86 AGCAC,117.19,3.86 AGCAG,112.17,3.86 AGCAT,114.89,3.86 AGCCA,109.32,3.29 AGCCC,111.18,3.29 AGCCG,109.95,3.29 AGCCT,111.21,3.29 AGCGA,105.69,8.39 AGCGC,110.34,8.39 AGCGG,106.83,8.39 AGCGT,107.96,8.39 AGCTA,117.18,3.55 AGCTC,118.06,3.55 AGCTG,117.44,3.55 AGCTT,118.19,3.55 AGGAA,117.46,3.17 AGGAC,115.92,3.17 AGGAG,116.69,3.17 AGGAT,120.37,3.17 AGGCA,109.74,5.31 AGGCC,109.16,5.31 AGGCG,112.24,5.31 AGGCT,120.31,5.31 AGGGA,115.88,4.05 AGGGC,116.4,4.05 AGGGG,113.61,4.05 AGGGT,117.28,4.05 AGGTA,119.51,3.37 AGGTC,116.55,3.37 AGGTG,117.25,3.37 AGGTT,121.11,3.37 AGTAA,123.3,8.56 AGTAC,128.3,8.56 AGTAG,122.89,8.56 AGTAT,124.93,8.56 AGTCA,122.59,6.07 AGTCC,125.2,6.07 AGTCG,122.92,6.07 AGTCT,125.26,6.07 AGTGA,100.01,9.69 AGTGC,110.68,9.69 AGTGG,111.73,9.69 AGTGT,105.15,9.69 AGTTA,118.77,7.49 AGTTC,123.87,7.49 AGTTG,121.11,7.49 AGTTT,124.34,7.49 ATAAA,86.59,3.04 ATAAC,85.4,3.04 ATAAG,82.49,3.04 ATAAT,89.36,3.04 ATACA,76.78,2.1 ATACC,74.69,2.1 ATACG,77.77,2.1 ATACT,81.8,2.1 ATAGA,103.91,3.78 ATAGC,91.22,3.78 ATAGG,85.79,3.78 ATAGT,93.79,3.78 ATATA,86.67,2.63 ATATC,83.15,2.63 ATATG,83.95,2.63 ATATT,90.49,2.63 ATCAA,78.88,2.3 ATCAC,78.57,2.3 ATCAG,76.74,2.3 ATCAT,79.2,2.3 ATCCA,70.23,2.3 ATCCC,70.32,2.3 ATCCG,71.19,2.3 ATCCT,73.54,2.3 ATCGA,84.43,2.3 ATCGC,79.59,2.3 ATCGG,79.89,2.3 ATCGT,80.62,2.3 ATCTA,76.67,2.07 ATCTC,76.56,2.07 ATCTG,77.11,2.07 ATCTT,78.87,2.07 ATGAA,94.58,4.49 ATGAC,94.31,4.49 ATGAG,93.1,4.49 ATGAT,97.78,4.49 ATGCA,84.45,3.13 ATGCC,79.66,3.13 ATGCG,83.14,3.13 ATGCT,86.67,3.13 ATGGA,100.76,3.99 ATGGC,94.08,3.99 ATGGG,92.94,3.99 ATGGT,96.35,3.99 ATGTA,88.61,3.78 ATGTC,85.93,3.78 ATGTG,87.08,3.78 ATGTT,89.59,3.78 ATTAA,85.3,2.46 ATTAC,85.21,2.46 ATTAG,84.56,2.46 ATTAT,85.44,2.46 ATTCA,78.44,2.07 ATTCC,78.05,2.07 ATTCG,78.54,2.07 ATTCT,81.64,2.07 ATTGA,90.37,2.65 ATTGC,83.88,2.65 ATTGG,86.04,2.65 ATTGT,85.12,2.65 ATTTA,77.58,1.97 ATTTC,78.37,1.97 ATTTG,79.1,1.97 ATTTT,81.16,1.97 CAAAA,105.72,2.68 CAAAC,104.22,2.68 CAAAG,103.0,2.68 CAAAT,110.07,2.68 CAACA,91.38,3.45 CAACC,89.37,3.45 CAACG,91.91,3.45 CAACT,96.11,3.45 CAAGA,110.39,4.06 CAAGC,102.91,4.06 CAAGG,106.17,4.06 CAAGT,105.12,4.06 CAATA,109.26,3.11 CAATC,106.21,3.11 CAATG,102.74,3.11 CAATT,108.91,3.11 CACAA,81.77,3.18 CACAC,81.04,3.18 CACAG,81.43,3.18 CACAT,81.25,3.18 CACCA,75.53,2.81 CACCC,75.11,2.81 CACCG,76.38,2.81 CACCT,75.57,2.81 CACGA,78.73,3.18 CACGC,78.14,3.18 CACGG,79.72,3.18 CACGT,80.28,3.18 CACTA,86.86,2.73 CACTC,85.31,2.73 CACTG,85.74,2.73 CACTT,85.07,2.73 CAGAA,108.6,5.87 CAGAC,107.09,5.87 CAGAG,106.73,5.87 CAGAT,112.38,5.87 CAGCA,108.25,3.02 CAGCC,103.97,3.02 CAGCG,109.19,3.02 CAGCT,112.05,3.02 CAGGA,98.38,7.84 CAGGC,114.13,7.84 CAGGG,89.17,7.84 CAGGT,115.66,7.84 CAGTA,123.5,4.03 CAGTC,118.3,4.03 CAGTG,119.53,4.03 CAGTT,122.77,4.03 CATAA,89.9,6.28 CATAC,90.48,6.28 CATAG,91.26,6.28 CATAT,89.58,6.28 CATCA,87.17,5.7 CATCC,87.39,5.7 CATCG,88.22,5.7 CATCT,88.36,5.7 CATGA,84.29,3.87 CATGC,78.23,3.87 CATGG,85.73,3.87 CATGT,79.73,3.87 CATTA,90.58,5.53 CATTC,89.92,5.53 CATTG,90.61,5.53 CATTT,89.28,5.53 CCAAA,87.19,3.02 CCAAC,85.16,3.02 CCAAG,86.1,3.02 CCAAT,89.46,3.02 CCACA,75.6,1.88 CCACC,72.58,1.88 CCACG,75.29,1.88 CCACT,77.89,1.88 CCAGA,76.22,5.09 CCAGC,92.88,5.09 CCAGG,91.8,5.09 CCAGT,96.02,5.09 CCATA,86.44,2.13 CCATC,83.88,2.13 CCATG,82.78,2.13 CCATT,87.39,2.13 CCCAA,73.43,2.11 CCCAC,70.73,2.11 CCCAG,71.95,2.11 CCCAT,73.36,2.11 CCCCA,62.97,2.07 CCCCC,62.48,2.07 CCCCG,63.75,2.07 CCCCT,64.58,2.07 CCCGA,83.92,2.78 CCCGC,76.79,2.78 CCCGG,78.03,2.78 CCCGT,78.01,2.78 CCCTA,69.67,2.35 CCCTC,68.73,2.35 CCCTG,71.64,2.35 CCCTT,70.58,2.35 CCGAA,97.55,3.99 CCGAC,96.66,3.99 CCGAG,95.86,3.99 CCGAT,100.63,3.99 CCGCA,87.11,2.6 CCGCC,84.08,2.6 CCGCG,87.09,2.6 CCGCT,89.39,2.6 CCGGA,106.16,4.98 CCGGC,95.13,4.98 CCGGG,92.99,4.98 CCGGT,98.1,4.98 CCGTA,92.47,3.05 CCGTC,89.27,3.05 CCGTG,90.05,3.05 CCGTT,92.47,3.05 CCTAA,82.46,2.63 CCTAC,79.05,2.63 CCTAG,83.95,2.63 CCTAT,82.63,2.63 CCTCA,74.24,2.85 CCTCC,72.35,2.85 CCTCG,75.01,2.85 CCTCT,75.31,2.85 CCTGA,94.04,2.85 CCTGC,84.2,2.85 CCTGG,88.24,2.85 CCTGT,86.87,2.85 CCTTA,75.8,2.42 CCTTC,75.32,2.42 CCTTG,77.66,2.42 CCTTT,76.37,2.42 CGAAA,112.59,5.56 CGAAC,111.47,5.56 CGAAG,109.77,5.56 CGAAT,115.65,5.56 CGACA,109.27,4.79 CGACC,110.09,4.79 CGACG,110.92,4.79 CGACT,113.97,4.79 CGAGA,116.16,5.69 CGAGC,110.68,5.69 CGAGG,111.04,5.69 CGAGT,112.74,5.69 CGATA,117.34,5.1 CGATC,117.72,5.1 CGATG,114.13,5.1 CGATT,120.28,5.1 CGCAA,99.45,3.86 CGCAC,101.88,3.86 CGCAG,97.16,3.86 CGCAT,99.57,3.86 CGCCA,94.99,3.29 CGCCC,96.31,3.29 CGCCG,95.61,3.29 CGCCT,96.76,3.29 CGCGA,90.74,8.39 CGCGC,90.61,8.39 CGCGG,92.98,8.39 CGCGT,90.72,8.39 CGCTA,102.06,3.55 CGCTC,102.45,3.55 CGCTG,101.74,3.55 CGCTT,102.11,3.55 CGGAA,123.97,3.17 CGGAC,120.9,3.17 CGGAG,122.92,3.17 CGGAT,128.82,3.17 CGGCA,106.6,5.31 CGGCC,103.67,5.31 CGGCG,107.83,5.31 CGGCT,110.54,5.31 CGGGA,115.56,4.05 CGGGC,113.74,4.05 CGGGG,112.66,4.05 CGGGT,113.91,4.05 CGGTA,121.85,3.37 CGGTC,117.18,3.37 CGGTG,117.99,3.37 CGGTT,120.77,3.37 CGTAA,98.86,8.56 CGTAC,102.95,8.56 CGTAG,98.64,8.56 CGTAT,101.57,8.56 CGTCA,99.9,6.07 CGTCC,102.21,6.07 CGTCG,100.2,6.07 CGTCT,102.03,6.07 CGTGA,93.87,9.69 CGTGC,89.89,9.69 CGTGG,94.81,9.69 CGTGT,91.66,9.69 CGTTA,98.21,7.49 CGTTC,100.91,7.49 CGTTG,97.81,7.49 CGTTT,99.9,7.49 CTAAA,94.0,3.04 CTAAC,91.5,3.04 CTAAG,91.48,3.04 CTAAT,96.7,3.04 CTACA,80.29,2.1 CTACC,76.5,2.1 CTACG,80.58,2.1 CTACT,83.47,2.1 CTAGA,107.08,3.78 CTAGC,97.3,3.78 CTAGG,96.48,3.78 CTAGT,99.59,3.78 CTATA,94.69,2.63 CTATC,91.53,2.63 CTATG,90.28,2.63 CTATT,95.55,2.63 CTCAA,79.13,2.3 CTCAC,76.73,2.3 CTCAG,77.48,2.3 CTCAT,79.74,2.3 CTCCA,69.76,2.3 CTCCC,67.63,2.3 CTCCG,70.39,2.3 CTCCT,71.32,2.3 CTCGA,86.23,2.3 CTCGC,79.79,2.3 CTCGG,82.39,2.3 CTCGT,81.94,2.3 CTCTA,79.85,2.07 CTCTC,77.34,2.07 CTCTG,79.0,2.07 CTCTT,79.01,2.07 CTGAA,107.93,4.49 CTGAC,107.16,4.49 CTGAG,106.04,4.49 CTGAT,111.64,4.49 CTGCA,94.88,3.13 CTGCC,90.93,3.13 CTGCG,94.04,3.13 CTGCT,96.87,3.13 CTGGA,108.35,3.99 CTGGC,102.61,3.99 CTGGG,101.64,3.99 CTGGT,105.22,3.99 CTGTA,102.06,3.78 CTGTC,98.48,3.78 CTGTG,99.73,3.78 CTGTT,102.18,3.78 CTTAA,84.32,2.46 CTTAC,81.76,2.46 CTTAG,85.06,2.46 CTTAT,84.65,2.46 CTTCA,77.66,2.07 CTTCC,74.22,2.07 CTTCG,78.03,2.07 CTTCT,78.04,2.07 CTTGA,91.8,2.65 CTTGC,84.55,2.65 CTTGG,88.3,2.65 CTTGT,86.77,2.65 CTTTA,79.44,1.97 CTTTC,77.72,1.97 CTTTG,80.36,1.97 CTTTT,79.71,1.97 GAAAA,106.42,2.68 GAAAC,105.92,2.68 GAAAG,103.81,2.68 GAAAT,111.12,2.68 GAACA,95.52,3.45 GAACC,94.7,3.45 GAACG,96.74,3.45 GAACT,100.79,3.45 GAAGA,105.36,4.06 GAAGC,100.44,4.06 GAAGG,105.26,4.06 GAAGT,103.99,4.06 GAATA,111.54,3.11 GAATC,109.54,3.11 GAATG,107.51,3.11 GAATT,112.11,3.11 GACAA,80.85,3.18 GACAC,84.44,3.18 GACAG,80.99,3.18 GACAT,82.61,3.18 GACCA,79.02,2.81 GACCC,79.18,2.81 GACCG,79.37,2.81 GACCT,79.9,2.81 GACGA,75.7,3.18 GACGC,73.05,3.18 GACGG,77.57,3.18 GACGT,75.25,3.18 GACTA,90.24,2.73 GACTC,88.67,2.73 GACTG,88.63,2.73 GACTT,89.12,2.73 GAGAA,125.76,5.87 GAGAC,123.32,5.87 GAGAG,124.52,5.87 GAGAT,130.18,5.87 GAGCA,107.01,3.02 GAGCC,103.08,3.02 GAGCG,109.62,3.02 GAGCT,112.01,3.02 GAGGA,112.67,7.84 GAGGC,113.94,7.84 GAGGG,115.25,7.84 GAGGT,112.28,7.84 GAGTA,123.38,4.03 GAGTC,117.52,4.03 GAGTG,120.91,4.03 GAGTT,123.9,4.03 GATAA,88.34,6.28 GATAC,94.91,6.28 GATAG,90.96,6.28 GATAT,88.95,6.28 GATCA,93.45,5.7 GATCC,95.14,5.7 GATCG,95.15,5.7 GATCT,95.38,5.7 GATGA,79.44,3.87 GATGC,75.42,3.87 GATGG,82.94,3.87 GATGT,76.84,3.87 GATTA,96.01,5.53 GATTC,96.04,5.53 GATTG,96.1,5.53 GATTT,94.71,5.53 GCAAA,84.46,3.02 GCAAC,82.48,3.02 GCAAG,81.86,3.02 GCAAT,86.3,3.02 GCACA,73.15,1.88 GCACC,70.72,1.88 GCACG,73.84,1.88 GCACT,77.18,1.88 GCAGA,99.93,5.09 GCAGC,89.55,5.09 GCAGG,87.86,5.09 GCAGT,91.46,5.09 GCATA,82.81,2.13 GCATC,80.54,2.13 GCATG,80.71,2.13 GCATT,85.12,2.13 GCCAA,73.26,2.11 GCCAC,71.67,2.11 GCCAG,71.08,2.11 GCCAT,73.7,2.11 GCCCA,64.63,2.07 GCCCC,64.85,2.07 GCCCG,65.64,2.07 GCCCT,66.35,2.07 GCCGA,81.39,2.78 GCCGC,76.26,2.78 GCCGG,75.73,2.78 GCCGT,76.98,2.78 GCCTA,68.43,2.35 GCCTC,67.99,2.35 GCCTG,69.96,2.35 GCCTT,70.99,2.35 GCGAA,92.59,3.99 GCGAC,91.21,3.99 GCGAG,90.14,3.99 GCGAT,95.9,3.99 GCGCA,80.89,2.6 GCGCC,78.75,2.6 GCGCG,80.59,2.6 GCGCT,83.21,2.6 GCGGA,100.67,4.98 GCGGC,90.29,4.98 GCGGG,87.6,4.98 GCGGT,92.74,4.98 GCGTA,84.85,3.05 GCGTC,82.78,3.05 GCGTG,83.37,3.05 GCGTT,86.64,3.05 GCTAA,84.4,2.63 GCTAC,85.34,2.63 GCTAG,84.44,2.63 GCTAT,84.83,2.63 GCTCA,76.97,2.85 GCTCC,78.88,2.85 GCTCG,78.06,2.85 GCTCT,80.1,2.85 GCTGA,89.96,2.85 GCTGC,83.43,2.85 GCTGG,85.53,2.85 GCTGT,84.35,2.85 GCTTA,77.6,2.42 GCTTC,77.98,2.42 GCTTG,79.57,2.42 GCTTT,80.14,2.42 GGAAA,121.47,5.56 GGAAC,121.88,5.56 GGAAG,115.76,5.56 GGAAT,123.5,5.56 GGACA,118.4,4.79 GGACC,119.23,4.79 GGACG,120.92,4.79 GGACT,123.83,4.79 GGAGA,119.43,5.69 GGAGC,121.19,5.69 GGAGG,114.43,5.69 GGAGT,117.27,5.69 GGATA,126.88,5.1 GGATC,127.17,5.1 GGATG,126.02,5.1 GGATT,129.83,5.1 GGCAA,108.94,3.86 GGCAC,111.25,3.86 GGCAG,105.66,3.86 GGCAT,109.07,3.86 GGCCA,103.13,3.29 GGCCC,105.11,3.29 GGCCG,104.04,3.29 GGCCT,105.48,3.29 GGCGA,92.44,8.39 GGCGC,100.1,8.39 GGCGG,98.79,8.39 GGCGT,95.66,8.39 GGCTA,110.69,3.55 GGCTC,111.33,3.55 GGCTG,111.28,3.55 GGCTT,112.47,3.55 GGGAA,120.77,3.17 GGGAC,118.29,3.17 GGGAG,120.29,3.17 GGGAT,124.02,3.17 GGGCA,106.36,5.31 GGGCC,104.1,5.31 GGGCG,108.23,5.31 GGGCT,113.28,5.31 GGGGA,114.8,4.05 GGGGC,113.92,4.05 GGGGG,110.71,4.05 GGGGT,113.9,4.05 GGGTA,119.34,3.37 GGGTC,114.73,3.37 GGGTG,115.9,3.37 GGGTT,119.33,3.37 GGTAA,113.29,8.56 GGTAC,118.69,8.56 GGTAG,107.54,8.56 GGTAT,114.71,8.56 GGTCA,112.91,6.07 GGTCC,116.09,6.07 GGTCG,115.68,6.07 GGTCT,117.09,6.07 GGTGA,94.59,9.69 GGTGC,93.69,9.69 GGTGG,99.47,9.69 GGTGT,93.04,9.69 GGTTA,109.62,7.49 GGTTC,114.37,7.49 GGTTG,109.94,7.49 GGTTT,112.5,7.49 GTAAA,88.78,3.04 GTAAC,87.41,3.04 GTAAG,85.91,3.04 GTAAT,91.85,3.04 GTACA,76.34,2.1 GTACC,73.99,2.1 GTACG,77.41,2.1 GTACT,81.13,2.1 GTAGA,102.41,3.78 GTAGC,92.91,3.78 GTAGG,113.51,3.78 GTAGT,93.94,3.78 GTATA,89.66,2.63 GTATC,87.0,2.63 GTATG,86.71,2.63 GTATT,91.86,2.63 GTCAA,77.56,2.3 GTCAC,76.2,2.3 GTCAG,75.63,2.3 GTCAT,78.48,2.3 GTCCA,68.75,2.3 GTCCC,68.27,2.3 GTCCG,69.69,2.3 GTCCT,70.25,2.3 GTCGA,83.26,2.3 GTCGC,78.62,2.3 GTCGG,79.12,2.3 GTCGT,79.78,2.3 GTCTA,77.22,2.07 GTCTC,76.09,2.07 GTCTG,77.44,2.07 GTCTT,77.91,2.07 GTGAA,101.5,4.49 GTGAC,99.69,4.49 GTGAG,99.33,4.49 GTGAT,105.22,4.49 GTGCA,87.07,3.13 GTGCC,83.63,3.13 GTGCG,87.12,3.13 GTGCT,90.01,3.13 GTGGA,103.6,3.99 GTGGC,96.91,3.99 GTGGG,94.43,3.99 GTGGT,98.9,3.99 GTGTA,93.81,3.78 GTGTC,90.41,3.78 GTGTG,90.87,3.78 GTGTT,95.08,3.78 GTTAA,83.14,2.46 GTTAC,82.19,2.46 GTTAG,82.23,2.46 GTTAT,83.69,2.46 GTTCA,76.78,2.07 GTTCC,75.95,2.07 GTTCG,76.91,2.07 GTTCT,77.38,2.07 GTTGA,88.48,2.65 GTTGC,81.95,2.65 GTTGG,84.7,2.65 GTTGT,83.62,2.65 GTTTA,77.98,1.97 GTTTC,77.61,1.97 GTTTG,78.97,1.97 GTTTT,78.7,1.97 NNNNN,0.0,0.0 TAAAA,104.53,2.68 TAAAC,103.45,2.68 TAAAG,102.28,2.68 TAAAT,108.51,2.68 TAACA,93.83,3.45 TAACC,93.34,3.45 TAACG,95.3,3.45 TAACT,99.07,3.45 TAAGA,107.76,4.06 TAAGC,101.58,4.06 TAAGG,103.92,4.06 TAAGT,104.1,4.06 TAATA,108.46,3.11 TAATC,106.55,3.11 TAATG,104.66,3.11 TAATT,109.83,3.11 TACAA,83.04,3.18 TACAC,84.22,3.18 TACAG,79.55,3.18 TACAT,83.15,3.18 TACCA,77.84,2.81 TACCC,78.84,2.81 TACCG,78.27,2.81 TACCT,79.02,2.81 TACGA,79.56,3.18 TACGC,78.11,3.18 TACGG,80.13,3.18 TACGT,79.24,3.18 TACTA,87.98,2.73 TACTC,87.81,2.73 TACTG,87.79,2.73 TACTT,88.1,2.73 TAGAA,122.17,5.87 TAGAC,120.42,5.87 TAGAG,121.13,5.87 TAGAT,126.66,5.87 TAGCA,105.36,3.02 TAGCC,101.17,3.02 TAGCG,105.73,3.02 TAGCT,109.43,3.02 TAGGA,100.23,7.84 TAGGC,93.67,7.84 TAGGG,89.44,7.84 TAGGT,96.83,7.84 TAGTA,118.82,4.03 TAGTC,113.73,4.03 TAGTG,115.89,4.03 TAGTT,118.67,4.03 TATAA,92.84,6.28 TATAC,95.09,6.28 TATAG,92.78,6.28 TATAT,93.55,6.28 TATCA,92.76,5.7 TATCC,94.26,5.7 TATCG,94.1,5.7 TATCT,94.81,5.7 TATGA,82.98,3.87 TATGC,78.95,3.87 TATGG,86.36,3.87 TATGT,80.2,3.87 TATTA,94.35,5.53 TATTC,95.06,5.53 TATTG,94.79,5.53 TATTT,94.78,5.53 TCAAA,87.61,3.02 TCAAC,85.79,3.02 TCAAG,85.7,3.02 TCAAT,89.2,3.02 TCACA,75.59,1.88 TCACC,74.44,1.88 TCACG,75.82,1.88 TCACT,79.43,1.88 TCAGA,100.98,5.09 TCAGC,90.48,5.09 TCAGG,89.11,5.09 TCAGT,93.14,5.09 TCATA,87.27,2.13 TCATC,85.07,2.13 TCATG,83.81,2.13 TCATT,88.36,2.13 TCCAA,74.33,2.11 TCCAC,72.87,2.11 TCCAG,72.52,2.11 TCCAT,74.37,2.11 TCCCA,64.83,2.07 TCCCC,64.94,2.07 TCCCG,65.58,2.07 TCCCT,66.26,2.07 TCCGA,81.88,2.78 TCCGC,76.6,2.78 TCCGG,76.57,2.78 TCCGT,77.04,2.78 TCCTA,70.14,2.35 TCCTC,70.83,2.35 TCCTG,71.64,2.35 TCCTT,73.28,2.35 TCGAA,96.87,3.99 TCGAC,96.27,3.99 TCGAG,94.81,3.99 TCGAT,99.04,3.99 TCGCA,87.17,2.6 TCGCC,84.43,2.6 TCGCG,86.83,2.6 TCGCT,88.71,2.6 TCGGA,102.03,4.98 TCGGC,92.92,4.98 TCGGG,90.95,4.98 TCGGT,95.29,4.98 TCGTA,91.75,3.05 TCGTC,89.2,3.05 TCGTG,89.97,3.05 TCGTT,92.1,3.05 TCTAA,84.15,2.63 TCTAC,83.06,2.63 TCTAG,84.05,2.63 TCTAT,83.97,2.63 TCTCA,76.05,2.85 TCTCC,75.36,2.85 TCTCG,78.42,2.85 TCTCT,79.44,2.85 TCTGA,91.37,2.85 TCTGC,83.9,2.85 TCTGG,86.5,2.85 TCTGT,84.95,2.85 TCTTA,77.86,2.42 TCTTC,76.97,2.42 TCTTG,80.89,2.42 TCTTT,80.39,2.42 TGAAA,118.11,5.56 TGAAC,118.66,5.56 TGAAG,114.75,5.56 TGAAT,120.76,5.56 TGACA,117.05,4.79 TGACC,118.44,4.79 TGACG,118.74,4.79 TGACT,121.9,4.79 TGAGA,117.69,5.69 TGAGC,117.86,5.69 TGAGG,114.26,5.69 TGAGT,116.26,5.69 TGATA,124.24,5.1 TGATC,124.55,5.1 TGATG,123.13,5.1 TGATT,127.73,5.1 TGCAA,106.02,3.86 TGCAC,108.56,3.86 TGCAG,103.32,3.86 TGCAT,106.11,3.86 TGCCA,101.35,3.29 TGCCC,102.95,3.29 TGCCG,102.07,3.29 TGCCT,102.97,3.29 TGCGA,94.55,8.39 TGCGC,98.58,8.39 TGCGG,98.67,8.39 TGCGT,96.64,8.39 TGCTA,107.94,3.55 TGCTC,108.76,3.55 TGCTG,108.07,3.55 TGCTT,109.23,3.55 TGGAA,120.39,3.17 TGGAC,118.05,3.17 TGGAG,120.17,3.17 TGGAT,125.17,3.17 TGGCA,104.83,5.31 TGGCC,103.76,5.31 TGGCG,107.59,5.31 TGGCT,108.63,5.31 TGGGA,113.62,4.05 TGGGC,113.12,4.05 TGGGG,111.57,4.05 TGGGT,112.2,4.05 TGGTA,118.63,3.37 TGGTC,114.41,3.37 TGGTG,115.66,3.37 TGGTT,118.83,3.37 TGTAA,109.08,8.56 TGTAC,113.99,8.56 TGTAG,106.93,8.56 TGTAT,110.15,8.56 TGTCA,109.11,6.07 TGTCC,112.01,6.07 TGTCG,110.08,6.07 TGTCT,111.93,6.07 TGTGA,94.9,9.69 TGTGC,95.27,9.69 TGTGG,99.41,9.69 TGTGT,93.18,9.69 TGTTA,106.43,7.49 TGTTC,109.7,7.49 TGTTG,107.05,7.49 TGTTT,109.49,7.49 TTAAA,92.05,3.04 TTAAC,90.31,3.04 TTAAG,89.64,3.04 TTAAT,94.22,3.04 TTACA,79.88,2.1 TTACC,76.73,2.1 TTACG,80.27,2.1 TTACT,84.2,2.1 TTAGA,103.76,3.78 TTAGC,94.88,3.78 TTAGG,92.38,3.78 TTAGT,95.99,3.78 TTATA,93.29,2.63 TTATC,90.32,2.63 TTATG,89.8,2.63 TTATT,94.2,2.63 TTCAA,80.1,2.3 TTCAC,77.15,2.3 TTCAG,75.46,2.3 TTCAT,80.43,2.3 TTCCA,70.43,2.3 TTCCC,68.77,2.3 TTCCG,70.81,2.3 TTCCT,72.03,2.3 TTCGA,84.7,2.3 TTCGC,80.79,2.3 TTCGG,81.45,2.3 TTCGT,81.56,2.3 TTCTA,79.45,2.07 TTCTC,77.3,2.07 TTCTG,79.59,2.07 TTCTT,79.33,2.07 TTGAA,104.82,4.49 TTGAC,104.2,4.49 TTGAG,102.99,4.49 TTGAT,108.32,4.49 TTGCA,92.63,3.13 TTGCC,89.55,3.13 TTGCG,92.52,3.13 TTGCT,94.95,3.13 TTGGA,105.73,3.99 TTGGC,100.45,3.99 TTGGG,98.91,3.99 TTGGT,102.47,3.99 TTGTA,99.35,3.78 TTGTC,96.28,3.78 TTGTG,97.14,3.78 TTGTT,99.72,3.78 TTTAA,84.44,2.46 TTTAC,82.79,2.46 TTTAG,83.95,2.46 TTTAT,84.61,2.46 TTTCA,78.13,2.07 TTTCC,75.29,2.07 TTTCG,79.07,2.07 TTTCT,79.59,2.07 TTTGA,89.94,2.65 TTTGC,83.8,2.65 TTTGG,86.61,2.65 TTTGT,85.15,2.65 TTTTA,79.28,1.97 TTTTC,77.91,1.97 TTTTG,81.03,1.97 TTTTT,80.78,1.97 xpore-2.1/xpore/diffmod/configurator.py0000644000175000017500000000527114130107110017651 0ustar nileshnileshimport yaml import os from collections import defaultdict from ..utils import misc def get_condition_run_name(condition_name,run_name): return '-'.join([condition_name,run_name]) class Configurator(object): def __init__(self, config_filepath): self.filepath = os.path.abspath(config_filepath) self.filename = self.filepath.split('/')[-1] self.yaml = yaml.safe_load(open(self.filepath, 'r')) def get_paths(self): paths = {} if 'prior' in self.yaml: paths['model_kmer'] = os.path.abspath(self.yaml['prior']) else: paths['model_kmer'] = os.path.join(os.path.dirname(__file__),'model_kmer.csv') paths['out_dir'] = os.path.join(os.path.abspath(self.yaml['out'])) paths.update(misc.makedirs(paths['out_dir'],sub_dirs=['models'])) paths['model_filepath'] = os.path.join(paths['out_dir'], 'models', '%s.model') return paths def get_data_info(self): data = defaultdict(dict) for condition_name, run_names in self.yaml['data'].items(): for run_name, dirpath in run_names.items(): data[condition_name][get_condition_run_name(condition_name,run_name)] = dirpath return data def get_criteria(self): criteria = {} if 'criteria' in self.yaml.keys(): criteria = self.yaml['criteria'] else: criteria['readcount_min'] = 15 criteria['readcount_max'] = 1000 return criteria def get_method(self): if 'method' in self.yaml.keys(): method = self.yaml['method'] else: method = {} method.setdefault('name', 'gmm') method.setdefault('max_iters', 500) method.setdefault('stopping_criteria', 0.00001) method.setdefault('compute_elbo', True) method.setdefault('verbose', False) method.setdefault('update', ['z','y','w','mu_tau']) method.setdefault('pooling', False) method.setdefault('prefiltering',False) return method def get_priors(self): prior_params = defaultdict(dict) if 'priors' not in self.yaml.keys(): # mu_tau prior_params['mu_tau']['location'] = ['model_kmer_mean','model_kmer_mean'] prior_params['mu_tau']['lambda'] = [1,1] prior_params['mu_tau']['alpha'] = [0.5,0.5] prior_params['mu_tau']['beta'] = ['model_kmer_tau','model_kmer_tau'] prior_params['mu_tau']['beta_scale'] = [0.5,0.5] # w prior_params['w']['concentration'] = [0.001,0.001] else: pass #todo return prior_params xpore-2.1/xpore/diffmod/__init__.py0000644000175000017500000000000014130107110016667 0ustar nileshnileshxpore-2.1/xpore/__init__.py0000644000175000017500000000002414130107110015265 0ustar nileshnilesh__version__ = "2.0" xpore-2.1/docs/0000755000175000017500000000000014130107110012753 5ustar nileshnileshxpore-2.1/docs/make.bat0000644000175000017500000000145514130107110014365 0ustar nileshnilesh@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build set SPHINXPROJ=xpore if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd xpore-2.1/docs/source/0000755000175000017500000000000014130107110014253 5ustar nileshnileshxpore-2.1/docs/source/cmd.rst0000644000175000017500000001373414130107110015560 0ustar nileshnilesh.. _cmd: Command line arguments ======================= We provide 2 main scripts to run the analysis of differential RNA modifications as the following. ``xpore-dataprep`` ******************** * Input Output files from ``nanopolish eventalgin``. Please refer to :ref:`Data preparation ` for the full Nanopolish command. ================================= ========== =================== ============================================================================================================ Argument name Required Default value Description ================================= ========== =================== ============================================================================================================ --eventalign=FILE Yes NA Eventalign filepath, the output from nanopolish. --out_dir=DIR Yes NA Output directory. --gtf_path_or_url No NA GTF file path or url used for mapping transcriptomic to genomic coordinates. --transcript_fasta_paths_or_urls No NA Transcript FASTA paths or urls used for mapping transcriptomic to genomic coordinates. --skip_eventalign_indexing No False To skip indexing the eventalign nanopolish output. --genome No False To run on Genomic coordinates. Without this argument, the program will run on transcriptomic coordinates. --n_processes=NUM No 1 Number of processes to run. --readcount_max=NUM No 1000 Maximum read counts per gene. --readcount_min=NUM No 1 Minimum read counts per gene. --resume No False With this argument, the program will resume from the previous run. ================================= ========== =================== ============================================================================================================ * Output ====================== ============== =============================================================================================================================================================== File name File type Description ====================== ============== =============================================================================================================================================================== eventalign.index csv File index indicating the position in the `eventalign.txt` file (the output of nanopolish eventalign) where the segmentation information of each read index is stored, allowing a random access. data.json json Intensity level mean for each position. data.index csv File index indicating the position in the `data.json` file where the intensity level means across positions of each gene is stored, allowing a random access. data.log txt Gene ids being processed. data.readcount csv Summary of readcounts per gene. ====================== ============== =============================================================================================================================================================== ``xpore-diffmod`` ****************** * Input Output files from ``xpore-dataprep``. =================== ========== =============== ============================================================================== Argument name Required Default value Description =================== ========== =============== ============================================================================== --config=FILE Yes NA YAML configurtaion filepath. --n_processes=NUM No 1 Number of processes to run. --save_models No False With this argument, the program will save the model parameters for each id. --resume No False With this argument, the program will resume from the previous run. --ids=LIST No [] Gene / Transcript ids to model. =================== ========== =============== ============================================================================== * Output ====================== =============== ================================================================================================================================================= File name File type Description ====================== =============== ================================================================================================================================================= diffmod.table csv Output table information of differential modification rates. Please refer to :ref:`Output table description ` for the full description. diffmod.log txt Gene/Transcript ids being processed. ====================== =============== ================================================================================================================================================= ``xpore-postprocessing`` ************************** * Input The ``diffmod.table`` file from ``xpore-diffmod``. ====================== =============== ======================================================================= Argument name Required Description ====================== =============== ======================================================================= --diffmod_dir Yes Path of the directory containing ``diffmod.table``. ====================== =============== ======================================================================= xpore-2.1/docs/source/data.rst0000644000175000017500000000147714130107110015727 0ustar nileshnilesh.. _data: Data ====== You can find the links to all preprocessed data used in our manuscript at Zenodo `for the SGNEx data `_ and `for the others samples `_. All the raw fast5 and fastq files are also available at `ENA `_ and `SGNEx `_. Please refer to our Supplementary Table S7 in our manuscript for full details of data download. Note that all HEK293T-KO samples can be used as unmodified (m6A) controls for any other data set generated with the same RNA kit (SQK-RNA001). If the cells are genetically different, we recommend to perform variant calling before finalising the list of differentially modified sites in order to remove false positives. xpore-2.1/docs/source/quickstart.rst0000644000175000017500000001404614130107110017204 0ustar nileshnilesh.. _quickstart: Quickstart - Detection of differential RNA modifications ========================================================= Download and extract the demo dataset from our `zenodo `_:: wget https://zenodo.org/record/5162402/files/demo.tar.gz tar -xvf demo.tar.gz After extraction, you will find:: demo |-- Hek293T_config.yml # configuration file |-- data |-- HEK293T-METTL3-KO-rep1 # dataset dir |-- HEK293T-WT-rep1 # dataset dir |-- demo.gtf # GTF (general transfer format) file for transcript-to-gene mapping |-- demo.fa # transcriptome reference FASTA file for transcript-to-gene mapping Each dataset under the ``data`` directory contains the following directories: * ``fast5`` : Raw signal, FAST5 files * ``fastq`` : Basecalled reads, FASTQ file * ``bamtx`` : Transcriptome-aligned sequence, BAM file * ``nanopolish``: Eventalign files obtained from `nanopolish eventalign `_ Note that the FAST5, FASTQ and BAM files are required to obtain the eventalign file with Nanopolish, xPore only requires the eventalign file. See our :ref:`Data preparation page ` for details to obtain the eventalign file from raw reads. 1. Preprocess the data for each data set using ``xpore dataprep``. Note that the ``--gtf_or_gff`` and ``--transcript_fasta`` arguments are required to map transcriptomic to genomic coordinates when the ``--genome`` option is chosen, so that xPore can run based on genome coordinates. However, GTF is the recommended option. If GFF is the only file available, please note that the GFF file works with GENCODE or ENSEMBL FASTA files, but not UCSC FASTA files. We plan to remove the requirement of FASTA files in a future release.(This step will take approximately 5h for 1 million reads):: # Within each dataset directory i.e. demo/data/HEK293T-METTL3-KO-rep1 and demo/data/HEK293T-WT-rep1, run xpore dataprep \ --eventalign nanopolish/eventalign.txt \ --gtf_or_gff ../../demo.gtf \ --transcript_fasta ../../demo.fa \ --out_dir dataprep \ --genome The output files are stored under ``dataprep`` in each dataset directory: * ``eventalign.index`` : Index file to access ``eventalign.txt``, the output from nanopolish eventalign * ``data.json`` : Preprocessed data for ``xpore-diffmod`` * ``data.index`` : File index of ``data.json`` for random access per gene * ``data.readcount`` : Summary of readcounts per gene * ``data.log`` : Log file Run ``xpore dataprep -h`` or visit our :ref:`Command line arguments ` to explore the full usage description. 2. Prepare a ``.yml`` configuration file. With this YAML file, you can specify the information of your design experiment, the data directories, the output directory, and the method options. In the demo directory, there is an example configuration file ``Hek293T_config.yaml`` available that you can use as a starting template. Below is how it looks like:: notes: Pairwise comparison without replicates with default parameter setting. data: KO: rep1: ./data/HEK293T-METTL3-KO-rep1/dataprep WT: rep1: ./data/HEK293T-WT-rep1/dataprep out: ./out # output dir See the :ref:`Configuration file page ` for more details. 3. Now that we have the data and the configuration file ready for modelling differential modifications using ``xpore-diffmod``. :: # At the demo directory where the configuration file is, run. xpore diffmod --config Hek293T_config.yml The output files are generated within the ``out`` directory: * ``diffmod.table`` : Result table of differential RNA modification across all tested positions * ``diffmod.log`` : Log file Run ``xpore diffmod -h`` or visit our :ref:`Command line arguments ` to explore the full usage description. We can rank the significantly differentially modified sites based on ``pval_HEK293T-KO_vs_HEK293T-WT``. The results are shown below.:: id position kmer diff_mod_rate_KO_vs_WT pval_KO_vs_WT z_score_KO_vs_WT ... sigma2_unmod sigma2_mod conf_mu_unmod conf_mu_mod mod_assignment t-test ENSG00000114125 141745412 GGACT -0.823318 4.241373e-115 -22.803411 ... 5.925238 18.048687 0.968689 0.195429 lower 1.768910e-19 ENSG00000159111 47824212 GGACT -0.828023 1.103790e-88 -19.965293 ... 2.686549 13.820089 0.644436 0.464059 lower 5.803242e-18 ENSG00000159111 47824138 GGGAC -0.757891 1.898161e-73 -18.128515 ... 3.965195 9.877299 0.861480 0.359984 lower 9.708552e-08 ENSG00000159111 47824137 GGACA -0.604056 7.614675e-24 -10.068479 ... 7.164075 4.257725 0.553929 0.353160 lower 2.294337e-10 ENSG00000114125 141745249 GGACT -0.514980 2.779122e-19 -8.977134 ... 5.215243 20.598471 0.954968 0.347174 lower 1.304111e-06 4. (Optional) We can consider only one modification type per k-mer by finding the majority ``mod_assignment`` of each k-mer. For example, the majority of the modification means of ``GGACT`` (``mu_mod``) is lower than the non-modification counterpart (``mu_unmod``). We can filter out those positions whose ``mod_assigment`` values are not in line with those of the majority in order to restrict ourselves with one modification type per kmer in the analysis. This can be done by running ``xpore postprocessing``. :: xpore postprocessing --diffmod_dir out With this command, we will get the final file in which only kmers with their ``mod_assignment`` different from the majority assigment of the corresponding kmer are removed. The output file ``majority_direction_kmer_diffmod.table`` is generated in the ``out`` directtory. You can find more details in our paper. Run ``xpore postprocessing -h`` or visit our :ref:`Command line arguments ` to explore the full usage description. xpore-2.1/docs/source/index.rst0000644000175000017500000000274614130107110016125 0ustar nileshnilesh.. xpore documentation master file, created by sphinx-quickstart on Sat Dec 21 06:31:05 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to xPore's documentation! ================================= xPore is a Python package for identification of differentail RNA modifications from Nanopore sequencing data. To install the latest release, run:: pip install xpore See our :ref:`Installation page ` for details. To check the version of xPore, run:: xpore -v To detect differential modifications, you can follow the instructions in our :ref:`Quickstart page `. Contents ------------------ .. toctree:: :maxdepth: 1 installation quickstart outputtable configuration preparation data cmd citing help Contacts -------- If you use xPore in your research, please cite Ploy N. Pratanwanich, et al.,Identification of differential RNA modifications from nanopore direct RNA sequencing with xPore. *Nat Biotechnol* (2021), `https://doi.org/10.1038/s41587-021-00949-w `_ xPore is maintained by `Ploy N. Pratanwanich `_, `Yuk Kei Wan `_ and `Jonathan Goeke `_ from the Genome Institute of Singapore, A*STAR. If you want to contribute, please leave an issue in `our repo `_ Thank you! xpore-2.1/docs/source/preparation.rst0000644000175000017500000000223214130107110017330 0ustar nileshnilesh.. _preparation: Data preparation from raw reads =================================== 1. After obtaining fast5 files, the first step is to basecall them. Below is an example script to run Guppy basecaller. You can find more detail about basecalling at `Oxford nanopore Technologies `_:: guppy_basecaller -i -s --flowcell --kit --device auto -q 0 -r 2. Align to transcriptome:: minimap2 -ax map-ont -uf -t 3 --secondary=no > 2>> samtools view -Sb | samtools sort -o - &>> samtools index &>> 3. Resquiggle using `nanopolish eventalign `_:: nanopolish index -d nanopolish eventalign --reads \ --bam \ --genome \ --threads 32 > xpore-2.1/docs/source/citing.rst0000644000175000017500000000053614130107110016266 0ustar nileshnilesh.. _citing: Citing xPore ================== If you use xPore in your research, please cite Ploy N. Pratanwanich, et al., Identification of differential RNA modifications from nanopore direct RNA sequencing with xPore. *Nat Biotechnol* (2021), `https://doi.org/10.1038/s41587-021-00949-w `_. Thank you! xpore-2.1/docs/source/configuration.rst0000644000175000017500000000253214130107110017656 0ustar nileshnilesh.. _configuration: Configuration file ================== The format of configuration file which is one of the inputs for ``xpore-diffmod`` is YAML. Only the ``data`` and ``out`` sections are required, other sections are optional. Below is the detail for each section. :: data: : : ... : : ... ... out: criteria: readcount_min: <15> readcount_max: <1000> method: # To speed up xpore-diffmod, you can use a statistical test (currently only t-test is implemented) can be used # to remove positions that are unlikely to be differentially modified. So, xpore-diffmod will model only # those significant positions by the statistical test -- usually the P_VALUE_THRESHOLD very high e.g. 0.1. # If you want xPore to test every genomic/transcriptomic position, please remove this prefiltering section. prefiltering: method: t-test threshold: # Here are the parameters for Bayesian inference. The default values shown in <> are used, if not specified. max_iters: <500> stopping_criteria: <0.00001> xpore-2.1/docs/source/installation.rst0000644000175000017500000000060414130107110017506 0ustar nileshnilesh.. _installation: Installation ======================= xPore requires `Python3 `_ to run. PyPI installation (recommended) --------------------------------- :: pip install xpore Installation from our GitHub repository --------------------------------------- :: git clone https://github.com/GoekeLab/xpore.git cd xpore python setup.py install xpore-2.1/docs/source/api.rst0000644000175000017500000000017514130107110015561 0ustar nileshnilesh.. _api: API === .. autoclass:: xpore.diffmod.gmm.GMM :members: .. autoclass:: xpore.diffmod.gmm.Constant :members: xpore-2.1/docs/source/help.rst0000644000175000017500000000061414130107110015736 0ustar nileshnilesh.. _help: Getting Help ================== We appreciate your feedback and questions! You can report any error or suggestion related to xPore as an `issue on github `_. If you have questions related to the manuscript, data, or any general comment or suggestion please use the `Discussions `_. Thank you! xpore-2.1/docs/source/conf.py0000644000175000017500000001147714130107110015564 0ustar nileshnilesh# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys # sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('../..')) sys.setrecursionlimit(1500) # -- Project information ----------------------------------------------------- project = 'xpore' copyright = '2020, Ploy N. Pratanwanich' author = 'Ploy N. Pratanwanich' # The short X.Y version version = '2.0' # The full version, including alpha/beta/rc tags release = '2.0' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. #'rinoh.frontend.sphinx' extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.githubpages' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'xporedoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', # Latex figure (float) alignment # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'xpore.tex', 'xpore Documentation', 'Ploy N. Pratanwanich', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'xpore', 'xpore Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'xpore', 'xpore Documentation', author, 'xpore', 'One line description of project.', 'Miscellaneous'), ] # -- Extension configuration ------------------------------------------------- xpore-2.1/docs/source/outputtable.rst0000644000175000017500000000410614130107110017356 0ustar nileshnilesh.. _outputtable: Output table description ========================= ========================================== ======================================================================================================================================== Column name Description ========================================== ======================================================================================================================================== id transcript or gene id position transcript or gene position kmer 5-mer where modified base sits in the middle if modified diff_mod_rate__vs_ differential modification rate between condition1 and condition2 (modification rate of condition1 - modification rate of condition2) z_score__vs_ z score obtained from z-test of the differential modification rate pval__vs_ significance level from z-test of the differential modification rate mod_rate_- modification rate of a replicate in the condition mu_unmod inferred mean of the unmodified RNAs distribution mu_mod inferred mean of the modified RNAs distribution sigma2_unmod inferred sigma^2 of the unmodified RNAs distribution sigma2_mod inferred sigma^2 of the modified RNAs distribution conf_mu_unmod confidence level of mu_unmod compared to the unmodified reference signal conf_mu_mod confidence level of mu_unmod compared to the unmodified reference signal mod_assignment lower if mu_mod < mu_unmod and higher if mu_mod > mu_unmod ========================================== ======================================================================================================================================== xpore-2.1/docs/_config.yml0000644000175000017500000000003214130107110015075 0ustar nileshnileshtheme: jekyll-theme-caymanxpore-2.1/docs/Makefile0000644000175000017500000000113614130107110014414 0ustar nileshnilesh# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = xpore SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)xpore-2.1/figures/0000755000175000017500000000000014130107110013467 5ustar nileshnileshxpore-2.1/figures/xpore_textlogo.png0000644000175000017500000037362114130107110017273 0ustar nileshnileshPNG  IHDR&s{2nsBIT|d pHYs.#.#x?vtEXtSoftwarewww.inkscape.org< IDATxw|SeƯtAق2e(A""x."FL{hinWK&irrNVO9s_}:` UUf B<="AAAAA刊_xz :遮˩}O<="AAAAA !¤ pY<7AAAAAADIA?!O_     #&A?=3AAAAAJb*8 ~BZVmn     ` O@APiEaRAAAAGA DA^)\!    !£UO&LaR'OAAA ~‰DA f*    `!@IA?AIAUHOFAAAe "> ހ ~@f.p3i@N# 8Χ×@jg#   "L 9ַ Ak\jwAAAR):"L It}Arx53AAAAК O@Ap{vb (X&&0AAA/A*AWtTL 9==Az}៯zf    Z# ~NTL x5R1)  PHgR,"L 9"L x7 k"L   4"< > "UA{XWł[AAAE*&A@O@Aet&AAAU@:TL 9"XgEAAA_aR aR[(9    HZϑIAH*&AAA&Do 󈕫    %&A`>! ލen#AA;s{2b0bt nf{,b*'G)E0)X.EAA׸te瑏C:zvL Z&l+zzDIA?T!"XaRAA\b~( ?Xm(R*"GGHҀԬS0)'Dyz ,"L  kXׁ όEgoxf0,: d媽wAR)X&0)  aXMCmVlόT c Uw ,yii ?)Va 0i3:hTx%xX/pG8OF_òbRo#@Ђ   b*(s酷eyf,ZjoUؕgU: ` O 1;ϯ'_xMf.kx?K$8=&VM,    T5+ץn;A{xu&nhG^$aq=;&A| kb*  ?c0V?:(<\t7WL&Ln}[ַٻwӶ9^" 9?[E''?ws"0eK?qXA]DAJ ЍӞ F& Zo=j5WRoV.z5 1|´헿UoAI %s7L[~8sY}cSYJQAi 87/ AdR@P%ӣ.& +_e=7&A [4 ۳x3 OTXG5"ACJpdgRqxM'o]&oN&u+<Dw|`ɵ[̢;r -ӣJ;H`jFy@p NRIx0l9.\z z4'hv1F 4ѿN$.(-'; ;_AWIJJl)XEOp=uXvӣqw$;vXv]!¤ U֓@t@=oKOd?۹@p6ǵg1ɶ½O_~{N1 3 .@87pP/]//'H䶨P 4 qmiY<Ӳ[Y@Vs-jƟ="$ 1fp"B sG ,R,$,3U ~{77 έlv+ski{֑@H{m- ڐg_, HJE S;M\ |G+'@a݁r.c2rxI4><~oNj~ⰼwNfDcxϓ` Ag)||=c$[Nd(aĄ~4o 6d7[k;&Ac0}ܴL,H:ֱ gFۿ 渞{{sQc՟IB ~l4m,ru,j{p }뛙ohf! V.E}!7ALr*'1fIX0P4isfifer 3l?WmLbe\E?džLaAK@+Ҳ(P|ӛ^#73 ' mT %q"0g7WR|4RT^/,m'ϛs_O6&x?-n%0n[%ث6b)6b>F| ]l872qnfG}\S:"U10OFP ;LhZdggsRpP)jj @bNy:h A|A#{La27|ﶝD3- u30|}<Ц-t(idN>`Eq-jm4& 'ϹEmrpN%OL?[%^js((F;&@V.pNҲ8g9Y̫h-C+k}d2!I0Kk I+tktIK{l`9׎lɷ6"x7{JE,0'@uzL_Et?Z %i-XnÑԗpYӳ9$9h夽 ;G<; dfqU"n.`D޼R`?SIr*{c ?LJ&_$-&Y}عʳs7D4J+&VRbx{`0(#*BƷMn8aGy2s0"U8zp *vK v Kxz0l`isT4GNHJcѕ)f vG%XcA੉$u<2fa?.k8{Ć:e+7'-1U6e)vm-{Zߏ凘$9_nMf=A#Mv#k ԫ>9Ⱥt 0TSp-Jw*9|% d(h8eEI_s'A dCmOd!#Ċ|uG U&16z0{ΰhm۾7(S*hx NPLKJx"Id$%VF5L،>œ%귬[|=+J|b +l1b , (k}<=p&p[zX}~Ʈ /ݤ- nm/x xg&$k(Cknf7.A5) <h8k-3vұA0'2ӣ(90)cWqZNDIA[Rqޝ |\Hg:?9C8[s|8Ÿ;Yy vmS~6z9 h[kǰǺ39x&u:LֽӯNgU&\2}x0+]|_KiYj.YAmNd(H 73%kt^ yrܗ}Rp>yUcWHP]mbÁr1+cTeQ^5F:p#þE 8BPA_l8*ǾͪQoE5>&`}ys+iEݷ/. [|f{x0Ч%Ц]ƬS>Ʒd/˙V؞Kޚ||Ѧ&0ϫoWkw >ΤI|2|~¹ ?š70i5o[c9u|=0~T$}u8f>&[^g&f>z '?3 LrXUur)߽fz ֮vH d1b#5'Ͼr<"ԲZ!;-LwV| %џM?o9 <2ӂAqq(f[.ttN_g|=\YVƄsbT6>GZY2dMzviYY ¹v9둜e` :Bxu n9GqU+>*@ 6BuFHofrlH5YF,(.*9nfr|!8 [BDBa'}EA/jw>-I ^Ǟ@Z%RD47*cq0ն(6;' ΈӦsw¿[6 שߧ|<ߚU5˪{+q!J_ mӲf&iYf2-3g%;O*V݊\MA) ͷ."e|fJh3D= Rlq$wWY+yI 0ŷyF>k鐒#Q~<= `|V:v׏q5 @&,Yhz=myE1(If.`Ym2rY΀Ҡ ָ~4G^|;+5|d|0z?:V=Q򾺦J}cb 0dQX&P)^qZ`-? ( bcϹtxozC )[(^;TZ3|72h.8n`ň|VFMM SQw$ h# ~3 +$0ncQF-V&-^Lb;-']g]Kդ(G׀s7sXjebDߖdX)"5 xmskq=_M޸ uvUײyxO&aMlѺ/۩kX]h[٦y$[UOs Oz)AZ!_dd'M 6 FҘEf5g>zinfq3$Bt^wZ|~S`>2IDϴ!vr Γ|}+0EBP t_wY\s`o'nfտٮ z!?]FNyNV?w鷝Nrnor+$a*V {=r,ez~:A? t`xu6Lu53_%Q簒lR6צٻ(FPЍ cQd( )n ,$ $5l .E`0ۧ(uDVG;ݭh̬Ȯ{+4 a%z?;+6'n%:R, x@is0Z&^=_o lj W)Ņ톳G|ڝ}ҳV2Z\Q2%ݬ]`T9_ہKS(Ų*+xy:0/Psw{+McO[c;4\`#Đ健OMp]۝@#;xG1-Vڠ?pPٲt:&YW4j54ꧤ5#e.vx*,@`.~ڻϪsn; G51(UM(l*ƅ}ya8?RL[p #[ 73HIt a50mco̢=ĄQijƖ>-'ZK dV=L<٬ zd&Y-ޯ>A| "OsnpSMŝ ݴG#kKh১~xozXF=Ʌ Pu`D/Zw!5g-8GSׁIfY_<)(BU Ԯ F>98‚ [ } cci5wA[13/zslx,ŴDl\FY)jVE({" IDATF= zȭlV|TR#ouOq-Um([c9`fVʤd1f&gdSR9g69C}Q5z.DŽGKXXP7X0:>X]=ks2dϖ&5 ,yh9WRi)Vu+p]u465),|s & o#d09μGK{-Iˢ=,6c9TH={w6AO^Aa$eҳq;;$D+l #ef7|0p.h_75XxA[{?S>1<-, ą#\ĆMO7V~'q~728yK`_-c`c(TP*/Y\,m^0١67/Zoy7U: K\^OgSVG#_<7;zz4C^݊ٗLX'VՇjAq4Lн:~oSoލtmk2Ydޠ}@-8XzFE#T3O&=O i;j{&m1mS͙<$VőYv:Xuwo`K\wtFEM]O̝3(󣏻u]:fuͿHܷGZAmmpǫ_7pH`ieE*V{^Ne\5*>\W;ozGΕG=~(%Zbdf.m[mpm?Zп5мswدr/Plj9 7lPόl^;r :yh.LfYbÁ(  |fvE+Jh_}''PѦ3B)G ^$_-JɆthEMdAC]K;/K `yfO~렓׀V_sIF0yiR%LL^+U]!.疒xn(;ړ3h{`9XbD:w\r0=1ah)oiqou{^O00%G%^3e nV`qnJ{^SӳQR "aOqI`  |ՃHcLLV*A{svk^Mgt0ǝ$K2s:Vy˜î=g'њ>[JXR Kml^}KR!{SeuZZ:gkX׵q^" Q2;n ʱUwHu G݀WUJͧ}ҵ,@{KT9l1߯ܯKքzџ]+ fW^– h[8U.>ϴ#\O.7<~w[O1aIbδ~woIz:p=HNYEz:P,m,t8͹JͲ@{ 6>jXZ468u q2h:0czk;QҲCؾ3vz -nǔoqEE1@̼7!zj-ke~\Gxuw3 5S"TV=)WyZO <Ԁaop 0k_~Bm[ p>c'0`*eB,mUHl? <:Vv/0u,6w\G= x02+pٻYR9Xb.#6k[O__%@֝A&;o}-zcTO tLxI73p&bΝt>ZI#?Ȫ(kKX{I#sws_zϠ.w- ,"BXIҾ6gbw21Qaa-br*M逇d5%1hWS=QRo֫Ӌ,H(3ׁ.Y,:xNu/v߭-Hgɖ9yz:suuࡆ4Q23,֖b򱬾0oo>O8\̱ED]OH{s8~ :%DP.8P(n 5y7~X=98'PС5ʲ (4-wVL,v#Gыd`yࢊIXA,zRܝh3lk0i+h8)KJlݦU6kٮϒp"^,ܕa5]Q /)a#z]މL$a d1 {fF_|zЖSovj^xh2 |.=k|Nՠ\ 0iY ߨξd[oY:%ofҞob׭ 1g@[ω|Ya2@Q f%\LaN~?>ah8-s71h)jPfY`MAij7(Y]S.cݢdl8$2 ad ԼוV=発U^e\*4vM>WS8l02ywj[. lDnplIx`[38x^fN ֪ڜ%7 Z8݉kx0hj݀Am}.gbNgk&frc-I`ܞ4 9qHJzֿm{,>`L~{ޱs#iۀ }t6Ovw?vN&\~*{s)vS]#PJRiە8l=#0j1ɢc?. Ur(ͅv,"KGK_xsv[ɄjQG=zG0T+9nӳO͜.dPHBysCgc~_Cz a}~QoxK) :nX筨L]7UZ .rmN׆gg.;ȅ3UJZc\tM ,?,ƒ4ѣc_@( 0#yU`\Tڱ>[b3e G;N[ߞFtm FQ2& x'~K"ʱ}m.C; X|Vsn>m&ov}:?SL4'>]O #gPf*`8ryRH+z65,MM`˻{'xk)azB(Jh +XͧclL0el଻  S}!fYѼ%Q'IH`Ez^]3תݜ߯Vujp㓦 {X7AAlefޙXU"+l?(c%$kね\o tyqHU'ڣ&3^,\oL6* ^Gxk̶!Fݰ×h/ZARa &3_6HxL("}}Yۄ9؍ +n<#面sV.-{Ƒm机F[h[6 v Zfc+=Ls5Vw6VsEd1aTw\u'kuelmMԘ*;9xYͮ\X TS%x$[Vc``.% QS{w4EI#>|] xޖEq,BX9-{W`JٴvZ0-pu52ri? 0``;f Y }(lbjQAٻ]K19y@u[y{3F f-TqekqnYjZ fB]ֽ!žiA2 #6a`;`ԓFr+q}%7^~`TI`XMyc'm9$kECy?<=I~PdJV\'̕v9` d6ZYW5p8&|9pposiHKc[XzV(}~a'[燵8mt&mt-)$;!X< $[8~Jϱ/M8^_vUS^Xva˴,Ѭ^$M:?q}JS .,de1eg,{@՛7)c:4buբ`mQ9LVV~gQ K9 3tnNa2&6f%A g( >Ze зcW_zW6o148,C:mF:mv 9,:WIÁEv*J J{b F8NG>'0s0p{5 xc&0}sUZZ2}Ԙ58mk2!dXS4mÂMZ~ܳvb(tMXkT|#-eOϖP<~7-c{7˕,5L?s]tVѽn279S(<ш=ÃntEJL~a^s[e.+Ԃ`{Yib΅VNbӄ_=mjo3m׳e狁= ?tj&_]cWT_!Բ˜Ic VRW bP 85uG8Ēo %xR 7m б2m- t-Ѫ%o?7tt_[cIX/s?+JՍ^2_B Ef9{.kqND@f~:(qp`.ڳUz#=0g@=uXz:WC:Ғ8{Nu`|cy#9χ?ªk (|׋U#w_.?R>X0ȔqGVҊ̓|xb=fY>-FŜ+uir8-f";͔-|EX0+XUUfE+vP٘n2+tqm }>#V9o|_]Z(գX?{Ҥ(-R)BKFM[NZ'&_9MŕTsr?plA\+^-JL.r(lKvyV<{+}*F`nrvb€ůwU6m {*۲\2pf9sR uoqR+~cV)|^p?fZyl|I%׮6j5=5X)y:Obōt*Cm;2[x۫^%D@&J&eI%0x5E5*W[i,Ӛ1¹iۙ봟S-,#W+kc t z@3d s+S2}+ IDAT yޢBϯ\$^X8If&U&P@t,} ~Vw.ZiΏƮgViEjJ#FsOTW[+K7POv#8p,O{lHOߖE,3r()e1GYwYI,Rs q7^R C:%m{JF>xOQw1&<,}\r.uGpUUo9/e|Y6WexQbpA µY WMLօ'R6L-[b&Y6$cȉ gF{bfݔB(tOA2! rX{}v|\QЯVӂ͏~c=Rݜ SNV?P;X}cB3G#`QZ~x^;NSњVLYqzFbXV ̝S*~9CND;֪EK:k-,?>^uw]kV`J ׊KVl򹲊s@QLL VǾ|yͭ٘}R3Y1o M޹Jga' }; u"Q7V>)[{vsL8y*;mx~EKm}n>+$?[T&-,Be'iG)W5N `sx۲HʹjRsi0eY-iQkgxj0o`#7 !ܶis(WQټ߁ٷWZl6 _Ɠ rD*AT$ޫ3sZߨ^XjaQ``sS1kIZ-~E}-DfV{;u+0^cS00iwu%o[9UT*ydޞ ,9\5I,Esz}"JǪ5f){˚U9nU+㹛zڶc1À^}y Pz-7,m\6lv=aĺvVjckÀތ36WD )+:aŴLt# YGLe`>M_zjȪAo "Ix%dxP0Vtu/h 0@>{70{,hS2(Lk9SwaR u+cS^M;m⟝$,gXٿ52Ѭ3vYU&UI$eٲLbV4,\V`tP>*/3w>^8Z;_B =>ޘ|GyzO]!'hkTčPÇr)0lwY ^ӄHZ喉2]|k>vo-dvVYqxB]v$OfGZ%=Ąl;2T3h.wOQviTYFa%`F 74Z}y5u= , Oh^ʼn rѲ7wD3p%WIK1KmMӶOq ׁFѮՓqz%rʒ?T/%Jt;@(` v*,E/2ث"}a uQz`ҭ!0-^&8J\J;&&uy$@wuGMl6vPDP |ޓ@Z,nU_/[hʼַJ:K첃lpPG1)8m<<5Qy}u)8U/yGOQsbl)9s7h}y*;K8xtW(l=sԤj],ZT/ø̈Ugfgg꨽pAz6F8ԏ SQxL!$0df@HiU7'8Y}w:~{őkZu+GRŵ[Q 4ֱ~oᑻDT3nʃʹ=mVWd2si5:&md_L85[kfe2%  dD3!jmFY@t9)Cmh#7{{>J7w0(lypňrn>=yhyOD`@Ӷ|%JFtguȩۘt>[!] '>\7f\&.|=0d&0KѡH}4=EہG~%fߑ GipJ,^0y%ApPC Xm"Gkz7~N&*_? F6z^0iť)Nj~'cHDNH +ɛ{Ć3|oӶؗd= `̞lS αpd%=Fdo5wU,ˌyN,^j@VE\٭YhRWR2˜P&/س5c_I[{+ğy)07ŀxz4nlgsl)>{_(3rA|ZYDg0u+7uL$grQ'AHYƒ-"Dhxי3?3YZ*bhi,ѯ퇋F:EC9'7RFy@[l'fĄv>AB ,`R tǫ]wgNws]B8&QoQmM:_xor55ۄIWy9¨E(gcॶܭ< tCB *3n1 EDҴبWFfWלxKZ бp`jE(NǍtuqKc0x qJ̗ ApK`&߾`+Gmk3~P{16.!nwUg]-ĿvKX&<4_4c&Zϯ'r'D}ZbŻ%%g5T;ͬ,i5 b!GJCR,AL*K* 2З,FTL*dPkS\+aMA±9IVNyX-xN}։܁E'AWR9^qc%gOO1Mp/{zEco։Ff& 39`qU/clK2׋ZJkcG\ϵ~~ھ(%/R*&m]<ϢWy?'JY"=5;+񼺕 ;hPLб.P*#2욕R.d i.JDߖL%k ̢TbWKzM3q.^Txs[)M2ھ'Kne &"@|܍ A JVR2h3zsnڽ!񞟢VJ@ќ/*x*.|x `mLmù0mXT=$P1ؗA=B=ޞ8e\Xu nL:U&|;[4dz{<֩!V;縜 ,?YV'hԓ߁C0$r1xrk] (#2j8` hʻ*waaTM%A[8F>\AIY珘kFf=="(x#ϲ!OץSmm K"y@A8%b{!8k3?kW5h]hY_f6l1{*%@GNZjZ1dT Qy=xx {K5Z0+2Ty ;I a5^m2폑??, ٻ}@qG"~7bX] ן?:oԡ(Zۘ2~s`l e6 w(x'MޤkS:qvZj3Bqu_״) La-<{B^%oKmFWR11&\y{ѦurQ5,zlݿN3Ρa1 G\{n3{J#ѱ"b)_Y-FS6 =x "kZ p=Yy+tO7xwkDI ,=ؖ1{ˋMit㪅gH>Jݡڦ~$w ߘhs!Hyrd MJB"֝y{(Ų 66g#>\\9iOL5Q3/پKTBe܊x ] x?:glݗf^빖~ #%oV&mcս֯ Plk[1Ӧy93Jdv滛]]-Ж!uDžmިJ+^m.#o&l=X\1;8K`[(֗;O3h >wPm/=FfOaS'X&He) 71II`jѬi?}Z̹/^I{ C25+z$$< fkL\ߓ\e)zUs6zl8!`U yå֨^R(&LvȉFycMK~nn, 7XA5Ul9&vH3Gݛ@+ &/`=} hc#&-~uҪzK;=F<.?`erF?vB*c+v^]R5 ZuJ f3pj?[%sQq.ꤊ΢}jd^N+[EO_2E{O`eu-܉uXQG74s 0|%7)9_erG-&i Kx eu/|xs`&GŇֈ=5G3;>Xj]Kl.7"Y*źF͹fV^0ـmu&-_r IDATϡw*Wk=ojq`Om(n:B[DBW8:2Z1Z\c4}I`ŽmϦe)ܓ=z h?M]z'k ^RA(&#r5&#'m˲Hj́/W3p8Х"'S+fgRϽYo%(-6!jh6EUuz~u:'SGt~}:rPGevV8ÍG̵<%R ymBԄQDfzw@`PV%mTf:C\@H 2c3OiCp-i b%wo<,5FDEwKm'7)txYJ!"'h~hgk/b!KH*;hbWuj_cX:u*}l ^ Efåh ~Xh40滶䉒FgIHcgLF`j7/;Հ5bـs31LK%N%[&f/-qsHY4oTfVAZC^u Kmea;; Ԯ`QX/eg}fR^3o m/ޣZþK@iw9QS.<&os̞@b̼S$mA=5ڦi~O LWf[jݧ@ᬨ ?o`"qvc;/M`^!LWX8~"B%w?(R /#в4zIqk(Zm;'kg憉7^h Y.cҼWu¤: S"Ȉ fbc&x=*>c6.>65M..V=gq/B)ԏO趰2e{@_y=K,&ɔ S̶*8qE:코2ޛz~ ׀jѻ݄< &N"3I8uI)- VaBN3^O=1*ћvK3\Il-, 凜8YN-V-ث l̏- (܆x>vcek d"q%|]T%LjI׊ K̽g@_}Lo}v~@ P@ihT<|mQ 3~fYX~7iMH{dlC: 9 2}|l[=@v-jܳhnecӧM7%)ő*dNT:~܉j5? twy9/LEr0q+EQS 3_7`|QZwShιR9/ V,Wh:g}L{21>YQ<;EɭAߊ@sdQqzI O(JUsl-qi~x*ի f% Q"]Y7'{֐jxm'p[]]z-p4QYIW,'eb8Ĭ=Ѫ4;#hߋ\>Yb `FOZ_VM뤟ph2^"./"a@o9AaiQ0uC,:~#uV [ܷZ;eHޜKUloH^ R]-@eIE gg7R翿QqnւQ#3ň5 QP |5m!_8žrSvt|\@Щ<0 .m%B"h)d8} J*I][ƛ.)k8[oe \:0ouڷn;z'(i6s(d*~9rK;_#,8)|`+;Àζ[>~D5܏M>o0v%8ncqlJEsOYDDr\eq/#haNb ӊ* zxlNw]DROyW 7Cs/VZ'QhvcBDFw؉Ҥ?wܹCdE P!u%-dV *|cZrsa+૵ҿ`z+μ =D7 Tǧo?ݣ}qq+g>R\:CpN@;f`,՚ y~h"* |ӹEa+(f `6][Wqr5wN؉اyI)x%ط QR0Vm\ީSЉuJoKGf^r|w&Eg|U-p ;P";gǿdd:)vwz Fc/bl۶?G8߸8-q?of4ك7r3d/c8c3#}-QS3\-fX{<]5*g=quf@ Mt,lnOH|=G%ceny=6Dѥ3Ěz$ykj.r0Їs~Kg[^@iCv_pLA18^Kh l8u9%TejAt٪473h~$|2`&G6U%{+JmĆ v~Be%2vaF?C!g2#[jdNEN"F^v6sGkikܵ = ?cV2oM ~޴+_Ĥ{ 8xWqtx ,JN7*͎n?O׊dZN_zTd " lہB\'n5Ǹ11ovt#8ĂvSsө[y%lqlDF-jf /G퀚:w.КZ t)Q/J>} 9C ^P[]+Qt :&5/08Q@IsI#\2&mM&J_sFqB! zx4.AQR|%9?`BC[M<.Iʊ&)q`;wp0e*ϚY>eĴ W1@۩ajЪc}/l *waZE3Ni+]k[θ%tr>o ofVmϫ7a[%ܯv o/ZL(d9_uQ (Kx^G_b _zTJZT%:г2e:p֣Gʴ @3w@<%sQ̟'no83YZ)l9m/|P.g&9\| 1u!3aAG{_4|"-ŝDv`\bEݻ$0+s%N`bF3\ٺL|b#A^=LGӒ3v¤a2uecBQr"1. 羿Tv$} ldvgv`fϤ{ 0O"ys]8cOKC1TZaD2Uҏ1v3i+MPf<Ŝ3jB}/c+SxAwlw&p6Nu ]E[KaXc^c Ȝ*5/E<1HW&2L Fm`[Ey/R,3r2Iʽ"&*>Ұ}_~h<^ZαI?~v¤ s j[.5 gψ 0-Y7QlʫfYz{Wb @QzB׌=whҏbjb4 NR,2(v2a. K#DIS6'pHH90 k?] \tƥ偩]yO`2|т{޽R{_Qtkt%Ld:pRDۋ3S+!ks*XoVL|LY@_V渽=NߢIPWwLakcSZE9g+_ ڣ wGGy>mmǐ;*L~5KdHV78"c63qx}Vsf"pJF҂~@]ok}꫐qaڿ&gnKR0Xr^ư(kVy,Rܴ5)u-OpxG_^'f~'Q&`봹Mdor0@_IqV-jh{#朗wp5 -{WwJ5\wN/ u^j3en3 ) RJw~!LjȈ̬?X߭gy ǹ0'Ч]WhQ:_(kUFfNDF{Jcдv9z@f`:\uhh 0gJ}MKYŸh`vwU: hPm $^Cvv#K!o&oSV4A e<;KUʱl df^?lܸ.yGszg\GGNV垞@XЇ7a?/k+OyW Wɟ{ [?w0VuMYI=hOѢh竕%۔eDI׉}9םS˟~$SȶAz| l:ؐp=¹i{ _f!cbG%s8:nko$>&ΝZl`zxGjm7.s]܋D*Y9#׹#_>cqXcf*!6Qhwȏ<%0e k,{o6_4WgYs0MetZ{^!/ r\@մ$0tΧg3k]0IO9_Yz !(E\Ap^;W=̴SB^`Ij 9y=:Tțl7wS.x`JǶei_߹ԧ/uwҭ0 `[ )^6:IaauXU FB >[%,A q-&msA~d.ٯJ<@ Zr6O`زCQ. ga|ٚ~ d%S7,=X\PH. d)ԏeg_l )_`3E 8OT4Śz$@}mffO+~x;*V OZnT(i0p Q20kWupJ>uV>v߄LcK;b}ZUw>S>F*ŲJ.ciߪt(( I nW/ȹEw%^d&\\Kjj%ib߰Ҿ_6s3{yf)xZ]vW9ꅀ~yKߕ/' l|(+n~~\+u(4;zn\ ҵ0 7x9q";@T-i툳(`?)822;e] |LjX } IDATϺ⤏x3BE|s@bƩSVWXA>;\P,͍B9g./JY͎k=uz%~S(m>9)]oC\);@ ЖDŽ0)do% R56Zy ܃gКQ)g+^7ʜ!ZCOcyo$5yU-1p_UfLJб<0E 68#;zZ /Lj$NDk:rM rY:\gd|,=+SSh5 XsLq*tJ*VE ƅ̑k,^f2Ҿ587 "2YSۋl7m9 ¬Z+NI$FA@҃ |R7uVds<``7bmu1qޯ[1@9MpX 06sK(ZoZKG(F+DQMYfZx ‰Re%/U/Q#QhoGi|Ղ:SQ/H_<LJ@I[1!ȯv]+:}cR :\n~51G~iVbNf@mѸ8%GBPm G*]umFmϢW3ne28;-\ɌjE ~VUC_A~c.(;R1}Y`r2PK^v]s?)ny)ޜcB,lk-)`w&5egڛ5یDww2n!L g0 fOaI $}};X7^(8hX;z u;Ifl]0v0oV7cκ,A_N0H6eZMJp%@m~„z~$;NpBHvJG:chVNvhE_^ZNVZ|ݒVK{.22+W(B<]omj=@$<>\R [7$K+3n-9Va`bQq'kD8MKp5|vcxC;)\Lw`40óiWNgT#r#.i I\=6cE)¹ >о'1(|J0 Edg8} XJѬ^k+Mya|r̋7gJo ~ݶs@ImʲQ,1? [.\B TvhT97ϛ7Oۺn^:W8 0- U׫{U 2~gɷ9|-EdUQ|펿g=|&1ߨxvZ{F uXqMgݑvTt RVnl>&X*J6]-kVLw7[.Y]ك jQH%5rzA8uϥ¹]vO5!L /2qSb[3@B9ovH=?>[&i.sъyY Ui%:>0Z^}ݪOiu9חP*nH UWxb2Rl_kdۋY$|LkwŜڜ7?M'WsɴZ^OTnrI!@竵CbiN]M}Q]Rv[d\olDF"YrTz۰8M_6I1Qoہ%g&tZ;`/v(pd*J0K)v,sO䦹"R@َwWo/`I ʳ"dw2sNVV\}?ƍ! ).;Ch Pk2 vmSynQ1q@߹sۋ]`U ԥf!vK~,rhAhZ2j~S۱Hei`FO+}n5ά`VoKUiڝi")*kY}-*=AEZ=+Sп|h1YL|@t \|ؐ3%IoMJB^%lAX,9ANاDvMY9R}A{8.Ԋsaŕl=#C-׽0v6PA~,mTn#fЕH^FsZM밼.g ڎQ<~X(Q)N &`Tkf]=&_zilsngV`jfدW!~cVė-9[ٓRfQq]c(GA~@2@טC܊F*O^Rx}Ӻ_e;m?IANQ\g8=VzW i"{(YQ.7튫ǧo/S|J`0#[%_Օerfs+I;c:vUBf3yELʂѼa@@ 쒖b!Շgr{l#=rG HqWkM1`A+Q`gDe:Je&vTjP&r6tma2RAb篘)7B Fp 4(O4-I1v]?oc+( ~L8xw)>%Ơ,[# AU,0j <1M.^F,\cڣϕ5Zl"`I:5_WU{3;?avO"ZLrNT<|#ozɚ2d7h ^0%h7Ӣυر|@[튥@_``Vk l7u_*18t ELw틒2ָ5 ]/C)k-Uz`˾&f9rsy)1imM39 Y|eu\pǵ/'o3j:ͪ'/̄_=pV.\Dfc!7OhN"HhVZ\{j{EX[*J;\xc-EBth#1`f_H'w({v3vZ g5v0ln;X%X>z+[[֮ѧ/ G wW7-K[lkd}׌ѕd iâo7 /AOLM_-J+#QKxLg!nكoТCD@rqn?8:Um|XG"s;*ʱLD:(f3CubHT)#=tz Xt1߯ײS#uǧ7f N=sNfVkuGݧtl.tt6IaE}J*.`6K}b&)hE8{&m G1QPw~Svb9 h^bd\\(h],]zZWQi= 'Nt@뷺e7S07MNޔ?_QMאS&#,+i\֣ǣ@^W-ε<[$_87Ǝ^{V 8F@07i_8 p+ycPgcU<'A?/2 X>ȟ((B1qTᘃ/Y_چ,FJe a2]V, /#_ϱ? g@0 c2rB)6'h V\:OGҔi\lfh+kq\gtrZ½9uK0'G0rG"ʥ&+P36Q_Zм{I \Gh`Bm6(GrVM8ǜXTez¤2l9;v@.K(w\?FfcGdUswhCym>_Ϳ[WhQ }m I/({Y?^" =뼂3H",0-S둸ŀW@ IJ]G1~% XHŁBTԂh>i /#j V :p+Gi=*"gЯ;N;=;Q5=X9xD9@cE޿&]$Fo5NAyGHP`L{kcYRl7iG8`n`\GŪ O@ p G7PL5,&U"7H*~+JHvWʍU Lx3Ysrg oZ2[O i6+(6j /R2l*+(3yȓo R1gV39CȍcP3 ǔۋ?}@o>}k &cHKÍ<njߖ8ld}k'/ht >uuى&UiJ`1Mk`}|Z^Z#Պ?.V;'z2Ck= Az~$+X,2L^/gkޱJ'nh`f볁/m?r~`fOb1GaꞚ8k} ZbQbt8N׀j?(c/j|z֏ޱ/Lx-O=~u]e-UT zT6@:FԮ GV< Ų)w^!LLH`pmfEknLF_^Pkɉcw7яnԣxcwB6g 1jg%*zrNRw2rLt#`$`75.O&YX tGz$?0'3e[3Nv~OLt^t$ОYI۴MJڟkN|ӆs t,,kSKY2Vqغ zUuȺDoK?fq'З׎_z!_8m( ~^)EI9v.3(y? R~xLɱ-S_׳+DZ*6^-@ SƄ\llSEr¤ #+fr{p ڕeEz-\o"Wx nm=}ɟ3x.ELE7D.ZBfsZF H z=+}~O==}- LxV=я0ٺ4qie$hɷmXHWQRNl|†v:&!w(.r$,>EI@$@Ql@Ɏ ܛ\xtaH9-'*9(k5bD;j_NtKnϹnbS41f3f 8 '9jk#c:ϛys?Ek 'Z4z8]3r V 3#W„U* h1r6v2gLb>e.~#Zqo'1Sw$?o;+߂Ycil`MJ Y'ܫ&b %^F`Av R9y h0N0`g@&#+;f1Qz΄5C6Vś}R2$_\f!>^Ūg%\ qcZ=J(B|mgW{܇=3[J~`;,uWaK>ڑ7 w8Y4ԎVjIs@hŹnb~NM)yҎ(>nR}NP,7Y wmx6+l.`ӻ)? w9tx<e[ 5&8돀o/FA 4 4oyy kDD6.' EO/b_O^pqp?qž@@ "G\@"WI0oվPIv}lb V bƧ-~ӷk[JŃ(vg-^Q`xi47.77MG@n?z#onUthzUe6+'gã(6|o iBE@ ҫ*UAgEP&EzQ@QD  H-!|'Svbee9a:sU\}M*?޶~2G(aR&}ؒ]dU)a)o㏙-E ʒM9a|(/k8 XDF;+եQy%0iGVNcT,c1&az] E+@:緙Y^1_1eYb$"7,9׹ktunvE]zܐy_& b˱RP}3soֱpO9"-\YXGc2"(y*Lڤe>X;/y*hY?N@) JF2ЏD&Ǥ $6W9tIT+ 4;})w] mJ sXq$Ϙh@|$\?|?0p=Z'WssІɷL9۴R8R XLELx邳*C`p]Rm+'650(ad*Cq,W=!gI(Q2cQ2.[aN}m<o`/ov@rfX~Q҉Izv~v''IɾO7$ = = cln'cnAwS/mCh#P(f^~Lr1¼Fb 'bё rYg$8"%?p{8%Ѭ{檜fWL3ID߄Na>bUra E7=g0y*a0y,iipX=y[JT(Kb=If&-@ӊ"juN7U\F^w"F&MHqR 5K>?9T J͎7XDǠsM{AG~Kّh'2'D &T/_t \K10]JCEܒΞ c:H>Wh$(@ :~Iܒco( 3#Ւo _vBrȾG vrRYrUájZ`B1ڞPd!OhtrIdAqp l$s,S C;+O6?߄࿋0h{ &d/QRLl0Z%͎@_BaS"Jܒ(>I]5ݯJu2qolAXY45?IʄB> +-"s^ƨ"ȩ:&n^}S~v(jJQr:xB-r0&L2oFܼ #s߸>swZ+Bc_/11TǤ9GAXF]2m.xq3{t<_ေ]QaO?O G@Bn?n##^o }J᭶?->wfeqa. #p8}#1l|Q)ѣt6к*mVb#>JTCBPx{]\ئo+ZFh% wx7ߦB0VUz(j&-̃wIqfլ|CfPmZṪmR'l1bkɥΠp~kʊ(Fݰ` ~3/kFI7I' ,V( `d%",>0ǻ@0zQBA} X#:(٬lḞ_1.>=(W̎w^ gP w)dzۯV?\ }~yBew{ݏ83c2_4pT(m{>O r4tl[Tⲁ9VP1nqLk휲 GZ3YQGIÓ ĦT^X(ٴtm|&ubiqxf̢[5Q@8 /3VX6TX[ZȼuHx߼|?TzF@)?!F ~W؝\0n#DI;+fq|:N$*&.>/j Kt8$/s'3 ُCe͎sI{puOԢ$s?gĭl)sqdFe8 },Jz B?rR`&WDݹ$BeJXxbrrgx<KZEO?GY:OLqvFd'LZﻁ IDATMPK#Ϳ KSXRo=ErAբH\iB`Nb4u@jmx[ht̊ Ԕ9 />}']>  _X+a2k<;"#u>u3w:\tAxpC_(7E}/x* _9@ŤX>PFֆ\dK:[ 1<&%yZŌ@ 6uUi;3ڲ5$k5: e͊ab#ZYdjK AIa=h =!|UV.uv1hCuJ*M+tcDcJ XP0̜;J¼}#;U2ZEszr.'4%&R`̷$^\gLފ|i>cZ%,kG%?^ֽȇ.0y%=XZh ;Bqp$85cR>Ө}VnR¤].\ >pB-*-J?#^nH>T'NSW̎=Wn9io?"!"M rpﻰW(@XlLOjCÑ 0a 0LdXvcV}`\S[!ɕB0%a|Y"\ 3?-˾n/8VUp-n_Vt o}حNŽv;`mdpt+2g)8F8R:N4Ok  QE F痶Q4f̌;dgn'Wl_"aƯƼ^=<u~0Xl<('``_uaSL^8 =YdXBAg8c-v* }#$vʅT y8lx}$!TSox'}j9o X^{NƅhxdtKE%8p=~,Ư/k1$1m"+ʋQPS RmJ!o[JϽk̐L˱0xbQG*b X-1^U (#f} t W2g{vEPXfG C!h#XY"vvY,=kץgf{R%H'N /lj6U3~̦C0z7~,fn =R -JABp* nM?ӯ`u0e6Ֆ?Jt#6UrB4~׸όx#P(zS~V>V¤My|FM*I"t(V_@ ̎ƳAp_0QfN)%#.g ]9_orݓRs k1Y+]<NBIFҽTvJa(:zZ|nLnn)ad+۰cMX>+ϲ@ήft$;"Y0ic>('c볽|E7rsfWX2XqYkvDb! |طa4al k{^HX:$m'Wb{k'rJeˮ%Pz/w15q]CWVbAU΁1!m}+QfTk8 Ƙ<BȂ̎9ꔂُe~q6< ێmwt,͵ײ0+0rNn:$w;&o p& hZvowH̋θRt%V\O6>>ZKh r~۲(Y >r$Mp%`v?C,"$2!#Q25ۛq+I/ 6̍8{${DH35; rqaR/OE2uK;]~V0+w8RlԷo> OAX7$ݘWnh7BAR M2Ո1LVɵMΞ O\ /u)KF 쵭F"d.} ^^jݮ*gs: dU'N:=*&`hh=sڌɽN9=V¥{mB+5gN5`|6ѯ.<^47cT)6; "뤒yjUo}oPVEj~jvcm0I,W¤ҹ 2H|-e~խ*:0S8پߓxN:Β /Uoxn~)} G/DDI~E3ChXHb27]~q2J5J元o Qb( a2?OAI/ "X汝fv$ ofA+P-i-6@aݚh -yKp5Vާwrl1$6A2n%OGՙ蛰xvspPusѯ3!y[#F6) v+_幦aWx'OeoO$bQ}u%wgfGPKX/3%ՅIg:&sU~̻+ŊN_ȐcvN4mAR ҅ˢdų7$rKcbv$ ofUŞS3k`^ϼ+\!q)pЎ<9>*Ł+/T+<#…Rhr!FĽ}6R| Qab^V +՛ELxܐ|h/DI' O-A\ K؈E8ygJ DWw~SM(aҋy/ ?xP%Q ۏiy(hnY eC6L#vó*^mvs}B =huvKXM\VxfCu)T*a>zA9R`GrdMP(ş 3R=he*5CW,Ko۠h*80Da9v9d^4Kv]\tu^qTll\ #i r*Quڬg7Vxa{{8GzVp8롹IE8J_LBmJb=J_Ê4{}"X5oloiE+2߲=OmBЎ;rkynpl LMv2DìdiaP(RRK_ YR7),͠2z:xlfZOץ.zrK-2?eR*t7ZyYf嚤G.}J^=ˊK0PؑɥP.U8ZT4>ZJBT^> k盉?yl ]gYxg9٦N땅ŃK*#wW B.ՊŒ~o ;jM)"aS2S#%DI® jdqitF+¸?ϊ#cvn"Vƥ> \9aI&91 iЛUNv4AƧB*j+%ƟTUKvvaOfGP݁fމ㪈gD0}:TJkP(2~LB"tDI }[yM˿#㮮DMfh*!D;ʨ$&KK՞IKqas1C*^\|)t_ "zBUsM`BB0f UZV*E7IO|T,me\FwM]H;"`,HfqwA#0QP jv4 Ez.FL>$AzvZaƤV+׭ۦ* k c fNL }j7p|1XOc: 7&&ð췓 9r!~lf.@"hehcb" =d3Plcק =ó.ؿjkF“sC+5xۺւ} x L ^o>ztР|ƅo.#vS~/b\nus&aRpQ* JĊ`婎HO<0c(cыRi㨾}np@mH$<\{) ,h/#R(sGhug RcorO' wK{+o?P(sͬ/̘X/ eFBo2Z*deos8 ]'i5Ş%^]&nNbD_鼒 G~41Wd}r)|o} _C+'yY z-%L QRX}>Iʎk01x{Ny%[g^cLWi1W(|ɽ1eTXTC~gs|?O%O5yZRm&;vwVB?Q;BϯQb3;s eOf,J^N lll^a7ǫ{]ךS~ָͬxoBpkoT>g_<cZ\ݺKT\jp#[^i urXԷAplQv^Mp !ŋ׿3v^aP/%uJs"A >JHyh,Q?(!K1xՃ hRSW$+j,U1 /Vܒ]0~(r!Q oת ," ~du`T'PX7}j6v >vfmqS7K'em3&X^x=9aO3 aP(C2o.fP׎o8pg/}uX/#ԶͬwFZk[ۍ i8sUzeӯJޓ8?Ȱ IDAT ¡'bYKas̊ڈibY<2^wRLNb^;hj( zEbp2[5Q2o 5RtྷRojU,vv?ZO#t% h⭘-tEf&}n` ZTxRya@HI?po{O&\a%AWxm(9k^U\ЩZls`媥ܪdqXH0'sQ(Q2 ݓ}]|ν9`t(Z~OL&(>(ׯߔsuS"XMl|6\cuUxk[3Q5+pZM?'_Ã\wn J=2D NQpFx􂈻k˿)rJqS\<\isDئh4,'n!5u!]'^ TǤq /{#%a 3i:~86QFvf9`Vd111[%vZ5rwĤ?דZƪܺ-ⱮȬPnއ:(Y[ bP@zQ2RY$I] /$J&&‡r* fG` v-ۡ@K'ڂմ >y|m^< B?j,CZ.FC1"t(q&%CV* (fs;j' g=4zցM-'Y [/gcQ'4_7D =~ \)Ԋ \cF]o eV0Q|t7c×K㾼jX~g#{:( ;BoX^V:IT8ާkSxrBHK-;; &xoR\BG6R LRةnvXS($*|A#o?|Nq IE7@:?[/|cLI\:Xr=Ab='d-*x{<&80#,n~AtΫsۡ7+I {Յ[̎~DK-' *;goj"+LE _#8!!sܯjV(vgfLnZaVJTya&s'Yf(Wl왘0(Eo奞}j?Px`xYٱciZh&Gt2ܴە+Ӧ1;B:&@/ޓ"J2rluzM9[5+"C`0d&w2'z> U߲D>C gZ>HAHHNsL?e _ŋN(QRkuOɬg”͎HpW\$/9 w(Lڠ.;a2T_t٢Vhw{1Z=ޫIsgAQ DfGP(dV/mGw .2ge!0/(9!|V^SZZJT('+c*38D;{X>qؼ/,Y%3"8:4; ·\zAro6vW ' "Jx̀ɼDIm7gYkۤYW(zƚ}慔. :۲fVB T$6.΄5oR=kcK'W(|`A3]l JaӃa3P&{k,9` }Bӊ]yqv^w)V#I|t??UE+sLzҪ8Xfx5,$#C?7p*lrB!\8ΣcJ*)I|-P5}6(֏Β+'/ YzPΛk1뇢Ro? G\t*[)JrPѴSHpF$%Uk}ԷrZ~Z-d^o' \C^;!izA:0fJE:=,&dv4֡q_E;?cq>55;8חIUJ)Lb"ӽm8 S7=i%͐L"rgF ,̦x#sܸT $ }m\~V9IAfq֍ts$=j1d=0Js)[=~YX;29)jz?{Ϗ]"JVyEмOɜs-S{O`/٥_1_vt w1+=DL~ZNzfGYJ}`&4k4 3sh&_k N{b:/W[I-\ljWqBFᣎp!Z~rzq+yQ+T}@Q¤"CdEw*a~9&ݑZga.rѫّ(1}+|$YRzcR>{l*_ bUq񲖱#@Vy2}x;2-wiUd5FuL*| d]0~kQFw.a0f s΢a|G5GZ%y=w>`Ee:zcٞ^4(=kC/\rLI*ɢAkڿ[6:_;8* 8OY"a`xirO1v!,HtwCj9Gp"zoƷC~7b ~?=-%J wb?0xsY sB^+#[WjߎP߉&YJKQ_Yjv$'vrAҲP JfGc-_lݱzt~ܘ1WBLXYRye>Qd5c2o&6'/C/{j+ 1cRT. uJAb\_mvo%dPp2FM:S*tY@_7oK1t7I%wV!uю3yek]\X-S~\41~~+Ů  ?O5N| \7*wxc}Fu~0g|3ج͎@Փ's*50=V oRp̙O^dBd8|yBaFx)"JH:ƺ8qC/Z%ڷR9+;7P~& V "_L>yfuBFpP(|QW=Dm%,Ktz՝8.,.Lf5c20(S& Dm"gWU cF?+*N|<( ґ$,Y!޾:|Y5.&ZW!kUi #Ai/=%]:)Ej޲QuF,bHx4Z\qʙG5R6!Q[\%$|4>DɈ`ӱFwlxC8qz$]gA` #ɞ&A/Έy$ߛx2#^j E#Pc$z Kՠ^9A _d<Ѵ"^ġ3oy[쎌.p 2lFs;-\:1/VkVq0kY?G o=R 2{l[T|E8K=it7 .V kGU18`6[?4xyDWkO9}MUy?:O2V>"XMߢYtJ^9Vy,Wz||zPxK1Ʈa#PC,S4vk9;GllNy qEQ28&NxyӢ2~L7kC-* Ad9w9p0p2͕j/w=HٕaّO΅/SaPKJ5nّϿ9] VvZ#F2131Y,}S+ #3ݥuU0TGZU.E#JuYvJgݼZhT榰"LLfr' G:RgXAGfG>;ߑDx[[5d8(ƬxOcIZtz6opҽ(Y&|?JQkťβsدGaaדM9>XT8Rr+N}O(@L+~U¤ uk/v=g#^t52Sחɜ=-I@-U5T*IZUdK3WE$&b2cB?]TVnՌVsKy e b)DzAEFE\QG0xX׮_> M>oQ" VU+5 zJG _# cIOF8s:M0^ #Ւotw,"If`Lcm1a'[noU)yZi]f"HNw?HNK0ۍuJ|ijנ(uIFC|b.lWV >h0"s=c 8'xb'-E=J4x!)m2 %dۏE:{(F#>#ɉ?``_ wZ?͎žX=UǤ7ǝ`d#ѺJ*KYѰX2.IOS.(ꗅ5&K=;@鸼rV5Iύ%"24YLʼ))-ӎqZ$zNMwEBBg۝N[q9|SuԚ@pZ|{GmPcF|~gnվx%-]Ӷ?T,r·J9aٓEߎSRI ][;?J4 JTdLB+OU+@wWX#yJlAĮDlNಚah)m4;bFh, M*-%L*ђxӚ)(lw/omblqY1k~ŲA)XD?Z=mT5Do~<>|m{^^r 4csL\"l'Y&& kg 'gEʽwĩuKI篕UR_*<0WŒ .KwR?Bg-7ցuiA?Oo ێ_ EJLOI筫\Vys렃& E۳zB,G\7hZQ"ɸ׫Wj!sQ[f%׾&l4: ^;XC5 ͠rugS:tj,!_K疑T($I]67z‘|8I}0хb%9o4 ʛoQ6O/1O g͟Xn&]提wf\ jY1% ;uEf@ BBMUi-k rcыQv$ 'f_hEa2Deo !";cB:&@|jtV%qCf9[=b Vܟ*lT[p@BP /(kȜ$¤B^_ }vo HFm>>(ߟg`tm %LzJ4Z%l>};)z@jɎlɣ{Xw +7ZSYVLy9=jgm o"^o7u j,']sّ-.NC:\pn>%޻Y!ݟzSeހ;NaArK0lVBD`0|SimwV>|1"F+QZ5K"]u;Bbb5zC} %L@d~aߛ'V:,O_g{~0Q{Qrχy-O 3Xp+) x4hV/7oJ; kQɊ)nb7l?s8'Џ^0iU%`[ڹǟ.B/H7 Z8{6nm"^3CrH7Y_ W/YYoCɷ8*bmE LbXVY fnH0Q/DCO7 6[ g:j1'  _rFޔKՊ@0iaB겓7]lzQ&,FwvX/vG,J3)%ɬ"3Rhat7`Dd5J{Uյ{&=!{ {"j}_{UQAHJUtf(ޥCBXKL9sfΙɾkdڞ9{ggEwOf[dB0X/pOߧ 3aB_#.ݔx6wK%}9$P/3^~a~V1ZE`ɠ"@ۈ&]ExT'o݇ɛeƊG/ڶ(&ٓX-PWT8 85 vEZUb->Fh|?XϼU<;uv;y$B2I$4#CK?ܷp1_(aR-@}H.`Kɠx V%88D:%z(JhXx _VzK 8S|Ѱb(%׷&#Ѓtָ,hs˓6k fI]g$&hY$𞒹`~ƱZ`BwID?K}y%@HǸ j@/=?(m%V+':؋ze?Vb;7 |3$迶4UEȚI 6c\?p&؜Ia(vf s kC8x>,Ѱ xxyOp+{, (,G:')9p$bkUy*ѳ=VeBa#`lc{{@׷t[$=y&N-BTǙ]pV(|& Kr +U!˾t.%EQVkfIj>?6o;5/0p9z7 RԷߋ7:?N:HuߤZ.-w XI)[$)$E0{a$C,Zv[M˭h0R]jSx0,\}Luz/6a`S V 4( MK&~!h3Z*:<^N̠ %'#zS?z[M=Q o`] JmWO[FtKoݰV-5BПUSvT@U0z9$Pգ~g K2lB5Պ=e*ҫ03 Ź䈒 y^.}a!G|hW(á闷[SUm-j_ WݰpX$NQ[ȟYg+Jd_"$H]LJ$+7YrpE'}P(b}īMTaaX<*V 7IogkHÖ`nͅ F>f$*qψ52G-;-a@=ټ8Wڷ(i{ۭmvu$y~Ns$;˻r~g0+č$9[-VKx9|\usXAĸ֦CIqD1V) o݇^G7Fzа$ܼOMU{j8tr^^$;#LzŪG JL;'6+ZD6DKkM,|Jz,|1 %o/ǥwidh2HuEz_pZڞ_MkMT}BfY$6y/!Ap׆~tpHQ/B%?P0]Sw!YT?#E:"_4>4%rH@z!~TLMlfu|:L!}$Q}ƶGz,$뢄VQ2<uۊbsT/(1܍W)gz;nTvx0i,f樓Þ`Jh[AVuBdӲ\m{BF#@ʉ9+λ>n< dzüG6n]vv% f)tʑKh6/PK۲XwkRՙ0{YY-٬MPﴂ\?>Al3Z{ݥ+U1 _H ob( uK5O"[B/´L~.݂vKY}}g%VhBy08/4!V=ZV^lWڅN#ma߻3~,h%Z$1'%7YǶ GYjC k.oZzJ0Ԗ?͠8{I&%ּ(S]/e>S>oz3N@/G ϲIi^ ca~#L'JF2c0A>A:Э%c@ą6 UGJ{Kj%ʋSJJ)=VM&h*K=+*=jdvmAпNbō{b誳NJ0+dLm6ġMF Ęd='x{t[xRGkǢd,u)yoTT'Gwk"yM=i:f BXU}jOuAb N 4xL7/H&"*&P Вu dIǠ(XP(H^(c"{e1{SAxT*fƕ59x^(u"˵lX`/?mp~ 6 Hg8yzDD>(ky6+B"P.Fفy'! $IjׁߋeV[0N aA8o:$WFfGl|PaF2G@4zA{v`:O+i W( W\o}Q&i5ԧWnoР$\-;.L b26\ J/Hn܃&n=^72IG`i:1ovPa۬4xAP)%Cϛx[9Nwg+T}kCp6+Y1@„/JcbBb/8ۧ$C^k 7%A,"pWAX/gKޮ3 G/% |$\4ں޺- cՂnOQC<0ӂ@g2^$*`0(iIǴ VZ5&ow>ZhXR&n3?.C\2OtIhBp>p3`\T=\Jɽ ELq#jhvgPxW\n}W K=cyޛP}E[2PvIUv&y^ oh?Y`IN`4KJ"C%1#(vt%ݥ{uhph^ +rݥve=-"CjL!*5 'dLoekR>e0{IU (aOS=o`V?ͤB~a)EaDxTR}\<ߍɞ KԷY,ٷ`w7VJWwWY4@(ItV3$ Z,9k k˖t;LZ0MmιS8ˇm5~cKSRV7B\8 |k`@Cz۝.Is%I% _t} g*EH֐7SrQroft.6"$,]&ge:Eaf(sp};J 3dUK6]nrk;Aҩo R`L\? 37z6 H;yϗ*_VhZkX,7؂l=X,Р|PI?v[U#hd u\ M_^Rl3xO\&ߓt̖l2a2G\%E[1 sqq%.;NJ24--I373<`D_qC4 -|B.z*<ײm_x#֕{#$2Tc-&kw,ij'G( _u ڔ܏qĦR߀.VS7WFwX2HA2g^l¤YmWcsܛ-෣Rihf%>u ^+|:VJ[&~I+Eo^I0>n^F Zl2"6N/O-o=!,K*qT1ڻX-RQ,-%nS1JPwW1-WGDzWV=ý9w_g(뜞5r~=>lk:HP4;,<NJz.M*нAx0m'\[ Mfyο#Jo,n2lR@5QPV+h[y{ ySz$EvcbW%qؗ إLQhaO[&7,$Iwþ]i&mOE,KMˤ#'mu4 6 y*|_>8b${nXZ:Rf 3{A4zK'B8dvzR<_UE:"{c:K R 0 B I.(<m$&û6>h#)IKo IDATH8s  D;L7S@+cZJtO- dMRmWn+4|0W@oc*?OcV|QYIg%O &o1|W8X&%O_iAW^l;`l%5e5J8_W*oO\F'Ъ1'rV<,(^_Mq~/`{IyJ}bm^[_E Sǽ>j Ϩ#%B ~ln?qXgHhXRUmL˿l, 2f~Z d3 ŋ2oشx`Yh;V 5oBZz%dd$]b2}",HlI1G/CpA3TEZI]VbVq#əfi%U*"Ţ-Ɉ^#Y4/Zx(dU ݐGKbQN,z1wyDz%`^ۄ(˷)̟%=yz yݾޠx|{jꢟv໧B/.k fm00>adE{A'V8C(9Ds[^,Z{9E8DE0sҨ2NqxX%p6kܙj!i/$'{e$L)|E Lr$p-[q(]lPCgEɇqRtr&ġ_{Vߪ4X71-6^I, - i-295 uoa3vcD&WHl)ãiC1%1=-D#Jv}HU?YM+zzM_~ٕ{ᩉ}^WzcZ)$*-D 㣟Qo.}Z8gXC(aG}Vz"w̌ w[=rdx0,.}}$Kd%smnR|jup}әAJ$;ܖEP\.OQ$s$V韹2 :ŒG/Cof$ ~>W|<<= cVk Ix޴V(!24iRsS'DBۊYY_>+移nI{Zb`6Y8% 8tQicA%xIQF6¥uBۅ\JX-JcMbX\<<3 v>n>m/VIsZ|}Aj/$ -7vܨPC8v* @˲4Wy9)ӶH@gy9&S@YkXBm~?&$ÃR aA(%!3(M~_AW¤rT".* ߃d'Ş-~R߱Q,% iGHykX~,.(Kqr\, *f ANCOQXlqL߮KnɉB07={=D 6z)~/At=t.q.BhY-W7d {Am \ر?j yV L s2F ¨n'JX!yRo3gFɔ|Z*Qz %9~IhR KCb3*R:#[Vx~uۮ:xY C оc)祷ϻل@dy*[5xij]$Ć ?y 6{$LxTH1.47^&AIy_3azR9ݛ7HZY+cE ue]wwpM:seas+&eY`),nϔ( =⤼`G}ߵ;"޷e\wmqJp:~}R-=Q(HZ ]lYt|?ʔDʱ7%o8?'bM>"w&qXJlbmگNŌ偞5L׿/5:_,s}. \7>)v_,U˟Dbt6>R>/:ߘcB'?W'˽%Hp1vX6:X1Dͤl8C hr z2zF&}[7ߛ~ZAhVj v>*zXdZ-кddy\TcO|zj[5)8QbOډDbXO̮_憭Z7n"޶*/Frݭ'dW*`^_I lêC{ZU6saP9yK7Ezy,3V7+֛+` gs\鑡ЩX.*{`\/[u]0 Ngx{ zI *%Cv~ogN`iWȻ}ծI, ]?6Wn&N+wKcP*@1x@zul-)~z %L1qqϽFJZAVO\"'gV$Z*0E7M>P0J]`wLMzp/ڎ'f*|65bCp t"l jAs*ȞeٸO1/Չ<`-h`ʡRiDT)=DlY~Y'L.#hTJELRo}P>zf('ޭ'\K,ܬiW=-_ _1e'X^^fi制Krނm*P&I P¤ɉKՠ+Za|Pɮ[\lS+O{:WymcŇ4ekc4y᝖-UߑL?&`Q(X54yxg,"Csv^cQ+`jyrW{X-'H@G?Cm̐/Z*lsF" и$ ΙJWڂX .PaA6`XUDMe@+|F%/U_ḠzJ`jۈCȵia8} 6kN]u]4K}ڞ-OH\Ub3B˝Ier'=įmvdxp~OO2ֵ|$I]){,k `7Jg^F$pY(3n %ؔ)b!K8ctZ≯&M?i[ i lې3d%-.B"Nܤ_ [O= f(-O0wz;AGĚa2/o`kDz% 3yYh:CB_$tG-BRX~h?%b-Т,d%蔔$`/;Іrmd@/. B0!\ǹȶX$c¨d/eywO%8)M`{ϿtM5nĊ~vYFT+.mB^6Ы`м 4c]X\fRn˸O,q'ijٽmI/0>G9IoOV T'm1? 3JglwwZ~JY9 DvE,d׌a  >6N`"4>SY(a|u; VK ?Qa^c,D+.u {ɜ&}%]zD;kXX&*ONIfN1%;׌`7m*H3WFIri6 J8=RlXaxqg{g=R 0OϏ^s`~Aﴒk $hNeop % k%K?i_Gh=z K $X9 %!AghZ~/Fiwd/i{^CZ3*^Z>4H{6yfzʽ_ǡw/K-JÇeEK%IRuD:V!{ߙդ܌b> (adsBELGB~tX$t'؟?zNP@c=B}wv?cg17+aҤ /uy L~9W%7u,IE' ' fyyz]-_'fg9x9voEgHdPK j6 HгLn+WG-;Y&2M3IqZTs$8Cy$$ӫ]a>0i29 ǺnW#:CTF uFcMP&&l4zf o4}¿)J&p_Hy)#P,(vx lKCr3VL:+Y,bc4cr1svpAhysٰ͞x[l,&B,YYwh҃/L7; &dYz7GPɑRhI";adpwm&kMցO&^xk L޺o O}IqMGP+ڔDYC7Sø_/Wv7`ܸg b'vgpGa+8?{k_}HZqpFNV7a1}xkv;HKD* 3I5ajۖF%|sc;bJM׃߾lߓߢIq4uaI^+#gÌ?D^yIL+],{h?>7 I)Sl O\A?xwNZxSDUàr~gY,szm>u_ˌ닇NL*[$L|V%r6q3~#E${z9Cxi_Ilj{πԓ/aTXK;IF H)(f%e~R^¢3'ObSw/ֹ[>Z 4P\+kHbŦ=@}%;!P(6|e!^Sx#(~:>Ѓ\at7h]_X`³P\^C*k|OZI8I7ICO&HJ4 &#]4Y Nq?V2ܧn{.$VCioF f%mG ^jJ<)nH5Wԓ`j}>XmߕbbT/!D2\!o;N`n,T3.n// &}'کk/TU2'wXYX#}(FAZ+ IDATpa2(y|X;k)gI 3`Ϯef$J0Zv1r.1lpѳ0"IǺeς 6?Ö#F&5%]+a:ݫÈ.=gOU޴fn/`a0 Dwn@ӯ۟Ϊ\L 9 A{-&MrD"m;!}#*僞jhQ,_[Kh0ߋ+%eajAXY;"/2fH /L)|!\ v7De`AZϣSIĮgHN]H?$Œ,P"l7GU{YL)J޾;qϦހ)bjDDErw.JSW\qh>UBo9`nITZZ &<N eHۙr8m+#\;T$w&*oLdQ%Nv /6LSN.<19&Q¤ܾ/F-1?lfn$F?v!fo=r1WO&z0D= .(}!<G ?{׌2aр|-ڏVigL0tuY[ld^o>/L\^3&Kۜ+%+y9XO,bI%W r$0;?zBھ/Jɤ3> y{}":\ħA೎oEBP yœ0m?)q~Ix~z㟎Ǯ|LPQCaf~p;ࣧlYRL(a S`^gb%rٽF ݡFa:󿅖ȓI*$;=pC5>uIiнsC^k*,V*Ȓea;0e3K}im}+m_zAζg-UסtdlP=s$Az4()`tT 3"O8Bq0yx1ǏWx>-AXLC3硧 wOcܭ{f.69% _p6x4X(o}mEɰ {.}Jl 3 af*l4+>W0TaZ0i{Idsp%%jgw~ARu dPZY|!mbbgkخU$cQ`v%ֆoRJ$27K{(aX(ㅥ@Ǥ7_dd(t"c,ᄒ'U Օϻa6Sa0wٵkyn98PzeO<8j3.^0@~ z0fMh&/a^ŠL.(K>spm9hJҳJ?%~)W=om J/;O6IH?UWy~=k̐9 [{+i$d3X7~{1'׳҂ I9*nq5!0b=pSFw8U"W(0SŻC3-*=S\ذ*SeI' ̵K(-U4DɫŹ*MkAjE䀇zVIBr(d zIOgF*&HwK{ZW8sxחmW*S2'Ƿ8X??/s5݁ighUz<_fgnI/2r-ѳ<Эdy;d#ueXdSީ\s(^$1a#"ֲ Ϣ%jA%n0^>_)eo'q^ ߿ ) 2ir1se[YiPR*%atǧL6Q(ߧϛZD}M/Cч'QO# jϩnM[С24*e|+s.Chy'^gӑ((Ga .~FG:9&X֥m=kmA}JcElz\Sd@̰?refgӣ& ^{g1|3ͪp,,=2Õ*Cv&L2a3cbhAfS¤h]^zKeKy`?X׺"Jx"J%JBҏ:CBb,z(U6Iŏꔴ+S~_BM%G&~,|s{'װvFXH"I1ip:Y(Č#1g^:goj`|Bi~cg+W6]o\{`^X_l q*TXzNݸ wz!3e%P{dollP Ir}IA0Xp]'J50:SJB/y}Q-7]o"Lx6QyO{:jr~xr8!8?+-f%(息JG7~ mV.=!KLȥI!OQvXiR@ge%NmZC>ϔY0c.f{/hb@"й tG J53ա}E願^d|TE*7#8 _%o8bUP *6z6σXEJZEo{w+o`7Hpת<jw"A4) ?$D-vZX,0)I~ӎ=%cu9+]BaV6}e1 .ބEz9KP-JP-\f//_*Ñkp ((yw+yJq4)HmŒ,@ /HdFD!'.֓@#B$Kb|2T9˅r,aR+>zF}gYvvo3_Է_ Nzv?уҹ`P1|gJyo2{i?j KBP0V"7WaFQ( _T}mZ븸g E=ǘ`YsϖbƥC%hSqVmcjC%km{,BO;d!}(:Y3з䌒ێ\9~Ze'Bd9kww*/#Wlx\ $ 7U*e[+e&XIyq21_fP. "ɮ#: OT\ awUa EYg6.0i vz }_H9g5/+6hph8JBrxPqp0ɦ."XbjVmY_'2yg> B OZ$,-JԙM;'J'g8JH n 6X3k'->%b¿ /7^i,S@[5ɖ=솵TR6#%9B1,.ʽj0 |F*i0g܋mZ0cVZIHLޝ"R*$)3IX6ŌBVAߙ'tU+3{I5&Hoֹ79hm *&Ye3TL| K@gO.oQK *yvBY_B,0\+.#]'*{eB} ^[\;zBbݪ7/GdYyZ~}9t.U?.> ߏj{-lUV wH;"*Sk>=3 w' }_!V6۷p*:U϶^~})nW(gYjkwK8cgk^k~rr~{a9UQ(+{yzཥEgPY+N]?{+i\ yu>=خ`]9 L&uIX,0nfG B|XDZ/|+Lzvhݫ-\[O9~X1Yэ>b(^C}1jΖ4~7f=k%0%g 'j:_ :)huZhV&uIz>LM2\MJp[4p.s5Tf?ψpoN|-%3W9W JwZAư,wKŃ:KU^{X-#Wb='j 9vW( w&uaNV(̄/C07UP8+x^&77+X,q[hRZ4D& !J+fm'%mQ*l}z0vY-$.l;ulF,[>Zfl }/beo,4u`OS {˴TcazI?rx tܿu?y"Cz!]T 7`x{ W[Fu)͇av9xz#%sT62€=Z"BҴR>NloU0Y3HIa@9а}w›\|3=,})~U1{}PRz6*,EyvpG'nk)s$R`L^l*U Eyߵ}Q%xlוT hpE*$c4A!ԅߧjoY !,lZEt.ٸ:&J57wKb{|A*=s 'BUȞ,g?fU 3tIp"Ca9/*Z9z[u0z& 0ŋ„gaJ,eW@¤ KF6+9d-펭m<krx54?{`캓.n=Ӳ8-N --ޘIWWaem8?Up`^+ۢ/zݻCbٯp/nAҝ"gdW[_dN_n 3dGp8= 6* wIh7^*ܙѤw/\ۧǓУ( rN_$J+ɐ>ffeML}w[miV +2[2LD5uoZ{$ [*&USl8 kFuHf Q_-c?rR`/f] Fxgo"HB,6B0&4r-ZQڔĥrIo˱}7 S'~B$l8I0W`6X3~LCRx7Cn4p599Uk%L&n\}<_!^55yB^HMG+{Z Kd2wwwUUB"R |My[ΏÜ9h(l|J[\FEÌ?`a F݇ 6WΝdDHgPc&u:``]mef("Ƿ%|8F!nyw9\%sXo+6n4"&uigi2מx;L?dl%V㽇0g/ؿ_ UݦD|iq'PhTt59vG&fяw|`(M~M0~ހ:_A"&eYzšr}Lbֿ6t""6Y}@ ")]n%qY{["'>EwOKɪ%}UgRSb=zaoʏ?4]r}V-\/<#KHI@]T))Y} )Ub>0N Pc?EÌ?S ?j_Bq ɝF3xs޶9*&A-i 5y B/t5 G( d, a)]EGuW;(KƼ5&/h<2"ΈeH2Ÿ$ IDATk?"͑@x k$J[9rYm 1񵦲p H|c>ZwHl@X۾"~6qU:w+*ڌ1`pAT Jv;T:ArQ}Fkf!O{$sXwU'8m+"8PT-(.U$vj$iv/Al0$X[55pMH{Hb#6Naq܁5aOT>c`6.>tɶm$@bi?g=T-([/.}rÄ^PZ4FLY}P BNszYm(sv0}vV9t j|u^ ɢGa{q0wI>`qH8z޺gqqH1Y[g<L8nj;rs^,L"T;YHGzܺ/ITJ0dNF?LYW V+e˹-u~ f"񑀏Y,gnWlhc4mg/Eóa!tL??G=9/J\*av^n,"PM?tM4-+%erKˎol`H}M ?_![4++"bÒmrPk-~ ypThM0Y-'+ qhuy9Cmю_{L-: nU ʼ"GT[{}m&%8@y2w?\w,(V+CY*IJĖIo6O?J՞#M5EQf$^J ~\}g@Q(27IE־RQ!(M8HZ܉r-~ [nG%}$A}dv"RXeIL~,4u-YRPeC8DKk\܊\l{?\\|IK*(uؒ=Ѷ[ZeM%(eX' eⒶ1Ch&+hYs n[p)RWo[8gL9ȡrYϖ'߻s-ѵoMK˸f¤ /Eh-\XFmW9uXO5*Ii#hTJRW{(-Gez+uF#s;jt(܇V~G.*'yZ6Q4(!n˥[K 3[9,3t'<(@k˂3,PG->P,yƃqin!80~3l!b^Ųoѯad0Tعv3 X~;kB"}K'Woܔ-SJxkbIۼ-ײ;=[PP(l=k;jq]B%!4"z44z48@K咊#[L4Juv]xUN]%KFa3êrҤENp2 ||vb\ -O?Wqt,%ݕ 35ceޟ-D*lzװ|RR-5Rg)hU1y>svxo5%ekZBm*BELʝj"ủM9HdK$u0ϕ}}/*ŔA)_ɢ?u-q?I2yDԽ\JT VJTy0,k!6NDaAMmY%D#k6bDw y!"%ct5|vFLdry\Ϣ W6/oygᆧqrR)iMg5֏:_9 zL?ސLηT:eV[Em[.֫K>y"4%⤵FBEjToGBmMз\KRG*;R>]F!_X{2R5/.|?-}ap}I,~m$S))T-(d,?,ZbԊIwz3LG,)O 8z^ z 5-UxB$Z PxgC* w ~͈d{oodʊ=л{R[MJ[Z}UlfdzU[rC-^Z+ކ_Òi[Yz|鍿GPGTBV'69BaIU*>QRP.ꧭ0狒=sO]oރg}[ULM0?tbt*[#1&(˵M*t'$vx*f4.L U$J@dž&|*j?, Tmz1s^L-F*u;y#7 9wSoz> 5ҹ-&.v+];ƔDEK/X"*>\!Y{){()چ-tM'YLaeHJͶ>l+ DIB| ?%.i[Dj=Mq&KbLӓmJ=z>^XJGPe|c%hDjk)Q}- !1qLbwkmV*Xnd` Ԩ fry\n(܆{VмXb&Y mïHU<:S BQ ӶI^q=0)0~yX\hqb O_w twvrݶ9{o= =Lg9Y_-I %BP(؃KØnVfLTT¤uZaJ &1@Jp#921_UR~}i/KN\u~>fx$y?mHFlu@9=I&={E|qwŌ5|Rmg;LA aDػ /.f{Q.-! N[%Jy]X}@5\ғǠj(Ѹfe=G?+=`:{Dh3H/C^q׍C XNT.飫 '#T*R \1H!|n$tA:~זz_(1,Kii[K3wJU/0ojY&ei6Y*y#,oaa}$5){ӃX_"|3н~> /7;T1".[H{3B҉=~v~!P8NsojcR2\sqf[߯:f8bb΢r E{C`=_FvH%Sm<`~q9g8qU{׏8-ArqF7Z;i?W,ơІ ?i~ ³( VE,d ?3;9 xPyݣ<aֳu_*;'G vCBo8}3ݻNжef>xmˇ= [ΓlBbRPXbڮ3hLAb +$s&IhW67Åa0+ -qt iQ&_@oe21?kd-q7zJioۥrSGӷ[v?LHI`qⵥߟ=t*rR(@OWk"`+fb[PҮՒ#OCbmk+[眻) ;Oٿ:E!wtԿqD0乛b2*^"Q *k:Z|"6,)/ظ@aàRij r|Z}bU |п_g7JOCi{yXhMp ?s7ߞ&DɖYQBP_^ojq)l\WgoǓ$a2=#kڶ[z=Je.,=ēX6v%?PDz(/ bOVk^oI>gW~K-ǹbĪ"NFcg&,JѺExqb4n:K6s' :Thō|&{(xքQb& S7ڊ¼]%c:ICw7 t =Ýh\n۵7ЁaCO=y?JVz-O]}0L'cl;ZoV}v e6dzdܟMP(Fq.]4׼B[JeՂ27OX d.khOSI fGd; ~5燫=5  #=>jYI^kC$ [ÈpP%_3Nh0ij:Šwԏ PINJ\$b-o0U%1 nre}h3Aô =!5& Ԃ8<ѽ60~>so-2`1xil<*BQ>%_ت|RvʜI,VyJ7%;3/$@ewub|{K]E0',`Ã`b/ L8EpJFKoA;y2 `jwDЎQR)?W<'A>O+Uˡ״Lm OO#ۗ#;S[/J\ IDAT[t(KqC70S_܊I7 Ǭ[[L X&M)~7Cf¤AɍG෣w,0. 3S_ϛ o51RW|D)k@zv=hj~xn֭ {Ov¤iK|ڻ_"-_ư՚@xxIL`i%zB_g$|&%ҶyOݯΕ"x@ E_B؊^¤VFKcQ뜫H8u#P8B`ۄɒ9BP8B"]$[ݔ$wai MaavKTjX'ސ\bP}x~.LҸRRJ)!/o/uHv|`ҏP!8\)<_&_8|=]g8{}c SV>JRHp~Z2'>v!RI.X$}DNʥ[pPOB%dmsx^~cY$v랴{{ &]I QlE$*fO|A t$yҿkV Db5>*o~mhF]d/:喸|/5迿Sݧٮ]Tn wHfmD0Ȝ^a'_RѯP IR(cJn yn+u,@z"Lc~}Co 7Z!,(s軯ol G,y9\}.`$*jB{]'îTa`]Ē<@?q~G7¼\CD۩%*kL,|In sMm~ gc8>80<(Q|{w6d\0zs:|k$ےb2*ڌ~zR:7t*Qr/%R2b^;C^~OWʈư阔okwͧ-\ҿ>Cjum6, 1r,l!BK7&[7ډie7^ ϡzas̙ ϥKwZAjW:@ZKڝ,av/rR =x(/64.F.oGaQ,u፼FoOc87O-$j#s62K(uPh $(gj$U-yd F%EݣNL!T%J*Q0؂=z,6KR˜I\i.D5䭎 i9x6X;0i*CldRZSzC}9G"_3 %~Z")`\7uIxin ӨLbıA!|Q t^I#^-JR>9{:Nо_ךQ>Y$2>Noz9 ' qh7|F Kcf▟^[ ӷ9y`HC+/ǫ`h5JvHhd{L&궅DrVa7BchôzSǿ F`瞂/+~N04ͶD09OmM^Dw^" fu\6"%Fk;SnÊDWHSQLjӨ#}^~ZSI+m ly(:RyqOszVӮ7\=ie6[`T|aRoZEz\~hXR֔ӿ'y@x0 HE0Ig_qX94˿'qm(}xUɓנɷSk4tAL1w;3 i lX88x I&3"Nfeo&/%0 H64ǶLFɜRٻF }!6G+FQ\a"n5)#_@HN-J{3]{Q.vrF&U/[K$7{wx;Z )Q6Q;XsGZ=6z!xiNJȵmBLRV`*!Ob)穙3ArlV-(ǫu?#1A]bM`q.&sV˜_:UNҿtcxD^k;4TOO o6/OfimQHy,wM5V/Jb yc%sꚈsf{G*ax#M/C jX:yC%Ԕ:ՃσYö7>Y`QXO)Z8Hx[k3c48~K <#"N)b( JX~\ k(L6rT.(L@oFCm3geCĽ`B.ƠA _$)w ǫEӧr-T:wǯ1rf3T[ygTZ#Tz}5F EioaO&)bR{fOq>M&hZZĽe -DPTMˤ2Ĺ=<'oHEGk*bC7%s &>fX*| ǯwZW2M:p#ba6I:bG:#` P"=;S _9?l'3)?0fHPX=LQޗ`*{ CuYSWzQ}/.kx#׷1zT|>S1KF0qq2S.07`ԃ%Cݢ+;ބv2O "5(!,_ fۙ3I"XTzg8<]}pDpAtA@wz_ɱ)rH1$.j>W/2g%ݾ+0BPlx 3\P\P}sUGbRkMlhv)bH2*2" B"A ,+nǮH϶/ʽJz=ꪵ*9Yu5ONH̑\X=eTgAÒ0?d ,2 1(Y TJ"/WS&4,͆N:~?!u(-LG{i#FC2gXv"LWtcdo&0OƛJi?Ɲ&ٓ}jΖ#1Z%Q*}^l!i{?{˥^M/K{`RfċІ0끑sf|6!8( KJq\5>WHȹmD|a~ZvBˊIgKggZrP]( ~ol/wlmIXIivxڐOI =QxyW=Vw4~>h/h##(އ6we&8{õc1qwdbZ?<1YeVlBUŤ~wkP0i|=5|o^H;r9rm DRuH _u\aR:f#>WUӹFWX+"eM/\.UULK|Fz>&|N*K2`ѰMVmb/-9_-I?rKY\!8f奧SzP]kBJC%qӊ&aNa8j18u-^ki-e0;} 2H.LjW8V&J`R~R?w;Z ΖOb~#ۖq)0F6˄ia2+Mܷ,A l$֕3ӵ[HoJ-;Wb1x'U=Lu+A\g:X mNj呭M!_m9P+2{"7#lmIwhzJgwFsx[ў@%Xa8|Yl䵰6d]cl8(Lޱ/n`xJrJ`X1<P ˖|6;/è{m1䙚0'nF1|ک8 DL`{)͡va iҲMG4罶 @A*.=yY!WƷ-,\Jh/>f ^Xvi0am2Ai,n݇Q&w? Ed!^_:7|Kz|Z1m<"֗ ־gdɜZ!g JIwg!^xWn+P(WżtՕ;*2%v) T/m<uda7)CCZ=!rUa,3iANY+pda$ArBǤ%aҗt>:I{15/՟Ea65:)|QiEp*|~f 'gl 6ߎZ co0TAe$;I`)R8Iq\t mkf_KxYO݆Y۝ 7,)Wڒa n!?374gcׁ0dFxwqaB)Ǽ 7|.P[xt̾}=( o,=X6t('%8lY5WvH= t&j!N޼kUm0ƹ}EV`{a.^8ȺxlZ뗐@[PkEI(졎7s& wE0ԃ̾MF9S(R+_m Ko\A0iuҾC绮?kҿ=]d'lbkm"ҷ5_8)zW"}^h79 P=(SFdp"Q`T= K &F`6۶stCu]xC~$$^hJ|R%ٻ=9{=*5$!)꤭'4P*Þ*FώRVD=dw;F_}o /V$"7I[WR~+ Y ed?X*jmcX8@rP($.Gu>f|O$V+ُB,IyanxGeݚ@xk]?2.V뉵D -ZI_Կ@G_m"Bʖ%&x5R*?EsVI֪bRBR(bW' *ݦne/äBX˶_l MkF淣%y sǫ=M*0|qX:E^O8E`d'4LTJ&\狳jzR.ނa͊fފvۿxl!+&O\'> "/-'[pcI*#ՒbZۅHX~KyJudRrKpո@kqrrNt_3ff-i IDATV""l; %iq![O6#pK:ßa ĭޑM"%L* m0 "XE{XwH{ڎ+$@zJϨ=ge? 1er8(_o]^[?Z ҞSm{lMs®%t!wf 5)-k#ff0U1*5à);]EL]%nރ7*䑊$B zO^ >j#^S`{jh |#U2iUjzfwKjxx$3935`x#qYIX*rХ L?.Hچ:3V{?@be14V٠oMH7 kK?ga6t&^Nˏ/6t["CrGqEmEhlA??Eg3rhkK ( -084(!ӵaI9pQzjGȓ6_N?aGb"Lb3L~@ڍ7f_I]*[I=vbu* ^V$,A"0 vqL$~±^qqo&}7uBj|mWbv:NݧO0o̙ Tv|m*Õ0rJYzIT~a!/ K)/N}3W]DV94_r|HK0r=x~xסGƽJU䠺Ш b'=[Lb~0)_2S6gޗ_98i *VP0 cR?P{(x5XkmR:d[; &DM*Z?CJDuRjWu3ܧ2B>5zlW~4֚$)>&r[ םP-K/]J|| 1X- 5'=f3\ L> +xAl#ѳo~!zyY8uڎi1{Yntwhջ4_  U;C10~{]\0Q#B 'B'DIˆd0TGMܽ2I~R2޽ELA W`K՚; Y %onw'3LHm5p7susken+ޟ%Hn`xkI8k/ ,d) %{XbNm/0d)υgydk%(7.U(%At%&TI413b) y:я$rk=ٛYk[K˰`csB}moNJ+n/zetJZW(O6fQ+}blջ-IaPrt ;Pi2+Md(Z/p2 _uJ\kU%u>5,WB;/3rrP2{(t< _G< aVb2*ZzJ:0ﷆڸ{"½0[ҟSeO^ ZTˆX~BkH_,2 _DZc*u>=+&ԞY 2gIݩz"r eP(,Qdp|oݚl B;hۿÓQfqvﵖ^&L dIB5do!.+JQ7'^IWNa30{0kAyFQ2 O¯G՟_o+A0qlRv|Y,?T޵%F iL~y\!gXܗy_*']¤bL,t$֦ cI?k\_*'/t}S%xd% o6h#LN^1RڿTF_dתxFУ0[_ryl{OPΎ[ ЧrU((5 =;S@Hh9F, B `TŤ{1{O$`޳Q}Ʀm|Kž-%ǯBguׁi^rtVz@||I6 Z!K8A0YȕY ~?#~|/݂Γ̝>o/=MlW^LRc)ȲbqsRD~ZǴ%]]l%CeBm <l 2T뽵u,!ك֖Jc˵uqi#(kJcbwWu ~Ծ^l>Go@!hc#ٮ& % gc{. #C IwBPhuk+K7-Ptn@ڳí۴yuYl;zTa_ڏ׷$ɠ4n#UV@XT/k%H؍'E\y(~59Qr@C-?'wvǒCuX5Yov=[¤O:_a&Gpsfqe+RfsF}MW>'U{B 8~BaV T+ɍ[.DB1R9kmpw ÞHL(kB_tZrՂr>y :T*S,U( .aB>%ݑNOF V0{SLP^VԃB̒Qm%%mO\Se(9gTEsn :2~g-HҾ"L !aē!F?-UiΖvG)!XOM5#5h!  ҹ$_LFr{WVOrQT;֢P_ՕJɔL]2O%wf> ~~4_vݞ' r[K`&vњ×8R2Yt&e,k31BIPwaNMvRv^w[PQ(^.VĂ *v7T,u1lL2$&7I2Sz`2=Emi':B-P)[m&" Q Ylɪ藅&SVeo$S4TYd;pD)ug1]&?*_\FfLn'F*? "n@3 '挗5Bi#wqষ(]$SuR,E{cF~1-Ϙ=,V.LZJ%w,JV#C.Y<1ޘt J>p+$6t̒,̔^&" J:+XPu$hYƅN.U}9)\}ʱ$[H[ %9P_$4( pXf|3}z }z`OXJ DJm2!cd?zx42${]"gL;Y݇Em>zl˭{%ѣ͢:}vXEWbYމbM.e^(}&t,YoEz]6T~^Vʖ3W^c3w@Sq1֯0U(jX {;A{Y\]c< yv&< |.>> (ޞ :}So q o%{Az\/I 1Yҿ.q,)1=0м\v0gc Hfۀ3Q$H3RgD+F)p4 Jϵ^w `N<}&0^YOE W{ Ar=a#Z r^7)C#v.0~e)J73>ۤnx=d8yK#[[_h3&(JY{d1(ǎ<иer оjuVkv_ iIFco._; ^4{4#? ,w0wf$B]$ClMw~=DzWgKˌ+KPNJd3~7=Kφ PA V37]n6|^}Q˘}[7ز7V1wLL4 jKIɿ1®'=x|`}av+=هU%ـ}[[\†ݒ6sh&o̘ԫ_"ڪb"4~h+Yt68q="ԺHjHώz_n,KI5T(vLn~xsAz琵5, M2|$Lg}yѭۖoαM<80Sh``Һqpl@FNfddAm'U|Xtū6=HU5 Kven^鯌}}%ۑ$1_~̪AɃG_"mOU=hX~\fy( tbY|"bF`# Ggxxà[8$j/Aݧ ɥr ;Cib'qndް8{k* piJ%#x)oGc-]K$;}=%+{o[i`l#8bܶ㝚"@^?YKY't>)KFHO \ !vfS$t(OկO&iS#gcS}^6(jK6Q1’|`l\~ܴy3I00imN~|F%w/us{Ea4 \=BJ<;DWu6Gb Ct5ldNr RS/I6~+ܡ~ } \ HMimTˁO4JJILQd3ˁWm 󀔮w`~)}YN9|%G";Uޟ(; =g`OfN?F&g8BDRT_V.Ñb Dž_z-m!D&eDP2'M2[HVAї\>LӼ&5~ +Hj\9\[͓4 dhʒn^6}NPon>KKfHY-Ԭ^m7> oaV"3,$雖(2RW#寄6u'O7|'%LC5S =k|ZYRnv7R1Rې0 $U8.ˆ׌M5*zoyU{lBWmN|3hSû2 YQ̴BI1d*Qm/ xKESA,8n))r_r.K~p?"w#D0qr0%d8Z?S]`A)+rTBS)tDn͑$UZ^kZՈ,'{UuAF3_:X2J^#ǀ KWE yǏjCFQaTxi`S$N-.곾, %8e-8+0'w8z\[bD8V!&eH';H$p Ycљ{*O~<1RUvз)8lgYN,@R, 8ic/Cm09c=l}k}%ʥ-OȮ dn~VekwHGO**[>:0|j􁏵#ZK?.R%.}x`dTu=kt ̛쑐^rӀ/ m<`j)۰[FHAXr!"҃}Ԟ;K1)½*z5uz7\gcRg3$׀=-L|a{qN`$ k-'ӣJl\G*r0FTUOYTmu ͐~ߦ@qtg xtgj964,*MR}xtHIӟ\#zS.&DL~D6kgN, ysF T;88 J!;My--;}5 }=&u__7g>yAۻjbG~ L jaב/Su ;uo7ۀ $OH*\ IDATA,\͸J&VꐒQͮFz/%fH'f;89&{7 =m 8id"5֠:}?@2~lRyBOm=kG¶% oQ[ fȶ׊&j(|\?Z3.|b, QwM<>uv~"KoUuK@LmdY"m.z)Qm/ *\ƺN</PP:=,8'P`RfKi_wo|PRQ4ֲ:*Dv7~4v|fo20 x"aVGn\GY' 1&^DS3Q<RLy eQO+?nIF+`V`@3k`Zv"[W \&?؁ǦI_7X!\vi8E e>3M7ukwɊpxsuB($ϞY,1vLzq#N%3?$NƔͤ}o,2ޥ̀5*cc9w]C!>: }01퀓:<;2Lۀ7=X#oNU3Vm.yɘVr"xLk/;CkʒI!-%R\NB[XqpYI8_}^ѐ nhm'+ˏsQVnJ+-NfQkj!"RkL;3YS]dr J-8dHOǑ$}9|W $/RJ(u*}zc~5IJ$kgT[঱w}+0/N<==LI79A}PrAόQU=/-cϿD},y Gb<5I@2ju;ЬVxJAލgJ t,:x37#Uu(Lm{?㩣l#cWa4ޭnԮ@:!zGaZwfy-1n`o1Q/W#k/!Vm4(w& E&ңuxN*ߩ38us)017-ޯ)pߝlKuH{?H&¿ߠה21:_@=1Zm[Q"^+-G>ιי=ǧɄ/xfB2$}KAGhqf_)i0Sqd -Su.:AZ&Ǥxp{R3&c;F`] N$| \>,mcYv2ؾsRwN>C2"pM+(iIUFIHurfg ELwI[PrӤC?V凒d&uf5]aw۶`Jg &wo'|Vvr -ۀf_E5Ƞ~-[?lQE`5#'XNը^7;Yv].^^P301=ޯSx^i5R%Dm$ =#ߜ˘jb)%Js&7޻_In jsk <|c"xe#쑐4^)(vLSy@jVURҦNL`z/Ylm` pBs{\'D KYs]]Bsg@eEso_غ*?T76t `>C.cMB~=T_0>( L\=_12ҧ?C760Hs@I@JOz$^vL4N~Oruk|Jƺ wm a`2vj+YtOTļlaz 0-0Caٷg8v2xBY{5%TWe'oNh&=vO|/;J9`pg&ـ4P?/ ƨ {l\9 h_xk4߀Q,pb.dZ`RMP/÷? VR.e*r[%[Vm~ Ԡx)1 k,Ly{ NOpMpE6JZ*XS.&#~SMj:(֮.@[[)|''g߷/}|)~k>|q+?xyƶIS/ onFͳFPRQVe9=ˁMoF@dQ)FlnQ٩Ҡ~TǠH@OD[aRRw ${Rk6ܤҏ"I6重( ڹw~ 1 Hi`x+ٱ/ßf]ʲbwz~..Xubpd|zmOmB`URٺ* jg6Q~XGvN6fLƴ@1(GQW.4XE.MFkncP  ̭QYVϘhO+Xu F588q E>'w_^NPC inO=z%H%='yJQ6;W{hHK-w)Rr\{u.v7|JǣSE`HZh)Q%[=' nw_z:rjfJZ:vZq:dIe$R[Kib'yDdgCۺ<򙔕 LTRR"K Ы7Y v ́%rS%RRO{`glס]Sd_I%V>!"RrR2}VP,s #U_\{&YRl`T?|4,^~#[mme!򺾌XAtHvA]+9;شq6Eܪ L ,!;x}A#22鯀u\ySUQo ,~Q OOqgjE=?aƤ9$IW_.%Gbs)pBG!Mn<* R2یd g+*}R'`m>W(PW|v` D[2'UZ/^~ :. Vm1t$laLe7u{ 0'9K+|X;E'Ⱦ;g>X J&t~>ghT%`Iv9|O-Sw eeP/N=+:gԯw6Oɤ 6,i܉̤,nycJ5 k^{ںO&3ZQU݄`M?uo-NeAқ!{Q Yv^?`RZ6RFdaS3|Vइ#ۖ<3]G}Jr^NDfZXFb9Iv~, LsN,WW&xi. Djφۭ-#T? ך@lˊI.Vvhctjdb Y$>y@3- NWnZ`x8{d \vVi /%o]I0wiOr\W@w%RYrD˘tO<^!=].4^%KU %WUhV(?z}K9^Zsvo97Z;[@^|j,1㌮`(d2;pykFJ]N|Ш }J"lh:-+Ji>[S1\)M;򅓛&I%#E&!|GYzfd8n PbI?g=P@^gϒϲQ{b>l>+;LF>KAYtH6ϛ-iWWA'J w 57ZQ%R^_FwQ!DE+ßH`,Z_)e|$-V:pD.v=ݿoPM"<;[E{b$JE!`8p U$I}NcqZYۂ~\Y8Dl pY/w~\|%$(ֻ\6 uȰ;57UYH׎k(Wg9u-6Ճ@dWo͖đjP ٦5ݯ~\"M0d.um y,tuo|9W7 <۶)'׏<2~{x$lar Joһ)viѶ] 8G z=gaCNOtJ') E9'lV3p#ǀ,O`ͯ51(KIͥeEA-Sjz78C{$pY,\'iɲ;/ݳ*[kpQ͑%`Ƴ__@߮괘 R$;g<1d,[{{7?E:52";U&^@D|'{mGDDǍrǑ$Au<8_z<ёcܥa^z;2IrY J\RYF3)}Y)WdO4{WH0-Yu"ϛNMƤ)2Pc yReepUOh"L@N?j.3a.ƾs|-Vbz  %=E6xYzkqd`H Q=nTu(H_)[& FC,V`vq-m>]hZʭ@Ǜ"OrpށoNZ^FAwKGB`L;`bG >ݗ*Ƕz0fM$ IlM~svt!!Y:иl,Nlе\hPj^rmT*Q$ ;!Aǀ҃*gXbGғ36;U>\JʎV ZYp_׌$E2{5NrrUkn֜qQ\=@zY堉(5XQ-Æ=kvJۤo&oN%q\G<}WLģ}$c(=EV޷*X [5M8pIL +oRQON`B' *&K Y57H9xf{%+mny pʄ= ,Lg9%0U:tT2l{%hٿ)Ыdrwd}-_Lz6hmR&)I>钼P3Kjg1Z" O6nȇu\xœNEz7ƵUbTdQ z4E'"k{G'=+VEZ2pPdܭY:rL{$sp'.wjVGQ rzt$~z \-d#IJ¥:vzת^Vk``R_wn`e0OL6j}@U^>^ˇ!I 1p~ӵa¥kR҅^WT>|௛yW,vPw{KI& M {eTm2+.٩dJf{A\oqrm˯6~$T3@A _~i#Fϩ/| AZIDAT+^x0Ym\0j+@V,rj}e>&{0QGboYUn]JdTnu0v`.`A1YHO7M7)+Ғbإ!q<([zĻ;Hme\Z2ФZYȾ޾OTݱS(?.5 ɩ恸OlI`K*W L-/8+0E )"ms\r}x:(;VƍlQx t'=Fn٣GS[‘bKOKI2#kTt]~%LlMV̢(MI<4#h~ISH+^ hT#@Ew@ߙnbC&UŒus増GJPsRaF+yρW|7.6X_.8r@X/յeAeGc;wIde $yk{$pn?{(jg3 {H@Z.(OE'NY5+'mW(܀?1Z8e:夿Q q Yн\BUl4*K?Y^^]^nrOӜJ$pe~YطaZ;oS 'Ul}'{KPirz(=ܲr9^s/ux \|,uQ0K2Onثct$?gq,ڵۤڎu:{MWtpwpK`<!^Aʉeq۶u/zܮ8FjC. #;=ˎV]hHZ4)" eV:F^,~ 9& ogPwE9"~crdQ]򳞁=^,績.`c6&8dVS/*0q2fUslXo5%s鞻$NI#-*+#=,CG~,J1"3h IoL S~ H9Kdo< |p1pO:KJvk`Ac0ϑc\L62"ŎTԙRMJ6ΏL *T{Uvkі4B^|J;4J8O}^[m Kq$3{Df {G}%8O^$#[6{T񫆆,H 3%.Y˅'_DdDDj+eLR[_b)jga\,N6w)0450"00IDd{H $q.DDDDĕ;M]Q$L'дP=yk`Ɇq'[++.yV/0Y4xE]!f9= e< Jir%Um@>%Tad*+T S5 p~NX 7m}7Z }! P_h](=gdz .O:x3f$"xW'Gz@xs$ n350SGDDDL\* UQ:֯,0Bhuƍk+[']j} X[C Ҫ_׫5 QzoZzizVwO"N""""X5qu.Zz44{$:&YtZU?OMu;3Qv C}Vcw@% `_#AOEg!W:4{6̈́(sK[K$H l \Pҟ^zUA4eXԔs;?]x;,9^aB˘pz]N Q/;FEQPo7p}v$"ݮсl?W~'*#""JL|VzJJK^=#j""""/2&vG`=JpHΪLVUN@[MɜL$I ]JYIKܺHeI+Q- ͻB4@vL<}__ _ AL|LܕFDDxf+M(|gWF ߑ@Ff\3g{جm6`40]Q 5^daWUbRjɜ$""J['uY&"""%T`2%QmҒǦU^oYF DDD""2&2 ""Dde<'"""""""`_,""𻔈|Y ,DDDDDDDd9f(9dL&%b?F#jA W,ƺ ᅁI"""""""L# ""mJڣ4:OX: u\?Z送I"""""""@9ilq=10Yd;DDd$ +?+5\/d]ݔ_04Qp$"XM""pyj(g?K ]>$Q?5gF*?F,Dd5dq7+TD"Rev⟌*!>{cDDDDDDDd\ 0e:~J!c!"đkǤ^Va`L1Y՟=uU~Х؜Q-dkeDDDzIY7˦Az$"""""""S$ـ7g9iԉ-<9 "JT RjrR@("Hʔ A.qU>WJm&4%Rn~'Q@BM50wY%o7:gRKr%""""""""""C yf"\(7zUEIENDB`