pax_global_header00006660000000000000000000000064134576555460014536gustar00rootroot0000000000000052 comment=6e065d18a8090c81c0f89bbdec410cfffff6cef4 pairtools-0.3.0/000077500000000000000000000000001345765554600135525ustar00rootroot00000000000000pairtools-0.3.0/.gitignore000066400000000000000000000021141345765554600155400ustar00rootroot00000000000000# vim undos *.un~ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # 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/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # cython compiled C extension _*.c pairtools-0.3.0/.travis.yml000066400000000000000000000020731345765554600156650ustar00rootroot00000000000000language: python python: # We don't actually use the Travis Python, but this keeps it organized. - "3.4" - "3.5" - "3.6" install: # We do this conditionally because it saves us some downloading if the version is the same. - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - hash -r - conda config --set always_yes yes --set changeps1 no - conda config --add channels conda-forge - conda config --add channels defaults - conda config --add channels bioconda - conda config --get #- conda update -q conda # Useful for debugging any issues with conda - conda info -a # Create test environment and install deps - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION setuptools pip cython numpy nose pbgzip - source activate test-environment - pip install click - python setup.py build_ext -i script: nosetests pairtools-0.3.0/CHANGES.md000066400000000000000000000007171345765554600151510ustar00rootroot00000000000000### 0.3.0 (2019-04-23) ### * parse: tag pairs with missing FASTQ/SAM on one side as corrupt, pair type "XX" ### 0.2.2 (2019-01-07) ### * sort: enable lz4c compression of sorted chunks by default ### 0.2.1 (2018-12-21) ### * automatically convert mapq1 and mapq2 to int in `select` ### 0.2.0 (2018-09-03) ### * add the `flip` tool ### 0.1.1 (2018-07-19) ### * Bugfix: include _dedup.pyx in the Python package ### 0.1.0 (2018-07-19) ### * First release. pairtools-0.3.0/LICENSE000066400000000000000000000020511345765554600145550ustar00rootroot00000000000000MIT License Copyright (c) 2017 mirnylab 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. pairtools-0.3.0/MANIFEST.in000066400000000000000000000005641345765554600153150ustar00rootroot00000000000000include CHANGES.md include README.md include requirements.txt include requirements_doc.txt include LICENSE graft tests graft doc prune doc/_build prune doc/_templates global-include *.pyx global-include *.pxd global-exclude __pycache__/* global-exclude *.so global-exclude *.pyd global-exclude *.pyc global-exclude .git* global-exclude .deps/* global-exclude .DS_Store pairtools-0.3.0/Makefile000066400000000000000000000012661345765554600152170ustar00rootroot00000000000000.PHONY: init install clean-pyc clean-build build test publish docs-init docs init: conda install --file requirements.txt install: pip install -e . test: nosetests clean-pyc: find . -name '*.pyc' -exec rm --force {} + find . -name '*.pyo' -exec rm --force {} + find . -name '*~' -exec rm --force {} + clean-build: rm -rf build/ rm -rf dist/ clean: clean-pyc clean-build build: clean-build python setup.py sdist # python setup.py bdist_wheel publish: build twine upload dist/* publish-test: twine upload --repository-url https://test.pypi.org/legacy/ dist/* #docs-init: # conda install --file docs/requirements.txt # #docs: # cd docs && python make_cli_rst.py && make html pairtools-0.3.0/README.md000066400000000000000000000154421345765554600150370ustar00rootroot00000000000000# pairtools [![Documentation Status](https://readthedocs.org/projects/pairtools/badge/?version=latest)](http://pairtools.readthedocs.org/en/latest/) [![Build Status](https://travis-ci.org/mirnylab/pairtools.svg?branch=master)](https://travis-ci.org/mirnylab/pairtools) [![Join the chat at https://gitter.im/mirnylab/distiller](https://badges.gitter.im/mirnylab/distiller.svg)](https://gitter.im/mirnylab/distiller?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1490831.svg)](https://doi.org/10.5281/zenodo.1490831) ## Process Hi-C pairs with pairtools `pairtools` is a simple and fast command-line framework to process sequencing data from a Hi-C experiment. `pairtools` process pair-end sequence alignments and perform the following operations: - detect ligation junctions (a.k.a. Hi-C pairs) in aligned paired-end sequences of Hi-C DNA molecules - sort .pairs files for downstream analyses - detect, tag and remove PCR/optical duplicates - generate extensive statistics of Hi-C datasets - select Hi-C pairs given flexibly defined criteria - restore .sam alignments from Hi-C pairs To get started: - Take a look at a [quick example](https://github.com/mirnylab/pairtools#quick-example) - Check out the detailed [documentation](http://pairtools.readthedocs.io). ## Data formats `pairtools` produce and operate on tab-separated files compliant with the [.pairs](https://github.com/4dn-dcic/pairix/blob/master/pairs_format_specification.md) format defined by the [4D Nucleome Consortium](https://www.4dnucleome.org/). All pairtools properly manage file headers and keep track of the data processing history. Additionally, `pairtools` define the .pairsam format, an extension of .pairs that includes the SAM alignments of a sequenced Hi-C molecule. .pairsam complies with the .pairs format, and can be processed by any tool that operates on .pairs files. ## Installation Requirements: - Python 3.x - Python packages `cython`, `numpy` and `click`. - Command-line utilities `sort` (the Unix version), `bgzip` (shipped with `tabix`) and `samtools`. If available, `pairtools` can compress outputs with `pbgzip` and `lz4`. We highly recommend using the `conda` package manager to install `pairtools` together with all its dependencies. To get it, you can either install the full [Anaconda](https://www.continuum.io/downloads) Python distribution or just the standalone [conda](http://conda.pydata.org/miniconda.html) package manager. With `conda`, you can install `pairtools` and all of its dependencies from the [bioconda](https://bioconda.github.io/index.html) channel. ```sh $ conda install -c conda-forge -c bioconda pairtools ``` Alternatively, install `pairtools` and only Python dependencies from PyPI using pip: ```sh $ pip install pairtools ``` ## Quick example Setup a new test folder and download a small Hi-C dataset mapped to sacCer3 genome: ```bash $ mkdir /tmp/test-pairtools $ cd /tmp/test-pairtools $ wget https://github.com/mirnylab/distiller-test-data/raw/master/bam/MATalpha_R1.bam ``` Additionally, we will need a .chromsizes file, a TAB-separated plain text table describing the names, sizes and the order of chromosomes in the genome assembly used during mapping: ```bash $ wget https://raw.githubusercontent.com/mirnylab/distiller-test-data/master/genome/sacCer3.reduced.chrom.sizes ``` With `pairtools parse`, we can convert paired-end sequence alignments stored in .sam/.bam format into .pairs, a TAB-separated table of Hi-C ligation junctions: ```bash $ pairtools parse -c sacCer3.reduced.chrom.sizes -o MATalpha_R1.pairs.gz --drop-sam MATalpha_R1.bam ``` Inspect the resulting table: ```bash $ less MATalpha_R1.pairs.gz ``` ## Pipelines - We provide a simple working example of a mapping bash pipeline in /examples/. - [distiller](https://github.com/mirnylab/distiller-nf) is a powerful Hi-C data analysis workflow, based on `pairtools` and [nextflow](https://www.nextflow.io/). ## Tools - `parse`: read .sam files produced by bwa and form Hi-C pairs - form Hi-C pairs by reporting the outer-most mapped positions and the strand on the either side of each molecule; - report unmapped/multimapped (ambiguous alignments)/chimeric alignments as chromosome "!", position 0, strand "-"; - identify and rescue chrimeric alignments produced by singly-ligated Hi-C molecules with a sequenced ligation junction on one of the sides; - perform upper-triangular flipping of the sides of Hi-C molecules such that the first side has a lower sorting index than the second side; - form hybrid pairsam output, where each line contains all available data for one Hi-C molecule (outer-most mapped positions on the either side, read ID, pair type, and .sam entries for each alignment); - print the .sam header as #-comment lines at the start of the file. - `sort`: sort pairs files (the lexicographic order for chromosomes, the numeric order for the positions, the lexicographic order for pair types). - `merge`: merge sorted .pairs files - merge sort .pairs; - combine the .pairs headers from all input files; - check that each .pairs file was mapped to the same reference genome index (by checking the identity of the @SQ sam header lines). - `select`: select pairs according to specified criteria - select pairs entries according to the provided condition. A programmable interface allows for arbitrarily complex queries on specific pair types, chromosomes, positions, strands, read IDs (including matches to a wildcard/regexp/list). - optionally print the non-matching entries into a separate file. - `dedup`: remove PCR duplicates from a sorted triu-flipped .pairs file - remove PCR duplicates by finding pairs of entries with both sides mapped to similar genomic locations (+/- N bp); - optionally output the PCR duplicate entries into a separate file. - NOTE: in order to remove all PCR duplicates, the input must contain \*all\* mapped read pairs from a single experimental replicate; - `maskasdup`: mark all pairs in a pairsam as Hi-C duplicates - change the field pair_type to DD; - change the pair_type tag (Yt:Z:) for all sam alignments; - set the PCR duplicate binary flag for all sam alignments (0x400). - `split`: split a .pairsam file into .pairs and .sam. - `stats`: calculate various statistics of .pairs files - `restrict`: identify the span of the restriction fragment forming a Hi-C junction ## Contributing [Pull requests](https://akrabat.com/the-beginners-guide-to-contributing-to-a-github-project/) are welcome. For development, clone and install in "editable" (i.e. development) mode with the `-e` option. This way you can also pull changes on the fly. ```sh $ git clone https://github.com/mirnylab/pairtools.git $ cd pairtools $ pip install -e . ``` ## License MIT pairtools-0.3.0/doc/000077500000000000000000000000001345765554600143175ustar00rootroot00000000000000pairtools-0.3.0/doc/Makefile000066400000000000000000000176141345765554600157700ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pairtools.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pairtools.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pairtools" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pairtools" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." pairtools-0.3.0/doc/_static/000077500000000000000000000000001345765554600157455ustar00rootroot00000000000000pairtools-0.3.0/doc/_static/hic-processing-pipeline.png000066400000000000000000001514661345765554600232100ustar00rootroot00000000000000PNG  IHDR~3sBIT|d pHYsgRtEXtSoftwarewww.inkscape.org< IDATxwE׽IH#$!B)Jޫt/ "_"_JP;kBzr~|v~3g={MxGNi O+ "l hmuDDzǀG[\l l 涰^",6KG])D`1Bb@`w`LwVJDd LmTf_fX|2[aJuoQDz3m:X0v;ZJ\ Z\NJ,[x8[b7KDdIq8vӮ؁7F~zuAw_ %`zc" <+iFliVmLޛZ\/I,߈Mj""K[X`=X|pHEqx|86dF~YWł<""v\݁wUm6}u- KEDDDz''&f:k'g;u`=c%ʻ1G˾l&QD$;fM\ $H{ץ;^⺈t'Bcs 2;t-"Yݓ̐Jneezs-"RO|A]L/❋kq]DDDDsI+H/Փ'NngUuR""u܉GU콝z, FY"K4@DD6`M`p66)3ؚQZr7l^,ՁLEXP{q:Xv[ xȊȒ?0({< XP``5`%l䍃eym3 XXkzc>3l?YeGewg >,}iƭ[$"Kdn/&,2Q_f(u1vBG+Ў^`,vlX;_#ۿ2ul _r =3{`;`&AP=;&ŽAb ?;^C ۧHp6x$ax<́lMRξ_Ifཛྷ&WD'%pAcTW=yKF$X:Wbñ bj78䍃s~&{ĶΫ׳ǯǽ&eDN0pubX_מ |-OEς/a=S܄|XЭٲe|߃e 駲$eh;+ 7'EV̼D]ATHD&|{t#5낉koVFѲeĆ*jÍ'4X`.>}F+#Û{pXNpS#{2bC@,bw0u]QM4XzHOuvG[..?o\n^@x8x]bƚ2="Y|~> ꇲICu us'#cEˆaજ߂VrA9 Oc#ͩHoˎ6/DͽÂɻaAX<v[NѲ}_~7^)R+9W=;n]PN"f`9e6Ʒ5H,+[5zm;OpmnjSY&a޸"-n,ڀ(;.8G}|-Zvh,o菝2߉f0BD' Dn ~8q=^+׮ %YTOHeSr\Ph'eqrLؙ>R\j,˷H, ?*(75+L0F T| _r'eMKrM~}}Lb=Ûe `YniTbo ǹG/X66g'g|*i2+x3v""""=Կ0un( ;̔=ZV쬜IM%"jc8-;έ~8E2J,,`MP8d8q؃g,$?+`+5u(yhǃeGNnhqcm^|CaOe;ˎ2g:E>y2W?愓`n X'/=eo`'Q}cr6+Xz޶Ԭf8-sC?_ "k{ zqqֵgs=,N]yaRCչsu궘Iw)afU>H? v2s yu {ryLuwC=u 5Ed 6k!Y86#4 qbv JDqD %6xh"<=~`.33d Aq`+u׽)"vDuy=|@Wug-]Ò.BEh#~:BNd&aj8nOR=[wzκòky|y늭$qV-cλiZ4dƚ[GT t #SYDzٿIg'Cu7diq} ύ( XwԾ=LFѸj""=kRX^!x<`}wco&3?Vfm<'?|bq& SMmFlEc-{<  N:F~6έ):oH?q j8E^OXxwk n.:8Trߣ-;qF"iLm\9o\N'[3eePEZ&iE'js9 QD/X:_8o>v:΂mgcP. q22P$x\/+l{1q%~b('AlmƮwA_Svؘ`3;zyX\NOEl¨:Xw띕SXV_!< L|߳epcJنrFOP8ۋp蝑Ī'cݰ=jX ~3}@wsvXnx?\7Ac? &uvz]W]MTaǰSI3PT1b2k}:8 ̻C{MHYnM,}cě""""-7NNffa'SfdNz !5>.ƭrjMT쵂w}Vfv2*"Ӹ ޴L 5DխK믁ex{?x.BnbNƏv5+7K(hk`]͝=뇙3u-8s"ngbNjishm2'l9?9uEf=UⱮ6<c%6u+ޘV ;,geNN,?4{G`ߜ튈Pq`Bua3w|^^,m/80,`uVg%%gԄ""ֆ8/VCGS&`cͶ>sa|Tv޺.gak͂OvP(";do.6T.%(Ol`Yv`7+Uo"p.qCSfk|fqvMPt]QdY>RNnfV7^-y ; x)e~:}neƪ+6Bp v|X.{hea=#oځ+1f5 ]WԳ! ߗ1g,<~ +a1gaåT{,ߍkQ>H:n=61Tߊ4(̪u-x^}.}3O~,_ܧf ISf~"' p7Hj5"""""""9&΍QDDZlx`cp $~Jlґ%yW-&""Ӟ!1~SƍG^&i9M("=a1o c>Mf&""=N'X9ֆM6;DNwR=Q,}@`VWBDDDDDDDDI.muEzk@%׺jH/p#pF""""""""K)XVW:h[W5ƷApz`_7֫+gb7XOjq]:$gb$⺈ȒkV1`ץ.Ǝ/_nq]DDDd)zݴd]`>|.?5Rz5b9`嶤&]Y.*߶i~qLMp\WVJDjķ%.]ay%R""ҷ.MpsݾoNNma]DDx7xRl؎#ED: 8,VhuZh߹o[Y.ձazF)WtI4[[ e;*"g`m /Z\fkxU`'Hus} i&bVWDD$SoHAKZXqTED]w<ꊈH""""""""DCjHOxh`S` l¢$6c&a?V˖\:J;uSe/`ԭ &wa38{cCS籬_bcc6bx#XW/W|;QY>}sVvޛyL?Kguүn]: o+%~/̉zÁ}m5X[92d#l\屮|n~h~ k~ ˌ? <}ҹǾ+bk~< < 7_/a`6XJln#9Xs)w#c=9~WS]wۼg_>w6ֵ/Xzl:{>Wq^xÁӲw lJsc;_Z=\dva ޾ `{9`cazf?ґlx3fl`-5c$pvCX uX[^\I͊?p0,{EYbD->@83n};%~Kyv2}7 1,w>KDe"KX pv.=;H;g;?[;=vzD}Wcזع?=y,㿱9kx-#3u.M:^mrFwN*vwud`}( ~J,_[s:o` ֫`OwH.> leO]eYSamcթX r?v,w/9܎'r[`^ NB.IJ>RoeCY;X< O{{9:ytkVTTaDg4v18?agyYݝM Dzb#b2G`'o/?.xm`[vGak2bK6`,,Y N,k2D F_,K9uaٌnkm N>v,ӱ}X,l-{r!6V#N=^Y.@a7t8'|%sTVϏc=*3X`m2>xֆ|ם3|6oeb߉;/c&m 콺 .}~[ÂK'' l ׼ Ѹ -m3/cm }_~ñTM{>}ڱE9^+R\83W[Pp7|7Xcv,y.]=kW~m;/s,s=v,8k{g8Xnw"3L²Ďñ,-عY8w֓ ?zVk+u#0 k/ηǮIq_C/Ŏ ^a ֖;ރms{ ""D37Sv8v`)Eۨ|`, H:/| ?idw'Se:>Fcw Fg~ ;t;& |OljE:vF CXvZ{%|?^[p Ӱح>N#č?߭O`An#۞  }߁B+3.o9awKufwev^ǛX(ԆݰpH0{!n2ĶgNt*X;UbgQb41~V,`Ne891XNjV>%2*mIA}{f8G_\Hwf8N,_Xc۸t}kt ~ؐ<2VLyԶ3a^|cc >3AoJHtg9 (sebH|{4KRHF' ,pg] Lb {L-VFDDd H9 &q=EF/ ,0Y4rѰh$|Yry'a)MܛX^6܆eȜrhY#}`2XP|=^,ghY7\f1%XvQ[` ȖŲS ],%=(c^ re 2.ZvD,/<2Ҙ3gvX3͛ qsP.. 8%ʤziu3te !^xP3azA6K\ƋUHk_e>H%if~|[7Z_,Ǖ$\x^˶+x"" Ȓ?b]S'ېƲ`&/WmϦt7z^GmhWꇿZ' E'wWoHLJ,K˚k܈0H^NaOq p6v2YOlQ*&+> g_Qv'o#+X[< ƿgȟ1wwt8oLGesC|.Jl]qg;M ǹfƂ.. ›kqKaƺCܟFѱq?q,{[OYV"Α瓟ҕVurhì5TKx]z"!$"h gY꺛'-enuaÏk79R=`l\"vAWt>OYgr.?LXP޾,)1Io,C/eX0 q}h-3؉jXfgC㇂eƠ7:9N2 (~z=(yLヌsL,n|aaO7N)ӎ [}_\3os\6:~̙ݔK-ߑ\`dcn[ L ǵ@݁ݸ^榪9k)IU\WKǎ#VkSbcx\@βXI[rTtLWkpE[ ,v`u{6&ϩ>h0^v]7S`y,;p^!x7]}uT;㸛 >.&?;{CqN-U,&ѽg{бQx5.gkƤuImʖPݩ,lt]u<x#)y]\&SfaXEǵf,90{p<,0-"זopu]WM{5{q&s1v0n_lϾpG8ˈ@z<03-87fkaV| eʝ=*,6iWvFº24;Y6" ]Nz"fu%] 6hpӂaE5K+Y/Y&Q=Ӱq1iw.`.*S .i䷲6T^ wxE,Ua,cCF*z]yhl# Xkܰ7 hMզpZNXkzkʼɁUmblx+v켱pDtQ"w-ո =w?nzV>uؚa`:eϮN`Dahڱ.py_ǞnջA;t1.U z];*ޯaT ,=CXҀ;M$lSѹsȧ==>XފJ0ۑsfH[wv\k z@x)כ3n ;~ݨ骹'D\:g5?TVOkz{ڹcN:iTDzeifom.ÏALdPnX PP[㿗G.ڇ~Ȋcoҍh ?ߦz"oo:mlڕ3ˮqrm7wH=f6:l5ߐ>+؆c]`1/3|lws:n-l/S~0nN!L)(EejpdNY"}\: s ՕW`דf"}|#{\>u)~Xfβ49{,6_w|D ~Xg(.kpj/př>)a'X)AiCǿgQ,vlwa ϒhԹ ώ~Z:{;a˰퇿`ewÂFyu'ҁ69>Hu6eJ72  ;Ul2)Xwj~O;ep 5)ew{  uXf`J8!#B(w#j4;''QHL)"]+uE]l Ǔ=O玏`筱F ;95UfAY6UDri gYL2|.N ~eF݆͞>[;ZbǰA C:`؝>|EAb8$vB4c>9/Xb'`(> 8j?ރ3۰}b'Ckvr>N,|.m2$IVbXݏ²_QX0x(pb]2^a㛱.`߉! pw:˚v~l,R,sPV{XXPfK,~|/^,vnb<ƺxe޵+,,=vq#Zw`C(SX>,?@+}ڄ@m7n2gfy.ڄXI~;zS>ca{C,~/kxʇnw݄ v̜}>uC=ƆZ ^֖cU؍%q7և_0b7DtvǴ`ǎ3c-)v6PnAع;vSFb=aM"3]v"v&vYþamo߆]}\\;W ǎ.x}v8,v>oQ>c,uHS]&*(c_)[HmVi,U{0g%_ȝQb[)eֹ`{өkd[%:6dOm$Y-`uw^b݃凖@_j\ff$9[8)zX&qOi&aSgGXfȭg,~wX_P~'X;u1(҆M9`ϒ?|^A90(2 =˚Kw`IxFcG=s s nO{:۹ ~w7.(j} LmJ9(W44MY{Yކ˼7$c<76"(sqN6h˸+u|^5X6uYc> =:o g`܂u}h46 hKC@ȇ)odɷ*6"lț矣ܤ""""""+&X5Ui_Q7Eu\MCKk$" mL,"""""d}XLMw ,Ƃ`_?MXajdM4v̺LODDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD9/ܝY Di#7&қҰ_3}V Jt~ס"""lnV̞[xX\Ңz4o`ץ'ikuzU[9F-HO2 F/ݙX[6;~ 6iDD:|ik[Xq-XI`d 8;GavB87  k {35 ۵c좴e]o+-Q͡=iFyel{a$""]m7`14l5:~V'k~xLεzCy0ݯD}>x#ڗ NŒ+zu &iq]RޡiFv,{O`&l;uE`'4i""yڜ^Kp8߮uXJV)\7_sYXD3vޢ6]N:aT)XvHo5k3*T3;;lnͣ:U6,8Rb7>EDخ-Ks;pOVH# צ2k)\kx`.Y][{cCy.B._nq]lπ| ۜFuVgcvmvEDBm-KW)g4N1?n IDATpsZ x 1gw}ThÏNz~+yzfvbhsDzƺq TzR xK@VGD=AD́oXo@^OV5 ƃXc(ez21 = lR][[mvVMڔ tC764vMH66^f6g=kdO\\l ?u%8vE]cuaѮ}\ZDَ} {uڰwd Mݕ]ðcmG!V.{:s/"""K؉B,p0?"૤/T/nnY]Vlp+ 1Ď%a;? f Jc﹈,' | Lpww/oe)L|jz=NBCuo1ڊ2nNZs OQ|섴(qu<pSq0dF|jXIdyyaF ğXف[9:4Xc7=S/c?=-Ur_hm~Yb3=mq))p~Q4qݯ'2 :2伿8; `ԟPܩ?S&^}(g݋wbWe/IDDDO N)bcq-hf> ʜSO`iXX7ma힝ݎΏbe/C2s}⃸R3k?$==Ӱ7|Q)Q epˮͩgc=yzkq]Dw9e 8 L. kV[: 86$ >lXX%V)#"ۗU ]]6їvs:Ι;߽XOm.g೯c-;`rk}_,?K{?NƮb Y9^"""8?,Lp'lm O6 L}wY"+3ޣy㞅lw'Elj Msؚ@" g|qb̓ݳ-kt\CnGK&|Ip45⺈t) 8&+",ۀ co~O%cԑر}XƯʯEM }u,x/Hٯ&"&6N~ j 8(1'Qc?1R;Iq {x}۸ 7]VxlkA|G4U "3mm}`]6+.b,H-vq{]Xm 6˔~X)(mҋXwオȟ4lGmE,Cl5؉j_,!哰n#~Xv`/!LcITrtm.e AG+BonPj!cNlg.p#ĉZv0_]߉t!Ԏ1X[eYu7xn1v 0 MƸ8#,o@VX(x|gVz}o](|O^jY-D_}'Ǟulq;;1l+rwx.\aTo'Uzn0 4 ò۱@ba]4Xw o 獜ۻktߒ>>k`y[t"=[ Ռq+'FۀO=]`tZ쩬,PqˁR}nUyYDe̯U ,ղ+ IJvC2{#^b>$ <&wb[Ǻ8%7 'o"ݯǞբ_P}.:kC‰V/n\u8[=-+CvN09ƬEm lؑP^F7. l¿``7C?"p'T("&wq/Nk L:< a2ՂuW:aCuxv]D:MleO CXw*> n{idMwibIml㉩\cYX)+R;ՃG`nOcjPțhu!l0Q(;'=lXkYP}AR4SҦYǞ!Nx_EuU:eo0j?pv=[vBӱ WǎաT0QDTmQNw;Y2fL[ack<s \wT`4 0O,o٪e,\X>W@QPE}52`ew sLq* 6 mPuFDJqOC^D6r,!;F\BD֧xnʯ=kbm||3fWE۰Oʼ ?:Wߵm͛Q>nְseL,騇y_*]wn;sb8>"x|<_w{*yˏ&ww;ܓXp]7e+?;\~7HE (|(bGEE"(("]D H=@H#g~ݝSu͙lyg|U\Ъy= MWz܏>7A.!{O+C=*fl_6߃"/?O~s<{{j%Q`3FpΟQۻ  u((][ KTiNdEE`s%EaC:Ffo]ǩJ?<&c؜03k(4u\S6zTOXٙa\>O|e`]ڛ/B#ޓs3^>?_6BT]=n icmqscwϺifffCІ$OPϲpU9{?6"8l\€sca֨.p{yqgOQ;Qj ΠE# ~#;qjcҏqyB< [^`R66z_mGfCJgmLyjgY$8yAZ6H&Yth<^hsA啟6 2yBԎ$l4?Y+ mX߇P[(?K/>=7;>O[ "YV}5$^D?seKff8ue2U9}Ehf!,B +Q"nGoE=O|M(A\d4 5NC'ynv/F5)~td,V~ }3[|BF8I9ˑ,u%ًm[z}|=4T%(ʥ~D~.ږgI^oWhgzd,Mq(dMgR?|o(Y4 6]HTa0fYM." 7vB:h('{Ѣ;|g%_ʊ'P~v7wtjB]FkHujf5zl:Pʺ%k BZ}CwHؒdOWw]7-HnzWqZl ܃?Uf[Q+GW333X_vEC'%g>t l n* kh[6ͬ6Gk،:!CF7N5`c```"0XoYSMuت.'fS[N<ʉyMufX-wqTVLFx')3wt3fff6، l X矇7ס P9&TV,nBeu(`ffQ.OZ^󛙙Yz&uq75L_N|+ܡl x/{Gi13\!6j?p V_=z<*k(0?:ǒ1+!իqy磊^~؁iOGy4T>XQ/PyQTN<8x+23NvF{дy^D7/D@S2FSMV] K,Ϩ]qlfff1;+`!p+p2;L`T|5'аRx03z p0v(N#́O礣8Meffݵj34ppx4@knB $t` jAvl6[㬛F i^Es`03֍F7n%;nV,崍v~Y{/̆-_Y[Qn[ 8ƒcpiffff-*{ϑݓ!hU w <6umf6\C=!8 M1:=lh33k]YAGњ,TzS|ZJ z|+VӁSQl [84} lffY*]`RW=݇pp7&F+zQҜx޶ԙ p#EP|BekM/Zez54ՆW63+0Z/>ЋU^+-+[gkV єD,s4/Q gYնM}CB9꩖!03+c {,]]fE4&x T]̪p>b<|zjvB W^ ffi+gS_N<ojnXt d#єvө_8{ uTVDϧC +p͟FU럋S]4Eki4P/^tLff7wQ_.B[V0]ݲGoT^:̬ik7PhǙ _D=/s\a̪4 F'g#v0]ff{ l_a3QkfffPܫ9h 5I&_1433+#՜ry8[ k045fGK~9-_YcЈtAx333¦R߫y6=zjg?fZ IDAT}v2Qff]*exC^U&̬FҽSd-ԏ*̆}_h1$pWb̬S^~zU&j|r"T U7R|D pE`*efff֪qϨ½ڞE37=ͬ?DSWs~Qљ!15m_\v-\H"'v4nifffj+5UAh< P϶v-Zp6̬;_r*5CeBj8 t>3f><8D RS?f4F|0333k3 55~(ܿu~tp.32̠6O; 65S?(_g,hfMS9ygYgО 5p>3333z 4, \Bm5W9<,u-F3"=hnwp`BΣ~mz@|3ffemG}_ᛓЃʆt'+/:433n 2j+,W!P+g|2s^ifVc"jh0^{}/^G9e '(Nz#mf/P_^ikJlxچg+M1)YhFN'R|5akGj(Z|  Q%.:X3n6LFS_H^./ffffA*MU9]iՁ[bc~PtzѕTPj|X o6K<,ˉOu%f6lAm38 o#oR[Ej?;d333F?/`*e*'R[9 _I}! Z̬Y=SVrb.I9 \Er1T'03]]u`JSdQ$c>Fغ 733e6zl0Zڛ\.')6}lD٠w?e*M5F3Sd333&(,4E֌4wj3d3dn'74E֬h85h:&4j̆{H* +M53^)CIy/3㛒_3>e=9̊?6O4L^e]O (S q`g)fmA*}p3 `!-8 7Aepmp>oy崛Dsv㡎fVos`:'f{TkHW;ʉ̲%3NA&=JrlxضXΣ}>4Gf?/'=ͬˋg9Y_fl͋0333Y A+}N]33v,xG:Δgw:̬wЙJa]u(f:WN\ٽK13+/+tR̬U?s>4wZ]33korba׮:e`>-3ڵ1333 >ӝrݺ 33%|y0KdffffMZxW Q%It23t5Su̬IJSkО`gѰl33\S}+p33XT6dS|{P̆̆%UJd`192  ^/\2̬"S[N68sjKUm4*e v%"̆&W@33Xj dsyU%^t._`ߪhfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffv7K*NK@rKU33kl7[@oi=еꄘ B ꄘY \ Я'0,j,~_qZ:+WN;Z8-ff6>p<0ⴔuI=C׹ꄘ BW[v-E]sҊI)׀J#cӀ+NY7=(y̦1;nO2^=x}R^k0V: L m$;ⴔsc7@o/BgS (pQ`&^kMgfC$y'*NKY8YEQ(X%w1]p9ɵ|ⴴ$?TpŬ&_[|>m0+ؾg?ӷ;`Qpμ,`~`p0xx}f8-O7T бpI^~z`߽/Fϧ'8x_Pp7GKgfC(3FKh6htcÅ yy*KEEhܛ>p0~fU8M>lJ𷻁ŀ5mQ5x7XF̮:!9Baa{`R{87OpJץ9 |F(/wi\{Ͻ+Ma4Wm9p(p>p7N({spo>cPi6F/Σ`p+W}ػQ2,rd_kAھڿ-"2;gQõԮ>Fv;^g^tmx_s\DE Y'r~pLVN3CsSjA>H: ̺Iuwyjp+LmEN/8: Wrn9uߟ)5>B~0 Uвr_"QzZ_{J 5R$9G79c^V>vp 9'-Pc7/yS"MSܦs`plI9~^0jcʵ-n 7\h[?}'? Gi8^:lT畏_.w α m{`VSj\lBV-_F/uDGp:u\ϽʱSIzBqC}^(o=Iuf)5Eӏo_ D-}k$_CDՕ x"'kVy]π3.?:Wk2{gy + 㴓}Gߑkx\QPfl[yHuk5 d7Jߋ[~ܫy&A3g/ՙ7@nF^蚮EՀ,D-~2?q&kWeA>4DOZ#;V1glOGk5_3c}ۖ3k0}M7*ḑm5<?dxa`tyjgȒu!63U]OX[k׌G\OvՀ ԾWjC2 +PϴP~}_ xJZvS[Q3kN[ظ wY.݀z4:{vp}&7 8/Ame_a~_εWW:k#OΛōd-E=Bm9.E碀A, 8Axhi~ڃR[s|ޘʭksq_s2oAFOǴ;F&q9 Y#.Ү9{P|(~e`ChMHj{{s0Tj[/py>#P^7'2gl_gS$2 -*6I%c֟eyo 8_m$n }Hp=.8 QG#Jy ȋ/|f^esє"0\1Or#֑( w:< 3hf6T6܃zM18%#8ߒmIH*FR?;cEjհ|u*M=yqmI*U𺰢=H {%pޒܟF} $tTW٬g kLۑscM8倳YpHM{h&{4߻HovElD;C {2AsyƯqs4p>zSp_MUmf lp~wXR>g#lol}sz4 έ<p^%8&-k Io\zPG8Z=Qj?E'`sej[I3McWFΠr!.˯9NJ$u}\aԶFgxE߇R -HE$M7G 8_uzHe.eibԶwt|;3Z0U%=H~9N^p'$_ "UAڃYp!dCN7OPopi7ۖ.ƺpYS}6 o{.NXG fY$n?V&lޟ_Ґg!q6U >xA KZBEGSbɄ㏐_KgYOҕy_CЁ"\бl%zhf=Qߋ=f}$Y=-2 95=ɨ3,DP{St<ܡOPɳeT\6/A2ٍ48'xJuɂU2j1iG7̬ "k̟?@f;M4DHN%頴qgJ'm@YrGFTɿZ/{r=}hW[<3|HxܽjmM2ZQ/ٰlI],A{o*n(6'ͼh{z*Vާ*dh$,ط' 0AtO3ݮipkQt`2Ci.?L#ٍK`2;Fx;뇫dN!|'ɪ8^ӱcP>Ita,٫y Dtp;+nw[xuv}ReWR7R$#P6ni3ΚG^x۠FQU|->dv ˨ÞVa޿Ifh֥/^+k+\,'e-De]rZV3ѨG,him8窨.*I/0/TtC0vE~o@=y:x)9M\dM4Wos̚17KT>g<m;Dt"i]KM0 yӚҟc^B^?cQohh$ӂ^Ӭ#?:-wjFFVȜE™H0.nM?#(oY#jZyP#O>hx"4+13nMƋC7EtƢi8F#|fX訁7+A/ao/ HpZ4ͤ;(;w,3)It%^^ `Xd~ IDAT(c,Cǜw/v8 $a-gǠKaѼjBKv`ߴV'wj}v ?eHFX6"QpԴ!/PY4T1&v 5w[eѼyGR܊}vVs3=, %~.g/ūuC2'd-th~x\=WA?Nxx z5f7FϧP;J.$$ rihEҫz:ZѪi4Y$"θ; U6x~%ioA% ^ܗ^ :%g?oPMezOھ©L^ݫSyٙDZ(^DvyHVm] uq9…~Bk=6ʈ3^|3uZx?w/Ds Gm"$C~G)<"@CIͬ9K]!ql2jZ)*7FypK|ߋH;P06>_$E?ݑ)(z-Vv-cFӑ~{ *.|˛sFӭ~Mw{)J'$qєۋ:PnL=)$km:6# L$pgtcXwH*>ߏ?<Pt:ur;7'R}>_^ot#laCwz_l_CIOzB#m{goQײ" Xe.;/ǧ5@Y|etieA(C<'s({l/qhآ5 ˺+xv}6nj?> e^_燨0z)اS4`ߝ 19u~l۾b? bǙY^&=Qpeo^s?*oA= oTlKWUZ֥6O I(Pp={0PE](~;p8r{x{FU.M=8hߣ}KUs%е>m/oc=Wa I~ ;u/ _ͪCN8|d2?-gpfַ9087]oPK8>N.Ho +oP,=vn>I3t(#}K`A?PownDž퍢!W5$9 /o$eޣ w:p+ysMmйѣBA?NIcНjut|AwGc€ҨgA t73f5y5' 8=&C\ 8nȾX"y.kzZi ffێ3LS4ဳY+Wo)a( A\Ʊ=λY_mpe5j"Zecى34~I=7\ Qts|1K6x>>&jH.8f ::t>lL=A9nhs(_|{S}~W+WԱpBjw@tԑnnby L8*|2cQ>(}fY yFD7پ6 ߈:¼ާuL}o0EPMdKPhFtWg72gXϠJ(\4'Ok{Ep;+';fh[^"b +^/cGi_Q< ̢YhĵWpא=Z$YVI~^P"zUm濹@2Yh;R~);(K6! TXǯfV^p z|)˷h({ ʇsYvo=^:&i)4tTZPyq{뙏ˀ/ѿbPs3ql/mHޗ>,اZ=Ĭ5ϣg~hh*Qox;5gdzC=dAuMsy,?ScD:zFCmR<-B}fC=]j? 5/Cut{ ]#׃=Q} j_կO ] Q#졹rE='Fi}<ϡ@|a{c?"`! 6B2 _a(nf@w@hơ\#НNfSU=k53Zp]SeK[ D#&DiJq<(Tfz|YcPC**'xߺuf6o, tQnaq4+lFSmo(Ԏ[jIʎ2C#b}\ vj$I5ff8Yg33 +lff_8G!O8ME < GC>܎;ffffffɅhܔϣtif?"ز4ޮASՙ0瀳 %K9`h: 4vh! bfffOaff03xVG Х=/ܪ3}?!lfff XN"YxHz12GIC+皙 B*NՊ^-:M0t vBw'i̺mD 0~ 2333맛Izs[qZ '<I򯬀xm|8ꄘ _fp^=)y̑1Jm[=ڐ5:wXa>233KqgkM*K uBe xOi1|#IcBcnjIm{_5?LߧQ,#K#33fY.fT33nG ⴘ G7wп] f,}MױV}RpfWW33ffVJֲrcaZ5e\zcXF/lW^yQ=cI`C}eSwyZdYocf 鏥u{֢#CCʍ'6؇gB@yJV>w^ M=\zR݀{ ο3Ivd~oF pJL`׎ڛqG3|,ڞ.;1[: Q(!;d:{e7D=ċ lu[=nCyhZQyUǟu{4y=8_m{ɴĮ ߩEGL9G<2^0lߞ:$wx74x/̆hn zi|:")pGwǡ _&u +n׃ 8l*1Ps9؅2w@PðsҸű=mz0uLـ=MtR{tmt}G=_S軶NwPơ" ?CvO^kxM+$7\0؁dAoO:`]hDVE90f7_*'IB_".ދPOHn'{:ߨ7[Po]Aթ$+(?\ : ϠU߾}Snm"<[_-g:SDž?Q~0 8'jmM6c{pΛkq^_ _xc"^Eـe9oE͚bOU9od, ^Nv;>oO dj[/cjO:43fEƒv#LcqtS1m?ۯ@ld=$yfUIYA0Z#Q]>Wۏ.$md4 TEE. ΛZd/KQ՜/E2%<5 baO9<'G[zL =Iw/ 8qHjG)Co T䝷fl 8O$3Dzќ67y>iwyTz:%8o&Ͽ Ie΍ϟ`_33-oJN > ǐl C}@toaTFO24TkMA>NA nNK_ǂǢUoP@8koJ!dD/I~j'4h΋rQha3,#H]ltPϓػ8t*E=.No!}}.ف't@j7 ta=ԛ4gi':^yY_\,z$nMgQ/ePpyelCplmH{P<%KF߽f3uý><›:R$#AӶ5zM ϳP햙hd_4?iR8I:=/]vq 4*^* TkFt=coR?gViѿ)n-Qya; 4z9Mjy xSO8;"4#iڴy;˳PXw?M bff6(A٬h3Q0ۨ7ND46~~[x4wynX3x~9VE.B< {K¹#LWy5?@ӕHz/L-b XY& lܧoߑ{e IG=u@om6|} FzJ IDAT(B45FU˒L}1S?eS, 7Ni%Cގ%{Xx,Ѩ1]74>9V^)t8ܯ]€h˰ϤWyApfh,58{-+2d4g{T[u`yڛDEu&H|OF=WF[;/ȧpf(2_784z>PVy;܎^zB(ދbÊ*UkZEk/zQQE@ EtI%̝v>'~g?gΞf֞Ϛ߬1x F[C1'9|d$c 8Ct1؊H;kb|廒 :;"ߌ5^~ a?E^b51vJԧcW7Zξok7 P/ӈ.U3rKқ{kS41Tbl}<szNZ{:PA<ͭ,_Lwv.7:b,iפA'{c"w;ѩ|#MznJgu4Vc׭:c&16p0'ƥĽHyxusҹR%iu}5;ꂸcU?Z\5ǰX6D~$,% 1d"}q?9b~CI:π3XXsCH9xńy阓_pMyޢst·A2U+)&hIi'YE] 6aқ/R݊/!.$&R [ :$ $g{zqˉbRܲk qt?-3nJӖC?\6D:ӪgDnN徙Y;ƇOHlxYhՍH/:9G54|z s]C%WusnGDyZb櫉F A#@<lJ[׾^uDzNݞ豕?x5p*pؔgg8X!D{SQ4; ѥk駈]Ҡz8?NiNS3Bdb F7\g91D7׹rM!oC:|O=C2~?Ѫ8bcn{%ZAKjeSL6qÞׯLWdK?YF-. #\ow!ךDsgeCw͌L9}(,ڪ*]J&m zm"͹({}hl;-<*wP+d۩Yy1n725y_D^G~S&d?p,u("s-!/@!^U%I35i`+֖8[RYenV:=Ímt C~AmwVs2#8M1~x6hIyV҉^eV7@Rf{0n_[1"ɑf\4OzשJX2!ZzOJ֮3C\Z T୕)zfqW5cנha۸ x3E5mRr:sD|u눇eiKFvӱ[~r@SӿɏG([J×&y.IZ"ұNLD7'Q>H)Dn<)kp:wuiG)u4NOp߷I̛vӛþg˯foʯWoCIsug\o ~<12 toIq߀ 1sH}V>GLZEj'ԯ??6Ǣ(ZIN?=74lN[~0q>TF-i / +(ߣNǁ_a=$ƺN '[D}cs|w*5ZC!`z׺S{-|W77O+u U4 ߉'Od]_YĤi;J7(nˎ/{0-r XiD'dZ{1W--#Yn  5x?/)moBH4G}$)o7 :k5h*?֊|siˎ-mIsUɺ_~9{ %9ĘyAimvtq㼔;7#l̫&__BSb^_Pz=JQo/,;"$bgY@Eo'뷬)6cx虖ቬ RWvfZD%7Sҝy+q^98ߖDEˈ`xn&rU3U]^{b]nd/''~K4'֤4IShlu߽UV!$I tS(΍w!y2%ݏ- <gK֗J~"Y{}RGܛypCW9*sx (ΙC-(M?r8=[I;q}i U&K2߅i]ʟ= $ZA w}q+f;$^A,n7uͶ&foOZn@\U[IPΓ+U6* MG"P_J/F+ >q]G{/\ۀ3*{$ U[gʿ{ym,ETewZ 7Dk*7R+Z7qiD]TWK[?}ϱbDhZ6gףp3}"ߖ Z_ .88=f ΛXFt>^g,_cvg]!ZR_bj ,Bdc O,[RthݭNL1ǟiCfG[ǨQoi "zx0|l+2̣8:+ry%ZZG3w#6ԪmL5[ݓMmb:w>cfINÔap!!x8Ue,YDຉ$-|+r;gU +l]ImJ|?='ͱ\K$]:Ed]z޻b$ ~@ѕ҂6!fي87=D.8N#ken'zG܀4v O'O#;(ZGu:r~iFlbED0)ƆIv!u2a9ڙAԝuts/"/7mĹ@⼲A9!Z=\AL9q]B7YT~[v#O#SD|9iw}"Iư|ȏ-?; 5$uVS|/sY$I##oP$ik@cbu~ybIa}"\5bHwgN|$I$4&$Mt)\t`D8ICG..s!6ҭއݐ%I$iTpX 1&D`4ˈ?2Z$93=W=dNV$I$iugC3;WMs1IDk (&';xIr)KYSf狵VηChMwJƟN˓I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$k}0i]I$I$IZUxa1ZVz``>Q/$I$I$[}.hű8d$I$I$IZי}- 8 ܕ >Œ$I$I-zsYFS]%Q,$I$I$[S}.hkpQ*$I$I$[h>e 8ؼu$I$I$IֵD@`R2'wgi6Je0H$I$I!lkkSP:1y"$&0 +("} 8#[ rۓ$I$IU{)xs,nԼs( 6݃_n DK*Z8C)Ơխپ$I$I/J/qGe+S Z0 T2"[YgAD8X+da[z|qAۑ$I$IU߉o ,ɶ Oyf<ˀC*< XY<"͟)Z(M4U餅3Iy/mN$I$IV[:$ϱҺg$.`p9$; 8_M:I$I$IZ-Mƞ6h<l68KبӀYL>R$I$IZ3Y~V\N܉28z2[U n(9@L2؋L gI$I$i䥭u!L"&XA{ v۩F7c+qƀ$I$I4+Yh$:̳2I0Mfvэ,cY$I$Iy O0$ߜo,/Ynt ;g1Ȁ$I$I4:n~nBO܍=v]"ݺؿ~.묈M&QL6xk 8K$I$I `a$hH N.;bhA*/vΖ>E$I$IƥCc.HM $I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$IVIkcw> KƷ9--ʐ ؿ߅4j>lBa_"ӁIm^< /ixm ױi}،~D$)E Èr`s\41ٻe1D,Fۉ=\h]`)q~a<,W?C}.UODraxR`Qt:X?$Iji?"8~ED: 2/nWo1ǭ 8e46"߯8c;({ plK[IwSԛsY:5(tx=i$I838Y"(.l6OM"Z=sne' -c>e~@YsY$RR٧eVwY@~U8H#4=lUD w<؂8OD$١Eo2eAm\CuEw>eĀ* XFe6V'Y#>J`>Eq:Y}.K7&%}+Q oI5\I\ L2ޚa.p - r |,DV-ߡ߮e>HqCYOKfMbpQҋIӉ1&Q w"WI/>˼Sr p~>1H16h$Ҫe##B:ﲼ:!Z|7Ė}-=k\E}.$I,ǮjMݘX"xفZn\>&cE/$( uʶxE9D ޵m?FLw[}^l0Oj-n׷($`kbi{w3W?RJ>ؐ{ 1~(.>e8͈%zΚĹa 19D=+{M#&mJ!&Eԉw\`lW npE $)q&S;ȿqgax=TJ9},e >ee|=#Id!lg bȿ{(:1k7&8ԙIԁ ެ%QoAViw!S{;H#B3[A$iM"nMn<)FI٪t+S *kOLU6[WPM]zxvMxil %ZލJy$5y<^v>K[&i:^La 8弊>U'$lj*(Z|I2mcgS\Wn!Ljz?kԟ'=Vn{ߢ|'#˝k.8 %I*bOۀu0FZɥt.Ҁ;n^4728y뷇dEˑv$eM{$hn<9&o$nȃDwDk4|Hă˃)&Nj7眵%43jip>$Ή{gSݲ{q،uU 8ϮXo}<7IsF 弙[3]c$ӀDAI@k4wXvI8CNnFt7&yzup~ST.K6&c؜d@&" [m3B'EK- ~en[&l@1ɘkdsB^wM &݇av|g DWoqu>o( pOű;^PL@[wf5i$J~..K]۩N浩$IR 8KJGy*V7#),pLdy"ݍ?S|OgtkRxVM;$dTbȆn8M$ꢼpݹSe?7jQ;?ڟ"nG ߤMI jcSb+)u+T Iu:cM~G@a3L!CG8?&z<`=,I҈0\/9]A< ZILq?1Dghu t88[vkBlwDݸ?"լL_cx[Q #j~{2Ug^C~*a ^ ܘ.1 D7J#"h$-dO 9aEn?&{:1ޟiI?x[њkg.ږhu#Chm0򭛡!p =D;sQDPyݫNW;g8JZ}Ylahn쒶,D 36%E 2 Γ[nU%IR{zWmҞ[b_ҴL)+1ks^SKOKٮ?,Y> pqJIb;{$~Ou 4jK_^L0m3H  Q?64 +}?n]Ϗ%iZ̝Rl݊igd?k6W-rP?EY;*4)CC4uEqzVvMM[tO\/޿ac]Y +֝dTbHe牝*K=G$a z7';ִp7T乥d "p;hez=Eknޟ~@ۙ,K}50ևƆmi aҺ:?7C?^-O1)HxF|v^cMg,Sؓޟl]jR2=2b HσĤe齝$ kՅ DCjZ 6Smy,t loqmek0QbE}츴[]ML(OOZ{HãW4]y}453lHE*7ne*j;?|>?^[0'ʯ)gA$;Iݹh:bhVnԍC?V'$K)oʶ3ܙ] 9a dݷ*>_iDA\ۃ*=SN[n)k]Շ1.ɶ8:Y`}I_ҡ2F x *n@L Lhm: As.Rl BŃ'|8~ 14sjKRusPC9TSyHyfӒ472=3YUP#9]lGܫy~E|{AsI &JUk;1 8ܝo]n^%K=F| m¾o".dyˍR'K1۵N6 b܇;+i4-D eɺ_u U9uGC^d bxtLDFE[8Ϣ|^7uoP{7Qݕ|ˊQ 8c&$1kMz# 8(ܭb{CW ('u#pBq8B %~s cԮ$I҈ؓ?3D˵7Ui@"1gp97"8Z~If?9N~OAR\.y"S@z@Зupx7yU-K'`hNH+AequuDQ"mΛC:ѽĹj DV!E=z;yWI#[>8<  ĹbIE9i#\q8T婲9rND~"Oz,:`ŘHmBs8 ԽDùDx;$]g_ z=.msަ5BRu^ؔD=A1znqKg[C ]$iHx }.XJH[8އrm@sYIZ5G14\~ڌ\x-Ź$um֯"_#P.E$ ΥIܡMYUy"\ab%(i|KǤc/X/sYƃR/ 4TS(XH0@esya/sY$I("%-ӱL?ߢHakW,n;$ͤdxǠN=b:̳s(`ٵő$iWWnޟ-ϧR~ѷRxX.RSXk8n`ldxZ XҒ4T%Fi c7[U},$I' ?c]GLtq(>klK4:'zt2 dw_YK.B4o͖/!&]K$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$IrNIENDB`pairtools-0.3.0/doc/_static/hic-processing-pipeline.svg000066400000000000000000000563741345765554600232250ustar00rootroot00000000000000 image/svg+xml paired-end DNA sequencing reads(.fastq/.sra) ligation junctionsaka Hi-C pairs(.pairs) paired-endsequence alignments(.sam/.bam) Hi-C contact maps(.cool) align reads to reference genome(bwa, bowtie2, ...) identifyligation junctions(pairtools) bin and count junctions(cooler) pairtools-0.3.0/doc/_static/read-vs-alignment-vs-pairs.svg000066400000000000000000011223441345765554600235540ustar00rootroot00000000000000 image/svg+xml side 1 side 2 5' 3' 5' 3' reads (.fastq) alignments (.sam/.bam) ligations (.pairs) --report-alignment-end 3 In cis pairs, we reporte the side with a lower coordinate first In trans pairs, we report the side with a "lower" chromosome first Pair reporting order Reporting chimeric reads Other options Unmapped reads Reads can have no alignments on either side ! Reads can have multiplealignments on either side When reads have multiple alignments at either side, we need a rule to select the one to report By default, we report the 5'-most unique alignment at each side Chimeric rescue --max-inter-align-gap ! ! Some reads with three alignments mayin fact represent Hi-C molecules formedthrough a single ligation event UU ! NU ? MU ! MM ! ! NM ! ! ! WW ! ! WW ! NN ? ? ! ? ? ! ! WW UU UR ? UR UR pairtools-0.3.0/doc/_static/read_pair_MU.png000066400000000000000000000410371345765554600210070ustar00rootroot00000000000000PNG  IHDR%sBIT|d pHYs.$.$* tEXtSoftwarewww.inkscape.org< IDATxux?(A!$+BԨS[گWzNFP$!.+?Xlv"z<]֟[e|N5^c'bK%~2CqcKR yQ%i3}ڦwo mL=ۼAqq\l<v2h 8G gܟsY[O[ y8fhBS̢l汃Ŗ$U.p3?;lIL9lh xs#_Ht`z>1-߰}{p,/pVnZW۲d]8*\ŚWoډهu8TmYn% ?)lxk_S%nhr>#dC:i7(gGNb7_k).⍵& OGj_M Gi%yqJ[lIc;Jxnr[lIaeYNS_U]ǥoQ}17TdųWiP^> ˣtĮ2VL^y"{2'l\ջY9m F[u哶⮭bŴ)k+HJ HG 9e` L/Yoۊ{S8=}~u*~.j-Ǧ`Zuki[lҏs3hwÖB[l9o glXW lEx.V=ue^CiFU/\O] @ &gsFmj/@ư-3,#[^D=eaI6ekhŖBPnq%/zw| *iƍi.w/Wbu9#K% h-Ss׻~pB.Ŗ+g3O_?[I{p02j'6x5k DɖM銟KGZ-fiGRœXny"6}?[l81HTW =wjݮ`rqϣt[B3h@w*o/:PF^mǨQ{ExlczKk<Go--?N.sao(Ezܕ v.M3iG_FCβǖ?v#.gb?>32ƌ8yܷhvHj""n˦ ɖp{ªJ[Q\x8x2/Tȱaa<8 "skQDww*7dݛwkH;:[l*\7m:l-yJƓqRS5U%.sμ[[Q EŖ=aK񈤞\=Bk\a 9Iܐ)ֺ5bR\dvl~v^^[2,ܱW~vrT]$;GD$՟6 \x 0YWs\Hj* {}38ltU٫qU^dMy[öe215Mk?r9 vlŖqRZ~qJmf-S8-=Ckv5$t;.ugedj7֮fN=Bc0Y6BWV66)Fk_7ב{O|Sovf@I|E$5Q$uEj ' O?-k_GH(YW?MDB1ؚ޽nE%%Kp'e~=)\^2y%+mr}7kOC&GLw/kqͷ-?FY-6YoSLpxM ǎE_k'6cf(CJ|rk--a[g\PYcKhɽ|x-l?KkI=&[ *ypB5\EH#ym9<(MLj\.3 $EDGi`Rv3uZXSbD/O8Q暹Y].p^p$)px|Zo5=|6fCj`>;Hg{÷<8FNq6V?=ocCȉdOy֖$i Gzp,9S-גG&ksIE/ؖkgisه{ëeZC[qb˶?f+m#pggKG\PCN7^ۖ,:bkU%ZGppm]D}W{a^Bqu+|4V빃8e"sݬ@IxUvz[$z(Oډݖi9 3(]#_<?{CLjC/~/-1GY5RW6~۔{"LYe$ݺd% x^[YE0"ɞL,o[Ï[lWƤ$潼u4d.g?-)I!/7\cIJ"tOy1}I.Iʄ{9Zۚ"9t~ֶ&&SHY뱦|,9߽l-F2=%߱mGZC._el{l ڻVΥ'mHwo]mʔtˈMkM"rIg. xMF$pc_nWg&4#\>,TIa-a^Z akl[lw&qƏkJTL? z\gOPG[l}i;b髧ٱp-hSqth(! b\^/5."SiTjE[BN-JXMBˬ@tWwmrt8aK-dKHdbKomÆin0/x<Mc7b\ǖܸOCzmBw6IAhyd\\ de|~vXAhqaJϜn=>'4>~:;Fri5V.]$SPbd/LB7m4;b(4=wrQ_fXvM6vgPwX"v\|b^+QAKչzGN_V`9M.Bw$uIso%ʳ㋍m씠[*m|eC^Ah4T7\{FZ,İ]QV%拦Uhka@V&ND oR B{UnCz[/汌 (,:З:0֣n=3u{(7~D=8 B A#fu,}Pt {7no'c'ty2Q+o\[~ºBp~j.!UI`~Wf>eme-'{ם,g9C' x kJ"!`JCMqM>^}*hn(WFDr V5n?ڞxEu^`AT Poi}X4V=~zB0pŽC+^YwVơ)AojN`pӁ肉Wh: ^ FM9>ϭWGj(b3Qڊ =`|c, e:$OGƗ^՗2u2}ٸ{ `tF {E[SOw"h:\ N" g(o܄fc{s9%UFL**'1Pf] @%xAT jT'o< 8Mv'&r[Ò&h}`-}ӨnL4 %gwB'%U2o6>^T,{v#,I#5hC%507gZ3da>AmQKÒ5X^5HJ7p)j`7~5*znCڀ8esF4R=GӋпDoB\]'wލ aM{Q1sQ)$~ctlߍ؁Uo"LP?FW]aI2n)B2e759.zs1f[š4=6E==|Bu~4kdV#ݓpW A0ZCi.Gl]XlKpNQn֎yEF)7_618o-=4Yc;NT{(/XGu/P't1](U;):Ul}1z?F`?ǾD4v8x566s9וKf\2[hznJTG{O#',芺Gm%8Q;o:C,pԅVJ KDl`?I(ϐU+0u/[|d ~B!:l%$*HG=Mduw@˝-tOVaxΟZvŨd$1OyͶEO?QtQ l|!QnnhcqͶ09atn{Pi߀CM]T׽6ґ\;(XO JH$竰'{|/8QaEjl,YK=nC: w.MDspc<|]#,)PRnCdtRwtj>%c}(MЍ n-lԨ_A{?b5%mޛEg§nCOOT֤\IIP٩|'ȝ6ґ CeU8|SQO֋3(;Q] :5>oC7݉J{+P'< $$ iPE֚$۝󀂰7 C Q㽽(Z_#]5Z w Qњ/,G;vf,{:1cԵx:cwU-B GG]0_{| 7mЩlպLQ( *B?6^2p+ @y23 X_QDx)hj^PixzA@a r-t3{zՠ7 BPٝ![(!_k75w(1IT;+žY؍5 . s=݆t@bQ#&HA=a B؂ YH*T4ROEHmC:@؊n D ]|xEZAA[6Ġt7GmHnon:hAԨA2i t^Qޯuv tM G *='^*$d$, Ay6߷ې=rb!D j=Ĩhj U(ynneAWZTeeBCITlx?SPYAMɈ8S&z%$Wx\YIIM ^ot4105VVz %;u'Vir vю|V.AEO8W;ha26:(FiFTzbg7ƌc&xtm q }W*+xp8%I#$~'x\l9 OCQ{٦o=ɀG3_Υ?hL&O)A6;GD&4EuzDHƨq[, ,/>JjPn1_~ȶكΰfN+vrt"C3A|x إUU OHd@uu:MY{[XMs5wq 7Uʖ8_ÛOO\ yE/.QHNK4=kw=LzXGtG54f IDATבDm`#^jg%$ÖBG+vr Cϟ|W_ԍ|ou+iTs"L1$ Gwz9 aŏ?d,sعGwsCMm nݟKM>8ꕦ|zl,7_mmt˱~ec=Hk[u z^_M|>e^}X~& \igبБH;Z"R੯qGkht* ƐO;ӚMlU>2'ߍ3T^>Op1vOP:TrPsQIMS]W s1u1g!7`#rFTnDcK5|>i`:+kVQZǫ̌Lpvim z^^coO-_U ߱hX՛oLJP{;ùt~#˯(&#&o3+!(Q;J(Μpy^L&6]hӚk W.[;Sg]k,3#s P z 0_E@qzPN@{juSgӚԅ[KKwskRjEwS+ɽ84I&ˇf|oetr/{;>-HÍc}c~{;22dpghqgq=ys 7͜F].HJ!+vy"JBBN2ɝYܜvfwi/aI5Qk~ʂZύtᖏQZZP9d`t3q~\Frj vqjz6ƊrM9|7%9R=9Ą65"hh &4Iz謄$>-؈ lχ%e+w2i`~?"j*2'TYi>M{;9)-P53nmn*ۃ7+^CA},޳ ʌˡ 2Gmp#5^n $;Ǹ̑lc5Sq1)5"R.@VXYÉHT34Иv݄DDٱe^೴NyBe ; :g}a *-~vmg/=4LAQTΓ/; BPnj#dm0ДukV?$!Z{YN~Z}7x;EKgz4/ޣǑNń((ZX?|֔i`Mƙw޽zx *g>GN} -"/iN?7,b/8jnήf?wv.8̛~IP&t 2 rb/#.:ZV;Ӌ O^M˨u7I=9wVѡ\eZ]}Xe:Աֽ[xc?,;k{B%<.Oڕk18dp>OC-Ǵ_MdOݙÇp͸A=vۇTlSkDDq_v]V e߿ЙANT)9+k;>.5Mk[zYdHWe]oBym-qe __wv?lюfeB9a@Xa9)1kjԂxaYfoaM{Z{G]H˥3{| $۵vݮb }Ѵ4ɝ3s]lYL4ΉyxSpQ\KL HהM\ns/<w+Svi$ICfn.`.A:r8p\]zPdYԴYwR@ -=lEP&toGM4) Z;<.ڕVP1_ѿMx=)IqlۼOL2Shxˉl:oR mϯZAKL'0p3 숐dYoOn^I|TkM.VLզԱ6w{o_KI.bixﺜ&w wCϬ\frg ݛsQu5ჇMsywFK#-K3,3x]Y1~δ3nΖs` +$&; wF2;jk5h>znF?h}t^:ͷwTi~zR(N 5.XԼBCIջal` SqXd-bjR\ljg'&m+6MoĄ129Ykob!нNbsɷ]k+w>7Pigz_ț^O4%)%Ju՝]Y|JDsp'9abx|9_k ~M|^ojO[Xj 415M75f#֔$DCK u:9a@Bm[sqF zEN&tf\` 0ˆذ:=ѕóM >.'8~ >pETsxtLSn,9!$ M$1lUyEKix_M|^ϺݺҡM-UUQަtV"k FhZ\4-bpf opy<,VN=J"ºb6ϜICz0ͭ 4A #AgiulS{B& 2,dcr5UZ;5(l%J?~AJ?~q\2܁ネyl֮CXQnrҏ{Eó+I\4D>xn * F?l)dN}JCLSBP }M`\9w*ĩD$ImJlyE1';zƐO㏅.O}lX0&vd;n 0gYhhUCgiͮV蕂œN1Z@ֱ %3b8y`i'/J 9L5}^: ϫr.cf-g/O倞psμWd1 ݝ߼` E8vܯ5A2sYC&6|YRh׽yBB'~0&"VU򑡂Ϙ^֔i6ZXV9+#45:^\m2byVf1Kx~?ap?+3gZv`jb yAk Iq+XtXK0B$@,[K0kT-o-(t /p^V1(<~~ֶֶ=oмmbDlZǺݻb>b=_F&3 ϭEYIMFϭ\NUcH/]ip?[KjkxҖcye9nF pk0Fcەߚ20Ԭ-uÈ5=6S,'/3<"ֳRwy^5B$Q>oQޘR":i+-f3+goa-rlͨцdZϨU\jq?tam& ƓD @: ]D-%1\mu9#REC9aH5/u6iM_=cLYecovaGm~=~G^dG6~:MSo&GЦQkgm<Fk|C6df `X_/6k#R3.nh)n >ԁgZ~77*6]4d},gcZI=9^W~ *Kf5kŦrF AyxGN^kOŴ/B?3k/.Ik;"b-Wn6}֎NLʄ񛵉\J`q0 p#u<Ѧ'z Pwsgp1pDc`!V 1ƈ9JKZ-)nSܚX-u BX  4UbqL7\f-T*C-K\xtr/&C|xm6L#<91F7yLi7#h̳#xG/k{`!PNߨAjG13 h9g5v:4-,6T9&--.Ϧ ڮmfMY^^TSw:20^S:Blo\N9|bR Wn 辙;b66LݟשڢG z a1μhE?{p,9񛵉GЫ[Y7||^4˨v xhX j\߀Tn}:8KZ£z@n &4d:P\u !iQR߸PID-.Z׋ HڇKڰEt@1tƂ tibRӍ.FdXv4={ }w9PpAv pCk9Bxo(!%0joX.ODB_R̸hJ|T4@/Hg,B`S K6.z(T3C8pNuhWXnAHd i']o\Eۄ?Bu~ӀE{tƂ tR?E16#9{XƿvHg,B ![┅΋x}pub~66!qKCY3;\&B du8=hX>n &3<춴](% m$nHr0.UI輼s<%xzߔθ`팭mAZICE)ml2G e/5,7.jθ3Qfib tfOC.JC * 1\g7.[ n]B:΃u$, ek~gOfdX^38Hhlމ^={ZQtM-uo^ZlIB`5 >=4SBFƝ >RSl/0.b9B '7K q-3LX-k)Ey 6#o24 jK[Qԝρc v"uoߍ׭UsWi)~@ϸUƾ'O ͭ,q硸%B:]/4.7Mn%;}6;FKS_^BWIDAT1.JU==k<#x5b#22K{pUWU6#tN7J{>jx!PD #bm-@:cA,gcL7QZAh+/PDTkO\JH,wriZs)j.B ֿcP &s#9 WKZoQ8Ԉzg`j,t1P1tƗ}U x#z'; z\jBDӴcn522QU?ī?Ӂ/JUݒZBXĬC}I 0N%dd,Bgת_` 3+xn#%dd,Bgg-05^J9QSȿ:'IENDB`pairtools-0.3.0/doc/_static/read_pair_MU_MM_NM.png000066400000000000000000001250171345765554600217730ustar00rootroot00000000000000PNG  IHDRsBIT|d pHYs.$.$* tEXtSoftwarewww.inkscape.org< IDATxexWր_;8 NN1-oq˘eN)M))lymگRJI@ӆf;fq,lHw$y$#y=3xIEWLK$Ci E12FQEQE?]T hKx)`\X 7-(Q`pmcvV뭁9ÀMK$SFEI&F%G* hNqKS`ik4x)P,#RKqE]yFAgCj~@]“L.yQ޿(JD5((({MK(Jh`R?N&18FKx%ہ}((ԉ ((((Vgҁ+UۅӘYfU. \&@4Af9{-Ⱦ˶9YosiI22{DY@mel%4ex2Ǽ5(0f^kQI9eJ>e;w4={KO(zs/./m 5ŭ%~q¥&( wDc[wmeҧcE䭻ZIKҧ_]=l3Y\R?GvqD' !ZSP:WdWy99iisW2SSX+^/_YŎ2uťIZG''VT[1G?Gk,^vCi!.@C. lh9 u|y\V>ͫTKDnD≷BB:1o .Gņ]Gty3u90Nv=i=FgҙlVC'w.ː#wAmrN>܉ݺsRTQAQEw}GjwnݹewEvtq:[.?Fp\s_vC0y4zߍui9\ie<{v^ <طº\.~~O l3$ \VgW AѸyڄ˙juQ\OM{J|2II^vnbKQc.'\̟p%ފj~FŔK8bRl݄{EJhv=+2.mp S7;v,ޱG (gۼX3:v#O7:v#Py?rSˁgvgs9iEi]srQ36os%)w t0o.A.37obQpQKy66{F:ˋ1o~=ae(e./{H'|E qDKiEIݶK |rA;mr"!Eq OHkҜWHjV'O;U.Y@jV1\r(5^]\R1]o݆KTof[2k\.rps|^L(x p5>o%z wPe ^6T=̻Tl\vL3S9}ٻz%zc.gv8˶⭥msy{b>_ʐK{.y+?JJw%5 F?OZfF\xm&.Jb %sbZiٶFrym.c\00m)uQƒfǒIOݦ!{رjIf\޾Ѥ}/#.KÎ, .5)'ݗtX޲9 ՜ӻ/tTO.gZqs~wo\QYy㯠bh֊W.qQŲb©iҡw˶y?t5'XX`oUow;ZZ3.S?dݬWow=2&xԏ7-cP3.~ȟbХ6b:(n^D?Cfvf\^E]°՛(\5ri2̸,~ WIOJQR<:UJ*+mT věs@VVrSa{1Ǽ6Nگ^|~;\ܘ_U.\#52 LzzsQD!pj=obH!pͥ\3r7}^p\iEt]m-+]̸Ǣ7-Ws.uii<8brZZN덵 ii ge;Ws.{3yqKkdE1Oњ,~]Ң~t;3.k{:ae,|F˾t?F#.JXv7l5re_1C. '9yzLaVݽ>'/˂),j_:<3. \:r%:nޜ ޞi,Z.5ACݴw˿ l֜]⢘b捿dwߥOIZX]d@ch'sʒB8H?4&B]A:M0x}5!n~U |˕Ѥ}>"ynrp2qZ q"c47\knᵆp;C;Nf%~f086dڀ¹]Ϲ9\etك>tǥtv yfQuA%٩ sm咐E ׼koe޶m|iK-iNm+]Zq!۷e3rxs"H0$7t[jx6(E M2/ ^Ml:%#ϓk QlKz&sy:77@'޴ \20ʐ$B\Yۣʄ*9y A\"m3G ĥM#j}l `Z'U[:qX-۾:.*6-.CKt3cSKw5t^vl F\"ؠ@zJB!aB]-%wKk" +?'۪{z)N3e@n%%=K]&][-SosD{+z|hAz+\8{W7pn|-.vZl}>VmIZFNjo^ETovOn~j%!.voeڐɠBl]yԟCHqt-]yfϋ[]LHsc2[vEO0p{κ.c O2eU9%/{9O][I᪹rYzpc˵O֝v3҂yw\oȋ.u9sjfkrB7w)6w2e;7r{d4jU`]gۜ\5L'\?&SsSrl;}\xՃii6d+\RӸ}pc.̚Q="35[SsyU.]Ğ]sC,u\:{H]sXt,Ɋ=Ac7?Ug9/.X',xGbߑ>μGǮsQc֠0G.qy2B`^o*O%UH` aˢZ^X~嵱8;$|W}4~]5Z!.]<%.߽).#O?yʉerլp9niuڍ<#.`dP 8ۀ\ *!.y-nO3.ws]1xMiނS1v3NTQۏ8;nU /CeMf^; FM`g(h|~ ."e _˟iexf?VR$ЄKF./@wQ˄se@:x(=&m. F浥TˍsyahL~5<x"6WlGk<֌\'h};ƟHU=GnZ kZkye<9 h=h3.gWXr 0rVfW/ϋcbjt:bLzɸhEߐ1]m߇mpC]m;u^i>w6^%">z':p-Ks9Ҵt=v߿&S?3.^Clych6ܼKytGfwk#Tvɀ]LwHS.tms_L/$Zh>zĂuiyhf=[Y,8m"M a9yzҷe_櫗tc(=O%ocsĐ,ynqk&D#._'o-&7ݫt-?|\ՇC;qaҳk\H)qYǿ/˧HNcK`GH4yŬ>Fǩ=O5 $4WoI^4;.=N5]z9{q96g©]N,n&C#.O?m]3x޶ /]x$ $d .&ϋbO /ǬObxK7a4Co89f<)d:Tb x{c^4cқpye \l祢yϏ6R1},ڱ ߛ6ٱUz>rJg[ɂdj#.V\vݳeK&]ƸEqT*b6 AP]R nfQ_\ᢸf(9R$Q,|9ȱ.Loπ+_$%=rf\r(5rٸyIib("IOHW lW"8`@b优ryRoEqMVݾ` ?xĘK\Naev=w.;LgfK%3XfDfnyVh#:u=cuZ]ԙS u C]غ%ֱ3eEq')\1<ʋ1oh%p?J]3)5zcEvq)+Njx}>1dfFJ*؛N%?.syp ViEqMToo>{Π˃.g%z YN5+?}ƌKSEG#.+aHuQ Y\ f.0䢸VC E'F\A#.\&Ħ_'s9r˿0L:eW/s GL6siTVͺլ97JZgJ;Bkecq1:ڥ K$Vi<)cp\q;\VS e_Tˢ7M< qh?oX_}kA\g:+ %.tyu|[.;i)?{|gdx?q92Μ>'sWN|:ɢ 6ElۏtXyԠI ǀw %.'7Eq1=N^MU @|c23ry% y%QRed^Ll+5RrSRR ;[iz:py\b[Zۏn'Zu S?dOuƩworٗ'mrm*NuhA{rLp9{O͈=L۴!n=8֠+.qQMмpyY(޸E{\7r{b(d'xx {5 HrRKC d ؀ˏ}^oW1 ~R}lg!t<4[2n.$UrLfZ xaRJQ;2'͇T*!pQ#yHz0PX+x_ٶ5%|?zNj ֞VbK Sh"rʪ5/Izxn+s95%O7/242[.5;!b$퀛H  [e $=5A*%d&ۄ-w!1T LaEq_~ `&ݜZB x5Կ(N+Vx@:/7LBOU۵uĮAꂭB(ьTz3xl;&8 O)?ߊc 68Vq? (ܬHOg*(Bm:ʪZ:"bp!i[V &A?TҝUFkx4ٿL % #緶lYHPC3dPj3'4zcj_y?{3nX,*d~ۊVgSm#z @y-?6=NCWǵ'IQS!: Xwxȗjj瞉@L 9T{d IC:Y*gLaEm9YWK$v|q%co>p!'+wQ~CG'k)XB‡ go]iWl.6$}O@' 2?x 7 !/%i\Ykw"9q:)VlƩ)I?> d-3>>9jJ=ؗ끣)FRFL$p&Xz|305C*P`|HH%U]5%yw a R( Ϊ'6r}ggc;75\MCǑՠb>*rQ#CTϵǣ%absr:=?s?JM |_֗HXNj!%RZ 3|?C*!hVMzajrQ6y7;يwO9!D? \M; 3%HI>,'۬XܯH~<JL+8œېk9΁}Cc߃-Jv2s_sq~:-0-p:#>$GcѤBbF7 wfk H!h 6_@^FTB+fLف'ؗ}?dT}¿(I p,>q{U M$ȊdA&]Q )"2_!a4H]џ? jr3RNR uJ)C3i>q#nژO>RR'J䴍Aݐ.sѤMJd|LX9i+\kk6:PINRB+Éؗ9v7R$x y+nD'MyHJgj_W?6"a1LL+.kډEu.Q:[ґ$Bːf x(ߘ|ⓋTʂlBZ:|V'd m=ߡ2=+>2$u`Œl#B*5)iE+~&_ hMҁb 2YJ| LQ*>dIEt yO2$V{8p'9n Z(!);Zg͌Q 3>Fsx]Cfu&sI8Cbt|UڥV41q1 iXKU5`DXO0 b$Lfu)3~D?+J,W8T̑tnAʇE36|624Gq?atθ|6HGpS%pySXH LcZcWǎ wNM%>IC&' ƙ KB5~UrXz#a҃s 4 vt'|?f%x6#'V`Ӱ2(Ѡ VLXuB6uW #&VxݾKi5U ?Һy2l1?ԏJ9L`n }a!m~Hy\D wk'G ̩% h[pMHɍrNDM(>,EJ+;R& LCJM nljؑ!`Hv͐DY\0dؗY`o(5 j.j ͐R/{poCB,nfU{ aO`%x-!E8Q|ACuR{$ EmX27vV< `6IW/Y CA(\dZD*ÐY-ng:#/?:ч ރ4v. I|3]"%x5fuvHY` RƸ}XN*6܅uqN3B'+b(HZI_#c (2YRqڙթBODxrB;gykN7> 蒕=F8!*6"D+" W ϿC[DX_:XFMgٴHL)EffV''YcV'UnE- ׽45U$1 s}< SbK}ƴH"dŭ[o6<$4y,;BB#63]dVK"N;$⠠n샶Xy5b.p$Xg"Ed"1&m'e-L$j#þ]n#+ώ&{Ek,Ss"DBw~' (l q#HsWFvX_譐NBa2Hw6j -r,6b rB%CDADp=0!eRH$g~`7vIa.ϴA/?eZ4trE'+)MB:CIn@M9yӬN %qWT7k `-"aX,ƇyݍK" Ҝd ԤJ0 \B[|W2LJ9+xmNƙy{WT3-#C mýzC:X,A&d3gvI"E\@s׿mVVڸca]a Q|iP㑉Y]c IHA:)bGB*\\=MT6 l_sI; h#^H:y8$o ?&2$w#`% N |s-658-&^1 )G81"73-$@f5\E nQ,X.cЉU& %6x1J'~š$v62hop&%ƿƼ}fr'+ݰ΅&T) 0 NQIEB >{2HYJ*:_ANX!F<jZ$Eʊc p_5/RNF^ĽU4.8M+BdUV?.D?B'K>Q}5EC?^di$ :dٸvZ &~4ɤH(0R] M D_pOlo?mO|2Ei,o# n/+ZF~UE\L$:=fUavo@wǾ2F?Ā1 !,yʜ⾯a%97(Q#Ei*wS #͐y?"u{"A,C;Npu!r h[p_ckT |HԌY}3.KΡ %W ?'6%V G4E(͑v}D$OcB #eYcdOI2= W?j_~lpR(8\S+%~AEQC R?xkUY uz'"kq:4ni "KR|yF?C欲(6. 8n?%p 2WԶq>_\Khd3:($B̹U N,! GWZu%d#sV%+^ː%y (A/Yu|2%s+q6(xﰾwEQ:ETbh) FGw}g!H}󺊢a+NAOEViJ.EQEINr6c=(IP2fHp#IQbCQf_ <p g=pt4}(J l*uH:s*Yz$+ÑDė e׾)'n@c+(p>F6,;Va5ϩgJ +] xץ4MWQXO/T.8WW$ȯ~|c(q;i+??"t#1VI"7C:Jh(Ӆ`dAyE?=" 7(X[{vW#~(&Pƿ?P?z$d+\L$U^BW l J9ߛQӐXzñVH~Ua"}Y,i,nBˢ2m`?sjqXR%u7nZE ӐKppW<+us]tK =ҨDӰqo_?F"Kk>34}gN:^"XG.\F*7%k *B+ꋿii~ IDAT%a52$7P3wgj1rRQUd}v}ejr9K s+ꇿa<8~j(X_\?uP q EZ7Hل_Y|!/;dɵuñ$O6v2/||gґ/L 1) li%e1dA/EWzc}n0#HC.$νȸLrֹj%H!4DSقhPXx.n0rxg%_MarYWL\92R>ك~QJ)e\2-%yH`z ?nXHE Z9j_]8:g $:eӭ H=ٟSd!+ˇ$&mƍX+8'DP{cG`yzV/d}0(3މ:?j". [TzQ"ն}jy9O[L(qC3\d nD B͐їc%( ʳHy]V6XX \Gc|g aUb]ε{%hj(]}]܂u2 (Ɂ (=%2N=)$h/6Ch3m(+J5ާ,oGhrȹrbf~ z4 M DBp&!(}hx8m-E:I>`)Rrν7_"s25h%5]g C:Δ p0@;hCOjQ͑B;SG$;+%>}H${>d&n }D5b^HIXI:nNˤĜ,d .BTv w^G:A(u dr)#zvH"`NA 1㍣Ers<]E\@>2cl >n\zX}@(s<MdP[Qљ@ 0WugE 0rS ϐi :vQ̓5ЁmX]msm_Fo"AM?"咉J6HXFv ` Ϳ]& yi%tFG{'L#[+(" 1' Y%8~cͩ՛ 4ds2Y!Le௴|1?Q LϤx{c<+=)`UJp6!8|18n2;>pWDGaXs4eS%7d"2'295# :vIt L"a%2!/"m-\xGr"( uiFdB>̜48,!t}\NºHGv5M8 ÿ(8:'&Tu/F##,?u(J(+Wv5J7uNm5gɶ6,.yôHĥ[N`",Ʊ@'sj1!NEQ9)_26fZPsh2:֡cE\bݔOvHӴN;1DI$lswa^ +NCbf{qӉ(t߉H:p*k#15݊5O$jNX%]ϑ<մHL($n̢CGb$oD(2kZ"r6R{Nb׽A:OBB2n_Luh, DX"nj5,$|5~D c-eD S (au*YWD 1CFE^ErrBlŲA~g "1)H!w$1x xH[-;.8԰K48 z~yY՘d#m"M2k(8RBD:효?Eq72?"$(Hh2:> /  p2H S9lH\}KJu@Wy8~4AC Li1!!#3ˑq<tBL|gȽۗІYb!^0 [ | l,YHRxxiBYRH(Ne? `>RY u>9D,D 9.D$HrDYI^dkXk!/t? Y]4-*YK|@$dN*( g;XvE&d+c7Qd~5(IdpC((ed۹PGf~^Ƚx.+ZQ%l@LDF~lFbFVv6YHGP2%VEW$Xi8oxj\R ӀO_QE c=&$ ["  $PҐk.4D(VQUO$E׹fuFtdպ( :_DVb2"s[x!uj NA_"3Ϭa!us;?e+CC耄"sbM2PEQC ZiZ7H Ɛx,̀I^JΫ5&EKt,Sܳ5:g2)ٴ~ ZU^{|׋ǓBv.v(/TSM"a>'vnZdRҭyśVyߦejQd@l}W &R #$_=rD液+HnJf~ǐ|[w1oy}%n=SgSXST-Hx8C'Y ezvWT0$5ss_|Lݸ%teh;h6|@vZtD˿7RlߑuAπxSGl9`@͸o|ss`}:W cpeή~}xacte6_/gQR)xyݫOثtMO=ZwkV|*vd&v|<ZCNI9Ľ]$97!ԟ w.JSRhT8{DFՌ4SRR|z{x<rBfKyUIZZ`mۗ|㟣 T6JTkv.*Ys'%xd^/wdGYssi$k ٰ{7>yFx}[i)+ wQž-#JK"U犳-/l\H!-'RO|^/)除\?II%-EH@e.G<Ҳ=f+?|nGjfr[II[5G0+{elD'!>x6to[ԗ uO,);lޚ^?`3j{[ط߼V= sxۻ-aͤN3U3ޣSHnֹV h"eҷs,2i- SA lmf4`չRRzu~dƘ'RRxHϱ,TbRzq7y߾dsWT͈~5'|}#3=CnO虖Se%{\Y< MeW^N6uH 7o $L7dd\mx.@:+^ndS*z9ۯ]!]+ w2v}IFe5WڢBnEٌwϟ?OvbsVTj3‰B?hjké#FZmAZ/?*YӤ>_~֧jnUɣqЧ~~;aСX: x.$Yl1TAC0vѿ`d}NX-ZbA18~xWdry效n5cvp8]/maףmcnBbVuP'h]>mAez[Tm|S7X-meY+O_1M3  C1#.AoQR L&>ޘQԄ3Jq焉n XoIazCjv=Gyg"q jTٽv`6J1/8y\·'5--x>i7A::c7Z-FYQ هPPەTԟoY+O7^>7ڂ8}U&䬒 75B[S=zH5*詝;ou*Àh? 10]` m-CpE|خOka oe iz)} cbG}kRcl`<.B7S\w)ǶbmvMK ^/OyҧHSGFJ}UYȩJ6[CsQ,|Ҁ@աq'c'Ҁ@[c4hVVm0+],/B]q>07 73JA|a}GUNg@ดl[+KPM=lҀ҆F¡98q3Y$ZJ'G~/9悛82a=K  }Hhl 8 @D=<_,wc!k_9h(gkYGL*uMgAKQo;g^Wf6&3w 6Ǟ:ֺJozVoEo$/=^+z$4SvĔ:ژ giiDΚ'h$b.;/ghܡ&A& 췍 U-Kߍ6Q8IZW@?mćF{'G4}:Lץ?wQC3޴V|@=0tn'Fl} MBUfnM m`q$ۧX,xvo_"'w1 E%z$%_+mAڂE}-gݐTD9_o܌m_Ao7UʺoC[/~= w1B-Sm/\aТphlJUuNnU>ŹQј1Bo>dק^Sl)0}ܓ6ISs{䘮2IRE1 )H}SGlO>/G[y5X+}6u59z#(=$ ?٘E;o FHa_5]`vԴT >Ru}Ö9Z̩7EW+E|;< yḱ#5:ij䋤ʠLJU1l13ڂێv {vኈGEz\^i$%_']TcVAn Gerl'; wd ̦yp=-;\}u"NENƣFz/ǡ /WGģ!ӝBcc ✚jl˕O[,O9!:y4bߛVc8&NoZX_YҌwH@iqB)?+? gCs,I<[V,KEz|nB /߫@-T~]ZˎgMVꏢtu_=A?'8VG|~8kţҌ)30tiz㗤7{k|>vh/TCz5"ljG}}F :ːApҧؠ /I3ֻiXlܧX)eES\+) ҧc,ol;~6jbX8΋6&l)k͸USaҧX)vKNHgק`)=\ا蓲W=KK1!eR{ɶ3g#U7I"#qՠQ9B7{-û{aDڀև1y,ׂK:{@Ez;<ʘQZW'w"[7> $}<ɶ}Y0o±_ ovX58UL@i hS IDATdlq?-LD;բQ/l҄ĥbT`6Hz:_xz']-j+;Sf<(g?iS *=@` \. ydݩF^gus3/m,fhZ()xbH5f\3"0 TbXQ$3tSG,!`uJ( \"|QWۨ+2.%P33fBb'HjPW hy=eE'gC._̂Wh|5na}fqPSKUbH}sqj%RYQf!fp33C])X>+KbۍJ3{,b!qc^m (1C{i'^߀`_qMX*ȏ{mJo+crg̩wЃٻ.: |8X 5 7Pj]']@OؾjpQ58hp^*k vV5Mec.w1W=$Qwaሻ\YwF[XMu²]kh\hqB=VX_@"UgBَvԹxk]h :Aټ=;b[*; 3^gz&Cˇϩy-L^6 mV3ؔǯNy`{qnT4`Ø$&N?j?rÂ|}3{0ud^gZ!lz-f_":-ZI0Z,=fhTs>'! rWEb A4p%-|n[.kQ{`A"bL}mʸ-Jnm [V?Ym3$^Hsܬw V<X𤋮@^h u;MJ;ݪyZuh=D 3?#F`Hl?\ X [̐-J&=fhNBK&/>s{S_<> 20-j^ cS~.ೢBIH‰Stu)c9?OSX&)f'b]1X}An)ݶuƌRG@%_"lq*d![\X?rg_O@z8*l 6 Zj*ICT6 ?yYLV W }CW\wărmVw!cW;ÅeXH^r{7_ X/ۡ]r;c(%m d kţWAH}__h3Dl'3CW5fv3H/K#Ԙ; k|}qx˳Le5fh!5`Tp0T" zC C7qc][LmSʁ}*z@B{aGQfȅ:ݖY>f(a\!OMYd] 1>n#]hwu0Q$ ӗ1C:0fȘR fzjn }ՠ1-&T * z.xO dLPs ?h7f}jp1O ޭ(MB6R!,[BDOw[j@ W@g`?caYrQwXmHLUGDžX XkPcLn :p;XGЪzQLZa̐ pT $@Z fHcuc01,\oy@ZQE /R" - Ubh`Dz^~aBǝ.%_c3'T ֗Bܙp9f i:ڰJem8BQLp X^{׫n[FSy0b5bЧxA*}S>3TM>>g z 1CrqH'o29BoGOnaYjp4uR58I߻caYr (kcxcsVӨP5fbG4ǧ 'o ,YD@;6*haق_CNˆS+#WcߕX7g̓bښꑻΈè nW#O7AǛǎt1##q|uNrj hC m[$hI$hrш MG]kK7_޴:]c12#yv4'q^F *  9Euux?BzrfFo7ץm<_@Ph%W8oSh~Gi!mڡ-fq@ N=х%o;NZfY()+)KHOt4 ҆QFbKA> } [̐hqD 2ɳwj(sLb`p(f.#wݓJrOIU1, 7WAr9} ˒v@ѣ,h TKh(3s{zm~:Vo2CGOvU3hQ6wa/4Z ܩ²FY1tiz҈5N]O/\XK@*%t AsUN0y4xA4ϓre1irR5f(HC0SAНHG1C䚱hT+mG1CNieJ$J̐ H9 1C4Ey%36=-F!Ezr8˧z[xHok1COIH}-QZ@{P)@vڧua4F'f?j|a]-VUݣ -#)&&kKJt klk?|VT_ۮ B̐m-@:)嘡/KjYXۋz|LHZxop $,,jPXzQ e?lj9y;X Xը53.B|}wO܏q&^9˙!FIoLJ1C?[%@(f6'1CDɑjTy"e*O">t.f*'1CK O+բ4|v[crW$Jߔt.]m-fH^=iFP[+V1Cԯ@h>HSt͈KP#Pd\ DO}̐%_R̐8Qz_"9[˜X ,(f%fB9fhI3rqNcZ^Xc6=F#ꜫpOՠ;; R z[:Ko[Z֨4d8WCTaҎ gt9C=ízvi[;w| L.}UI9yXȩsX:{Ro;^8,-uo/bG /s^bnjR"nR#!rMI1NVU#aW̐Qi=1,fkvϥ!ʄn"ye yZ[~cK1CKŘ y>>ųJ&)4l)P!vK1C,-yRPٌ3J"}jiCG98q3Qcԅeժ oKEb퉅ebb w)3sV jЃ*ԧnw8K'_nl0Kn!| t²%Ugby?YZph:H}JiI$UG~HoG}cơ #L?h1,a175fkr:z6C>{Q#_crP]2Z:~_t(f(㘱0 Ҥg3Ԩ}~kj$#L~oV Lc3#$ΘY4W㦁q>F][cEҕm1C3 Ԙ!AtLpk'1-mEfѧ6ᖱFPQ]]ݩ!O1'! >+KK u S_ؗV>f{B__1_(f(M.x:c3qBtᛙBg@uEP ZZqhaܵOt;[ΪUYGv} oc*KQ'zAAk lyMFqZ5XuGIo vs[x>@JڣgۚXC2k&_3Cǫk~CvwdqeRbVc0P^|}hߓ#;7}vN1x10pWB' 12|-V+[[-#bRDdBLQR҂# s']-\SҊZaa 6bdafd`>؄a~DxM C "aE/|Ld•F|Us36A$A]7FpNatƭ#D"^DЇ0`rV =ʚ;*w80í$~ 6&V@X8Moo}S*< Kv]k)W?F6#{1, \Kq3HEs7Q#Η|ԇ_J}R'뤙{鯍Uy&^{3Ae;SЀrڰ0ϢTX927‚z ʏ\ 0ĩ? ^2QB`ycf*@̃(`,`LT_ݟI#(ʸgmmAѧmI:u|f41G]x9hٳe߯ۃOD,uB,Q_pۯíz\#"<@-8;! bCiC=.>ܮxaCl6cz|P<>k³"0QXb" I(⾽RZT*?ūSF#t-.ollc6Ʉaa8'*} ~#M^7i`ɗ`ZD ;vMnR?" 9k6٫!Ug]zTŗ/ 3HU|@Pd2 +lE];'ikEoIN>BqZ7z, k`[9hn(˳{?O^p1EJDaPhx.9쇤ƺEjD!yVpxDPo!Zo4Oz[m(|ш ZqfaҵlCC wKxo=ONxu!Y53^#XXndqDq0މ'̪*l7%ϋS-mxNV|~oڣ5|)ດ1 QIٳB~70w ~cT ZZEA} R&~WuU#{māJy-1qHfT5*k٨ {$KN%pʸ|qҌ:o/+O5Ϥ/LkDهPXgTX6>!,U,cMmmAD@ mu|ܛ6 |#vm^V*E--M3` *,$> #ONga~J7_J5qTBo|$, ZWHH;)0k\~t)P]<+q}0 NY++(6,]KgАሻz6?5֢Т& J+KدeRӃ&Q=GyQsil< xIXx8:(خiaaubX#M5W.έ\c亥~>;jeesD 5*+IQ]==&ƶ6}S3vKLKxN?c_Q$-(RsT `7 $3ZmX*C{0HȆZW)6҃7cn:܊ȍ|LH^nI^|36<]MP͸[oחd %/|P/X,瘃C)D+m<$|(%JY#@ĥI)O0(Xh:VB%fCċy&aKӍ )/-8mTH9)@C횖r8y93vB@o2,džbzep1G]p.@Ŝ˔A&ج-^Vx1A0l c/M0lЇ$^!|Dbپ6>El[yDž-$~X3Ը§%j\I藤N%?) $$!y.9b ?=V}r\%"_<QACycN&@lmWHާMn9Jۨ3VUa\& ~!cwm^:1CĖ{/\oٻP&|/7#g爽N 5²W=. M, l[<=7 WܫPUMízq1dh#"" {~^mߣPs$/3RaR9W#?1CY߈hcxΘ=v1PF̐s&RPh1C yd=~%|S~w x dLu WZ6 f}R|/OJ[Ԙk 1 ӭ lԼ9m_#&_.]$4JYI( $ՓPNCGAbhf-Tbԙd(36J7o/Ycd˟2<g @mV+V okLQ&׭ʔbnw E`},U,D IDATJ,uJ,sJ, {Gň@F_F'\svg78qBUXm^~v1CBU\jyZ!`_q51CBF+iP{>cĘuB)f(R̐'}Ď6$"r7ظI;Z̐0fhR0뷝 ||61Pj ]r '/ k9K1Cn`gqԷNb;ێc##XV 47W60ݕ&eJƳX 5yi2D[KQ!c8X 2MTr?,1WF}yz ~Ug90q diK4hmn/e_Bp!h tx9CQ*>zB ~ @K@ xQX{bd` .I Oڂ.=>ERP';ڹ.f(i8`9 >|AGcs8ǦR~o>b̐X9)f?, 1ެh3a%f)$AцP /ցccb>yWuőKM8 aivo(B6"r#U8bԘ oq3ȩ v3M4S{WMڍ@f wr_k >.Cxe x;Sh $Vw,^̹;Xx@1ӕXcb,bAͰM^ T@qps:QhS;>X r,@J,PMK ^;` *$ Hr Qji!蚔1R,PYCUb. m3Mmf3n/yGJ_BOPT νVo*2c6/h2՘\бO^FcpiА1C߭+#WELO(@FJ?쟤5Ř!j/;fzo4 I9EZ! U j;ڐ\;b}f?д+ a \# g8$3t%h94M4Hk1d3ݎ^ M#-9!r|yJ;@H1Ci3t,}1&כ"avSE `3th#Ҍ7O?AZh 6$"r7al܂ƹ3/z3ȹ)Pb"}qޯQvQcԘ!%jIJ̐i=hp^k]?|k3Uq(kw].7` .X5bBc9 kTݓqXV<MuGƳ ,HmbF2?Kx@'v m؇nX>(cC>e]k ^*Gc ,:+\_qj노³"p/!fJ@:A,gEj,QWcqU<=h(f_dF^}!K1CuOȳGvnA36/Ĕ@wXx:]~ X7X!C1#.A_*4? wLO5McJ\~شXoq T].9<ECǞ!u  ME/ 0>wtڊbHWw ^̱r|3OIX:QA@^Hxɶ+4*W>a!+2 L4ƛv ?Cr4Ъkmf8g %:..hC^DZ@- %qhB\XKXV_Z$neFjqg ċvɗ6 EP -=3֊UjЍrz9f((z4"Z~ 8gxsLƛ3p6톺5;5ꂛ8ҘՕT <ӘW&T^o(Ti1Cy50J̐8fchdh(F7E@:Xƾ*B; ZqAQ/87 Hae:hу^mfqN}eqZ뫑A[PdxDh1Cߐba[U_ޓ e׭`yP SMAtۙPMK ^SbfM7b t^(Lb-m{?mL 9]q_t ^Xes{Rq'a\@mۡ03eCѦ+1P4f˜ 3@b5CD @dA^zGgQTm9Jzӯ[:n=?!ݿl[)S`;47`op-x轇Qu* ozF `ԑ5DW#A[:ض'U6 +~BX9n~/lTHF7!s\+sA o? =6M*X]o돛2Sh,GsXWh `thU8m?\AR'M?7j k dVUJOj}fAC[+^N"lV!X Ʉ#ǷިBʌ ~9,T d5x◟`ZPeQn_:RLhj_~/. -N}, E0}td Pg b.cfʱ@yI, BKzjNp!@ۉ|h5.ЪlW  #:'Wc&G-Ubq 4Q*kj[FCqy\|7_aق/b|0;! _`_^zil<66 Xm  -V?5QlYüddWigĎ|fv)rY/''^kȩǃc7 m5̷@҂?C FQVq[-8?бKy0B)hv'Т-ShGV+Z뫜n6td}Eɗ!0B{5#_Հ{QM3qL I: .ű?Uf[umBƵ߷“Xw""|A9PeW^aj>w:ٷՂ\Бk?E[EPRh @8@>?<0g [:&v꬝q.$V7 nuCG{QЦis`\MӭЦ CX_pӭ79~]k֦[- N܈ XZk(i֊ XN"#^зZRWZVCIf6-VA4Ե͛jV-UxO m``pFȕ}Q8ߗ Z@ O!ee@+Z^kmV+B`Y hlmEk,@˰onf6+'Z,hlkZ)O8Yڏ6 lk!j[Z`LHƭ^v 9 EJuS?㼭 hkx[B 턵wtl9 ?t4ТSS2W6BnUȁ`p&B;w?s]m2^$WpKߕ>CuD \GAHP\" yýI Ϝ8gah~_ua?4 bQGµ^&:YD[mu0IN~BNM n="6|p_ Hp1V;m!FQ6(n^p`+}m}K} Њ;z`hqBwC^{:h1څxVoVr툈Cqn2fh=ۡE-8۰ڬ˓} DD4hqm?:֡xXɕ jWQFO)>7) 4 `1%蟃?DDt zhz@c q T'@uGp h } }~B_S@ :"""Ȁh6h9Ͻbӛ|?<'"rOz \ТuUL6x@wR/ Qo2kPכ `r =(DL±ycV±FHS[ 2VK$ ߆WZ zDD۰#""հ?ثGDԻ^=""}DDv5}DDDDDDDDD370{ep$o߮A:e6{Ȉz Qxm$ 嶇 c^:<"""""""""KAxkEz{t90Uil;5 ^ p<|)}spr^_w+ <X徭2DDDv{`\(A] ^o t徟Ѕ> +yz7:nϴEf{`%ٰ}?1q]I&ǚ#O `qWwy#zDt,X9d{G?Y.7;^=" rt{d .fx&~9RwVNǢ.l;@PWa]8,2 "" _;'D];k'NSĻ~퍔'نۉw+n?"""" `# w}NdzO"""rs>Gc} Q4πa/u;mWFG/Z7_Ukv"L؉>{jg]'^ضPpP' KŻnáZ.U;~ox蘈)0U:~,So^QW}@a,60CDD-@7ZN=^|~{p'۟`|{ !qa25}݋C8)0ń+-CDDDdcS\dۛ|_Wxvlfh'DDD!/<[b€ΈR8A""\ {pk`itk.&O9kM*0 |֍}'cR~{HDDM/H-p` ;vA"0g.9 PIDAT8Tp>/F^%:#DDDԯ4X~{ x .Z\ `n/e?ȻZ3anG5Ovs}-Q8(@D9h(b/]k 3l'f8`~ h\ |_q!a Bćq|{ %u_bށ[pPﰸxub>Jq8_oQt`zQ_>Q7ҿ} `_k LvݰϷU".`["o!QUlo-n' oR4 ?6 JW"?gV+Ɇ#j60NQ_o+8rNO} 7*Fn_-σ-rplD:#l -Fȝ,0f%FHyF_w9(wT9iF6 #s :`ZQ 7ҿ crni@`vqDCod`xP}$7?0f;@ߡM8Ccyx8eQw#3.GO^:ǿt}@[T8v* M0 @Pmc!Q720| c ȑB -fXTx*q߅V6;8(@V X-045ܴz;yڶ0_}D9G'/Hd`oVlhݥ{~{ 7~H9 ۻ8_n$/ =)CwF$VtC0ꥲ CYZVeJtZ/6˼zu{8qִ;a9Μs+1r~`„(^0 I)?E(6 /3*:-J)]Y"5(g$}`@0mo88/ɿ lCR-Y"5( x+IF')@R@Ѿm'QuQ|H 'jGQlY*SH_#=YCqfpIiZSFȭԈ8 F9٦GB8Ap.!I9A^`BmӖ\VYߖ(=Y}qJA} 2'$?{+qIza۔Pk^\[7 r;yAR 6-ϋ^>Uj(=яϞ)} 25m$ _Vkem .C23jK+R`}.>~OգRśBBCRtɯ3'E(vFI~?Z?~'KoCg5 Qއ/^#]Y1x4ʷչ\.3,~,hjJh~g$~Eem)݋[OgMa?⢻ %yG_K}<|n:k<|6IKM3=!q(_ɇ HjjkٰdjUaPH—{ަt5$Iʇ t&/I?[)(ź73@$5ݬq?= E{ 3åXy _+ " $$I$I .'fHԴ:Mkݲh"sF!pBR6I2 NtEsfej}U}ۣQ]{kK}Ӏc3S=> mIQYgwUII:"ӈ51 zFM]3jꪞ#.n,TCm(R*fӻOqYIek3HWfX.ӈ>r7s>?. |Q8ʿK$I$)4/gc,=G`0!$I$I⠀6kA$I$I!Y IuVAH$I$I•n~#$I$Ip5]AIENDB`pairtools-0.3.0/doc/_static/read_pair_NU.png000066400000000000000000000241271345765554600210110ustar00rootroot00000000000000PNG  IHDR!6msBIT|d pHYs.$.$* tEXtSoftwarewww.inkscape.org< IDATxw|׽BXa :qօTgk BUqW[jZZ:ZPS3@$?νq$=x܇s'$sg|NurH5$Zy IX)Z׃Mԁcyz&ǫ| ׄs)Kz8.x=p=B.mùt ¹Z% 7sHH%SxIMޛ}Ns I7ɹjYsn.LNKY3Nqt:h\-Bd{>N.~$?\:s ]=A{6Ɵy_tFox |FrhtN*,-a -2e/ؚ69R٬jg{<~w\پb. ec4!ľ#(> ug >A&E3|xOKq~~z'ׄ$ԷIY kc1sG \GX`'>\~4z/H¨\4se%Tji]24;63o*w\:f[iˋ0w*ˊHʼI(5)1hCVr()bTl( qC/qvN <Lķ }/z3-2_m)70W4Wr?~)[ HHm^&.TV0K(٘orIi^61υP% +&2x£$g婫(^=OOOˆXNrh+lVw?t=|\6<ݎ]+Kcp' Kq'~wB14sc3G߳mMu_=9^3*O|:bi60nئ=ncmDc;,"m1$7 81iޤ I$ ə|#bKMOR39#?B019fя1c`f(/O|'p\Lv e~?{13mx x_YOck-=.{{jΓiPe󟼒 +$K$IO^E3I*'Y$5+܉I\lMzv";, Ƞ&}Z^4uٗKJ0lWzs?o. +A[0kmΥɘ5uU'Q[J JksUwh4̪b\A3&Ҷϰi'n.%=$՟Ȧ?rܑWЮ>VrYd6|λq9گ~;NߓP+fc<sE9A_b{7162 \m'zIui: = wޚ/;kWcA#HkOF1?tIdW{_tZ݉~5[Hek,jb~Jѻ6USe)h%p fSs 0gV[ȥ-|^wɯM' +h#p,XJ7}\2,/`3jyTg>4SGջ%cf7;; rG6;TawpwRpl~\HdzׄǬU3#^GW4LIHyǵR D;v ѕb)EFW$X~dҶp.i6w\/`v*N%gtb"; X[ +tdG"K̸\f1x$If(x2?qS˦ڦc2#LYH9|<rL ʟ,nA LYry {3Ij fOaŝWPe#-SoM.!Ch?F}Ԏ= &eك(IlҡmU?֎>ڴwƯ YYՕvu%MDJv76~+wYy_gz+z̆ k`JoF؜$u9p'wXxބX nۀ" t'cݏ.|9X_\S,y.9܏ݜb!v8z7'껴ij0We3&9N%!K)r*Ҧp6R &Td!BT]VVKBj:+@EfKKJ:'PMq'֜rIHؤ\T,qR4PZ]BT(]5\Jb6HWەq!و4h%nìh- Q$ljt6t?v)fH4V ^$^Yfa&wWDĚp?V˱"`9 L|DDZn$S.yFx+p?f{Yv ۟j9VĆޘ՘Z!"--@\u Pu͍`? H uUǵs)SGR^mbH0!1ivӑV$BhggfW-"-@̆8;OZ L""-RI8%k9Fx6fRDDXJl:]@c""V A9˗`ʯHv s[ atH|xYs l?8b^""R'~l9037‹1m&""ܫt˹Hq[T>L̘ġ1WZEv.^H s1+dG&# 3ᮻżDD \a;˹ߛKp( wۼh-jSU 2 | =3}ț|; [}F_jnDVx⻰*&x6p$rXK:=;Q+p/ˀ MHX5͞=pe|'KIy}(/ξטly8a,isp+{̉{ !Up)bŒָ'= Ǭnc8?(Aɛ|sHLxR'3Z9sY;)Ӂƒ+wU1OD/c锻(n@~MpErꘉ\ D`kmƹ0XYOj0c6iQ6|7;q~GZN\VNr^{p,_sR*}F_{>'(٘}\~޾|kgĝ>6]3q+ `8-!gmy e#TSq䌎~^_w~^m.r>N\=}{x򟜉a4~M S9 \m53+ ܕY9&"Ҵ?xu˜W+RT'tH2ĕ;KX:NҾ+=cԿRQjswڙS['uR:tkVdR`0"ĿкO""p+|Cry^&Vj7غxBٖNw RxYl='v6rn'[sdDQie,}ݽbMCϨ% pX ~uʥ+X~n?P:{P,95SlONA`J{+{?aj SewˡgЮ>N\Q79cO{KMf Ho=Wؾܝ;23:5g ѻ8ĥ}qu8] G^Vd:qy6w9g/TIS29DCZ " ;!N)+t˴.}ss?cs@052ʋͿ+IJrW TQ9CvU=Kq`Rdzo bXGtq2""M#jrEvG5#u/yv*˜xͧ/Pj'$w;cbV}ϺɹU;<1#z "Bd? w5'pR&T_} D3&TYa&dyum ;;ڏnGě~Ms>i&-`UA`灁1NFDI;fwV2Q[(zuEc[|?p@0\7;r_wWugKݪkVbviR:tq:qo}~w.bQ)왌;%{xg/Qd{xGA?~?ī'fy4""-FOJ'Ub_mrԶ4%Wߙ7_|ΰsh|\Cܶ?ĩў fKobH &4rV5 J^wgw=|owugvEw_a4'6ٷ8qBEug@梫95ڜs:P0鱈HQd?W;Wlix}+ Rew(@/ug/y.( ]/S??\ρMvvӾE0I'()։2z<9K^;t:Э]]-s$8qU}ԮUV[}WM|`X_N$;7x#`W{Cr;D0)շqVz6S2+;3|:xaF{.n"m;$!xZaHO0iCC "s= &9T:wz*U'+}̷b~[,lw,;SJ3;v)ȮG[]A~?t3<{U\8i'x -qfBDD~5xX&P%[|zѩz3\! L UsFnL}e)/ߓst.uuϧÐI @01,grE6id19ǺoPb~6nt^sL÷K`p*06緥HiW#>A;7%c6GqX::)PQ݉{ !x7ou7NLJN/2o+E.DugQJ75ɖwb!_۞ÌcG0=WY˨i$O_*xIDAT`A^5>@X4ΕqBJ*Jؾ|N &=t; 7PV9&!-4(ȃJPy)k(߱$}\/Ce*)^kmYGey%WX1Ĵv}եWQ^yt:ݽ2>3.}!7s(L{Ж_X̫DczyCnUBDĞ1j50o7^(++Ft'6F9^`QTLܡ1+yO='v"Mdfطʅա5$"ҭve'c*?މ*uS, 7йCEDy x֣sm'HaQoH N@NccrF8~} ,""0>M*"+y@(jF"0v1v$m' "g%v""q,Hבfw`!15""u  %+{MD09 5'czEG""uw)/5iEBHݴnm{ ZLGZ5""us y#*CI""G5p{H \""Hsiҁ$O^צe@Q$G~(JB]l_<<6N6乭:7z^l5zkk/ >ŸTrG3VV{I45"" z09ӑ*\ tDC1Zpw$HjMc-#Hó,^1"AS$!5b:e*nx#5}'"u ҺgF5v~'4 cHp fp@V6C7Y̥AǏQqi&"+pu$c(ٿ8b:R9Av4%*UHm2j8T[p/LNSc?4g">di,wXʣAǏ8:s .,~Vʋ!ğqB+j㇮Ebgq㲭X,#X<"ڷJSc,[>?~%YLGjq/1|o긡8~Z$'Tɢo"TQn7#V௞ R.8~LTܮpi0'Pj!>|^6R{}X⹭Hq/C$Xփ_a1A1f2WxAYTV{W5""5+.&\驲E/nFRE9o_Qqz;c~o޲ԠS$b"F&DqZ5""utx$yo|Q6S1GcߧVjQT.UpKŢp' sZrk0k};S2i>nG*4C2nZ2x]?;k2EW@:%qJH|Kj4C03Q9e~ B$Qr8$T˱KU3 S!.hc_ H` 7 c?&2t!-'{d3!^2]K?şA9kIENDB`pairtools-0.3.0/doc/_static/read_pair_NU_NN.png000066400000000000000000000413041345765554600214000ustar00rootroot00000000000000PNG  IHDR,sBIT|d pHYs.$.$* tEXtSoftwarewww.inkscape.org< IDATxu[ULgJ"nqY-^vq)H.n)@)5&&HnMzy$&77=ET;036"jWu!U@c!DrM`g!$t?\ eX!{c\ A֎ """"""""٤ Zn C-hk.Vq!JY\,:%6NѴE(=(FUD]yzr,Pkv:c~QLz$= <]4.+1,,gʒ,zf鱤cm67gkkoBK}Ynre]lUT\Jo_ux)YU?fm} c*Xe8p,Mz!Qr@,O!eiy Sidqʾ~3q^֟'?o8I^u@N fItxxawQH)O~oc_'[^>F @XQʲ%A(,cRw>$Y/V_'{˳喤,; |,^eYk'BՔ|9ztYF;q,8rpn'|&{TZVp7og7e)pDunv/+fOMCI$f./Ea' B\ twX˿DOSISziY4QaR,9K]:+B˝ۭ_޸p[,{8r;p},Nr,wpE"|2&6KhҾ;OFROVY1DwY}TRdI%vܳ/6eU"el _rQD \(D-H$ p`p 6rDLqV`ۈdR{c c06ͅc;rJ<8"E:mZ\ڄOVfxYf,Lot0˨"4Y:dH<k_d]<k_0|o_R^,8"78_eGYNNr,}1K"YNjNx.vZX k&s22BYw Que,1FGnkcM̲,3QgID)˝Uo. A7[;Imteobבʲβ,Nrw0Hka-e90BY@+Yfڭ2Qq Vqp4n {Yړgy")QD,5@ϛC cr_ ocg8\eW;;,9"; ?Fbt~f9͔s;NXM Z,fy2..%UdqQԘˊ\FE?r ,^Hv{ְ079ҲsVۃDQRYDjk'VteeI, V|xۮo/v?CGvٛzu=M1c+?e)j,Wj,Gc9ΩN! l\CEEp/~2u{|Yܳe]+^1ɅG3oeיeޤl5?άw`e h|g"ZmgCej8K|a#M[\yY< ~Dqhй$Bp\;')&1]Gu5r,Y&1]k Ǩc?pwkKa$j |A5?6E|VUIt ey{ila2{2DGԈ)!cSO(M\}L6,ws.P dɋVͭQYwŊ՝x 1!f,g* 0 fd9 2>wYD2Ύz$8eNY%?˭Ul,Wnb۰ygGYnb[)l.'FʵW"傈ey׾+t…IY u%\wX \kwcJgk߁%v.pd)jTQҸm._ĤGQj,'˲n923Y:,_IೈT76Λp,:ˣ+)DDY]fʱz.bmL6B,*.S}Y:D K3l,â/} -t!9'ؐKWYNʒpᗜK`,A[wbn/ݹld&{b[wbQ,gOa}se/6Kǔ,?H$bX{>VOk,GزP(]E~cvň} lĘ,c &gK{,^M3g뵙DHOY=Hs).x(LeMe.VŁDXC?x/^qP0)K6.ˀ$| fe(U7Zʀ.˾z$Hۀ<3vkIJrK,S(d+ ,921MK6Y'g DB:,%Ջ?Gg,+KZE$D\JmeN=y & c\,K&LǂԄ,O].GuD icw٨O'Rg瑘sk4Y^b۰ 8NK''exQ ;aKᎲ$a*$I\.:,&óLlYO:IYFJ!򜃻,'v-X ʊr~ V1YV}6HYKYznD, ?u&+esubbrTR;Yڿ [%ercRqHFbEb @GY+aKv +9_uw+p{wkJ7g)n Z@7eybl~gW꿬d h PڪI*ײzl*+*(.il'2BR %4I=Y2_Ml)l?b kqF@O`\YKW,=HpT'k,>wV+YY|lc%Y:cvT B܈wX4 h"D)$Y;rQUDDDDDDD Si$틱"G}X@"Yw'̗$rl[ؙ1 Ga']"`C3~ >8p$V{ |} Hu[b2wg x`26u`{ՂK&aT^ٷJ` 7 VN~Vc_E\UWA5Z |-Ӽ{KJH|8Hma}Ap24sM"{}FR[^6qDR_?bNU[r%Ӿd2O C59,]}(K4t>6rN]Jg ۊD./|uJMeOX|f$6Î|=ŽvA_j3K~%(\X댘GuXHa(Pw.ƪ)8;[uD6*[2I$v5/eX!UMXlQ9`E2ޑ|dFS%/g-E7}7" N֨^Lza&)|x- l4cr,"> y$UGV`4V$yNH⵶xw¿^ջ)'8>OHއnH K-VhX\>فhQD*a%ؙvNI!~m)m\ +%$l_W]S6~A9M#XgEUHNvmC$jSp|MWҽF$ðdG ^m%X}n#Fi]:+BJ5;!3ĉ=&XJW(,A9VX],ɲ)z "P}o%Yga{$rZcsʿ$8$:SI.~%%(`s^'u?EؼugV\[8X Me b Pg$ #ؼZ\x CqoV,3y I_`l:õm?Ӏ?('Q_gl˰"zҏ*4Ev#x |+ GO?jSh Rpc-v븋bl$U3>Sݰׂ7W<ݛG m$0YUk\"oHbVPm#0ض$͍fk3RP>I5^?ߑ~_hi\JpI2Z!NOnDZB{F C9y%hY߫V'4[&7]M)\(僩ߓ?CrI4iX^S/v?լ0-ȊR6^+~vz#1yHs/Kg5vb#Xxvָ\R6:OK {1U Da(~"{y)6vxo$0e(ҿ)waBc^"DͰؗ]zG:"UwTxh3eh<խ8;{UO]tqFSHzaS|2T=반 ƦtVh5cs~w!VDY.5$] e؋ ia+a5&0fQW;W%t-`!"RM٣r1lH!hx{FﰨFDC[&1a͒ؗIf# Hؼ}dGĨ{2 B~dJx1x;ƹ66Zb7OsۦG;yS#RDQ^M %y!${5F4uj;D}1M?fRa)qXgk%x菆.o8ڍHJj1Jp.'0Ӻཉ'Ct?nU! 0 Ϥ[WD$Wv:wQ! ؔ Y1\ lרh-!Yg8K& $^Q_Dݗk}fO' X*Y:V_R{ľ5q+Ha8ޚEV"شbR \Da-H9M.6)tFTj`)m`5! GNqt3󨥳tRc1sBPN^gIaoh+a_$D#ӼCicwoI56WRO*<@atyc'K`l옵PV| /A5ۗ.ZVF~$4\n4bDFLʃEs0x;LyIv-ѝ%n!QK潋44ؗIw;Rh t|)X|5ߑ#8rc,MQ`O(sI}0ϫ0Et]ghhkqۧv.yJ|R׏ԗMH!H>9/ɗx0$Nkz}3V6)1b V6y:9x?x~CEІ\ [-ie5~>룮)V,H>Mۜt&nOG )VO($3p闏jJݗɶ.K%v1۶jP$eYɵIpErvT| D"aju>RQ)l[JBe3zv0|!]--%74=)%R$ؙ\iD˰e0+nNW?H!9e:WMsGV~t'NwH_s޶Qqќe4_hjL⻎Hv5vuDK/lZD`® VDQͱbɣ)ʰ%PH>XD6S݂U$NnYx"{WfEbchO΄YzV$VY st5K"~qblם&h=^Fg }gY||"j6{/aسYKp4E,>H !?ޤ`ddHzclmHy0g;%"uS 샍 {^.&t0ܐ,?T8NGFv;REjG!Nj!?H[ 9\C3D$;.Ap?8RZޝtsW= 89w},S;%""R KXӏQgdSHvՅf """ z !o(*BDDDDDDDb}i~u+rпx0H$#zY Q_b s980],i\h%]0A| r p8708D] Ib`\"p u4YD$?%]7 H^:%sH$^+"^R+I*Mِc&|ӚNI "R mE%=Ӏ&>WBxa!"ElX0)hZ[}L:*vpHDDDDO"O*`nj:%] 4סDDDDD2Q䛻 ``$VyYiYD"""""Py $>qD$< F"J~m33c=#"RkI=FnC*,jsqDDDHT|8KTFT1VHSDDDDDDDr3 4b́ӱ_¤"""""""9untobsd<3yiy:bg+T}@\$MǦ xʁ#G$lhw2XrH\k&Jza{8uXxnNO8â-wݽɞΏn>x^K@Qq1Vݻ~x۰i6**+*h=`(=v96~{eE9SY邒x5Jg {94z7}A>qw',`sG&N(:"lZϮ>8I$""u[~@; }Ί~^F-w(O|ץt{4ND$-{o@R6 0Y}#Cͻ d!:_EJXq4XWwV;<:oot$M88tzqr=+Y[߸Z?g{mw)kQ .~ASlL|9Ί݀n∈H=NcEǺ ӇΊW0 C[$fw|BQaEf37fقxAce`w=vkb͒2~}xEtsLy *+l5; AW}p gX aC"ݰg:'gObK{j)4x`gȊB|Mꇴ:-#,./DHC,99L|zxF;~Ȱxr*.m՞{xOK^HQq`/OힻO}Uip9V}4"B_.N lk} ϯ%v,rGDD,D~sK`IW'd? )T/a#݀a;NcY'"k/nwHaTdR0p7cŜivΦybq3&1'펛I;_M56xqcT=n0s]ȁ5KoPwY"tx5GYDD$>#0T958WPJl}/ڊS=^NO~n^eFO-e87VUhO閞HDtojCKbDZ/ lqv`#,#hMHJH7cSz 8ZMXp63^wݲtjrl]#50u^;1$z][Ն""fzCr+p1c#sK;6|s>owlOڮU]'\GJTEE 8(*&nIZSo"J|Gp=wa+ys/N Uz,[|徴y=3CO 9" !XL+DDD?F[ lr"]TDԩ0zIQYϏ]6pjـ(dZ+WADDDw[V&6Hfqop٘qR9Rz1瓧YvᅵLI*d4w'` """ r_OŀyFn󈈄g͒2~}1QEt6aOXOo7iו_Lb`"_oUn%"""zי0""8BKjvRܸY9w|;Vc$ ##9#"~1tԩO^o7nۅ^{x1gS=z9*0ceL~{x^ݤ ]TVZSe#/pT 4]&aDDB1S=(*߈C}:FMfNf'O7ݍەL{j\]ڪ=v?1<TYoq>iA`r;g)$1/pTL4aDDBfh *+Y]O noֹMuLHvQq#ڬfB1 ~" ^seRH&:Fɭˊ qj)=w>f&\gѲtfD=XSӢ)}2V-͌x~#Kʨ`ʣAee.[@~}VV/4x"g)$1R*li㻼ﻢKÈ#k1;\JuD2#  """@lY1 OnC^e@SGS#HJ=NO{m|+/\áJ Hp gHYH9+K6ٕvCou7#ԑ:䵁#Ҡ<XRՆܷ o*Hpjbu>q0ДhK}Neʣ|OzpD|><>ud2Y1g`SG:#>e!tfy>O(.mJ#}=#?@:@[|`tvuDjd5gp8""@[j!9w`a P0` EDZd{lLtb oo?$xE;#fS;,)cƋoy7x )]1ċvv|/~x AmGF^60*WFϻc`AqEjY[a?#""YV q2`9#nAgCD$7Ak-o lS#oܓ#!tV/|ujkFp DZ6^F;"""w<;vkH.]0wX|*MnrF~z >SG<#]o#!ق((i}iq)A.sDje>} ""Z \ȑvujHy{4z$H 8KPYQǯ}#icx{3dͲvn6xFx[92:bgx뺖b" }d.GH]].:DD$]@pU#p{7ԗg4$ Q7߾lVw÷Y8H}SBDD|)@vUW,mіxcnQTTLmT]C ~q*|GaQUѼO~`/_y)miM5iNQq#|>ͣI8)_z_u,p!V<NpB1xDeؙ2gc'wUB`O|AWkTqy.v$k+hKY9v/FqFbND$otoOyr*֮ۮ 7)N:ҼMͦ4ϿʵkdpH Nyԑ,i$:C o>s%{c% @d7q>Ės5^BQQ;w߀PX[a/I - 0DX/e$.EDb Et$|.ťM>4n1~e TVT@QoK޾ŕ ,"ͺ㦻CMz~ @-HKo_1w:|2EJ8t! +yXFW 1](z8k!(tV>_ KL.V8$""6>|p8im^w}Xtw#;wb'v<_&1z.>xRg.ƆDI_ 9YI;Nش(v:}{+/ ! Ǒ,uZtO~%661 jORl>]*muZt]7U V3cN;([DDę/YQu6$ӆyj:özc7wqDDDbJ!zL7Wi;2Um;3 ""Q#ydѷX]+¦4T]WDDl.џҷ>žk llT3:ˣ4NDDrN`5;6$ߕcK~:ĦXE7sED$^:XGy! xX:HV:]@V9""""-A^I D:,\Su-4tls(Sn∈4hI ϷsEum[Ie#_ 'vy"p[۝ sp_  8ktZN.b-""""st|8H"W=-csciҾ8"""""84+'PHjJ['9Ӡ~-}kZ6}۴elr)""".]( 9A _MFOY+wi|Gsl~Vam2[lEDDD^`Fr6'XCBx@""iM;aZ:H"zE@,?"`}%6R5a!"R^^ (* cTÕh=݀BxMDžB'liZۄ0$;8e)7J8M+ r1<ϡ *\ Ƒ$|H{"v-qYt709vU 3uX^'"p8t aIN1';""nIp=_tgc-p}>Yɡ$*#"w %sǑ48[NTDc$9O!"٠sv  0x2v-0:(7H8|(:uF0Ur;LYDĝc$qHJ~?WCj|f+H8s- )1$:~6 9,NdAa?4B$7*+*+1TVS|2Uq"i-E7} ǷTZ>+BxbK 3uXyErG`X7HaEc뀋Hác$¡sx.&h^#96"hxHn]|5?}vG28YD$t$R8~t._lmɭUJ˘h5$K]nD8CE$t$R8~$V]Kq:vyl:SE h9m~ǗPիH`sݲUyzm/"czxq \f_'0 ;9| XY6H1۹/a -Q?.azjcrZ7yUi0:,DDW5V//8$:twv}.7T[Gr9a!"^cxH5~nA͇!^ϻr9 """U7aLa_QH X @e?=4C\DѮ-jxvmo-*"""ѵ;},htL'IY)IDAT&~oO]sJ)n ֕o-6fXxqA5N*"ٰ #w9HNEt" )#Fr708ULfj hnz +yf.D" Oc,|%PYܸYWS;Vn~kύ,TI}wʰmKˊjVYڑ S <<2GDkH9M$"u-.[""RsQ؇'W×M$UYJpci }7688wX`F-5;{H#,W`覑nWܯ󹺊@'D\ ¡"yJ"["ݗVuXGQlLϕ5%D$a!R8a!*)"R{  '1!`fu?~uÀ@{<x@:uX_%IU""< 6x)VW"9Ԥ떻""(W/{}@2y{-) E7 z.2Kx<$6BG+9/QRT@Bv47\ >J KkIK;ӓ}M;Wܣ^*;'uND|aFQ>Qc#"x{j8xrYA` +zIzHƫJh{kKQކ4ȚkBj~܂C41Nhґc?La~67hMq\<ԥfcWxuh?}iDqӈJ\ 8:9SFtJI=n'uC7Hf=%msNfU+Xq\2T0 p0a5"F5oǓF"R|d-SFD&ZcoI1kIA>"&NO-a{wW`btd-1^/?77N1 bb^4IP/’b^ÅA<ؽ9,_TReK؟\`*pDS. K\kEM&OtbSecdm\Z7H|6Zr2̥ϑи\~2ь\$Iͻje7oqo\~Zm{iŗ )*{%6B;ʣ˖PVrOdR$FEp K) PCw?r,p{Y@\DGy::Ғ_?gif|-[%;ߌ?ӆhd3nt40-dYĶ݌^N3ђKYBAhS~;)kViɥib"zI2Zr &H[BS.iX_65"BVl9Ɍz>MλEK.Gf?2:ΥEc䒛xS:eh%op/v[0u5'6LۑOkɥ&Nuephk&FqQb6㭯UɷSSF+2*蹔xY3N7FgNs䒹'ĩi8q);M1/6q*gJ&N>g^%ܝItި%2kǷv褥16c;swX㤣wލ1藶.0HS.y[["tG|CkɁ(ڧ%ZmN#i'3>{ԒKRSHjqZ}z$6 ^9tl ېt3ڴ#;hɥ*B;6*6dq4nhAK.AGDAFSq-|ꡃu+=VE`KEsTb+ 8"/bCi?œ~+Uu-m}D^ zs9MYdo^Ί6g;|:9ⅿ2v@;j}ߟ,ҡ]#>߿%m?/VŊ/-С1.zyfcɤ}ku0}Եb 2CfƻO,%u~îCK.ɭ{ f|oɘ\ZvH ZrIhҁv=cƇ9/iɥ"! FN=IIZrys͟,ٿ=h]B^P1m50TS.pV k US."d5.DŽM%g>6xKx(DŽϓz\Nе8-} 2Ne+GDmvlM7Q;rVFZr~>ۼɌiӎs5ђKK5V0((hirv>KJ<r;rq.y26(8;}ɓvž[wrt |ZݹAK.e ĩY}\kSX4M<#EV911EP)䈭4gJt z/Z*'t7}Q P#Q׎u b|@ T.E,˞\OTrNft2EtJZ'<MB*\D9HqU6V}z6yr G%#"&[nZt\ Ռ4a6B0Y N@! &ێ*i5ǍL^@q5k>jqz?+e: Vs*hMkMOrMX 739j r+58ύP<Ȃ(rB%犓XTXfwԑ` ?u$Q|$^pn%E}ճnz Hՙ>jJzgչ8؉`֛Ne+Wo}Q]3:uYxK/  dAu0Z ,BUZPC^TM%=5.Pwyya~F>CiUnD4F-yHJFM0 כN؋z`M""PKGm${/Fs޻X?ks~ĺy`&!r}#L\:Xs.j]f=f$j {`yVRKK~؛`_8ǩt*x]ʞ 9h]-D 9.ƭx3ju5ԐcSNu^z [Ea7KY0pjUH4HЛNX7> /MPmGxVr/X#Xof6wgu?jֳqRQ^E,koڲ w~I{4n5&Pk4p {+S,Q/ߍPTa+;gߵ_z5 n0֋J=Y.ԧ΍8_KQk*X9[o*!+'b<$c(= n@ȮC+uo_n/HԛD$BTcR@xK4м|*O>W E9PՔQ9[iGXJPi(KT)\ԥ9_)J&Z)kC[`2Ή_yH)N\>4^SC 56Ucwك& o8hs/2D.(QۃFvm &m&8Z񗂬 \@ԇ ע*muZi B5Xf˺eFFoBX ^?jtBP'ԆԤb$aښg/G˞B4ʞx?|;jXša g!k]ZftMp34bMd úoy 5A j-ew oS%pa\Ե'/=n\U8'k[`]^=d'*5SYԇ.N6M[v"O^@͜FU1yqkm}m1Qu~1̭Xe'8 xuX Sy}d*5X?}s9YMPTvrQ5Ձ܋^Vapvc:Rv\lRCᬧ|l+X;. ;&tzmhQ6QpzjE7ewNYuU5`%(BOsj?qs Q\yj:5\Fk!{V#QOk}@h].| VĢ:֡~ݨՑy DIYsej;'!ol*V%ߛ4^<͹X?X͹y.&z ?ghD܈IUܓwPk+Ʊ U9KF9 e\;@-T:.m:/՜KUQ)R}Gkm*oEm*kIKo2}#4*D-J^j&{'?<5i^]v e}X-vAod![M&ƺn|݂>'4ƹOT|}~*AD.} I-\T%A5R(^Kxq-ؘ$ոޏ=-ciN $QثÐL]'?QUr@bmjH[a8AƁ6~E x%j%TO8yVs.T>\CWT?qX/V 0UX)z)Da}Jo:"QK|E..D1/1j~/c5q|\G['j^Z±Wa՘We'|Gϐa kw/㼧OQ,-;!lVt«W,p-2L}ycMj =P-2u#ڲB^> ]4Rt@Cw"Uڜ`# 렮S'5"꒙qv5K}x%܇UjIQ=m]oAD5PkYE| 5qWE֖ U>W`!CݶƠߥAjpc9\94qwsC-3ze-O1r?@8 Z <ߠl$.] !4J,W"PBlB?]!,"0qu8k[87ڲB8fC)4@Bf>܃VGA5Ud7j۶I-B16Ew"*Rd&ΪZȞBQ],NG5Ρh"$E2 '5Or8묳2dRF˗3uT bEJ;%Xv Сw}Y =sNF_쇮iJ"O&c=Fڵ5trJJJꫯ},j1|Rܨ|ڶmOem*Dx5kÆ L9 ݄ cqxGw|>sLZrUo+[w&:qڟCfh<3fش#nxu#{r\Qt3INgǷևWMnlo1ܹFU3Gǎٶmqh;j<}YY?9P*7qfΜ7LVVm 5bkw΂ S%BT#I3P)KPWV\jSpU<讍_~z^h"5㨤4n"U;M1cW<@lj33>|4xIqav/:^:p#(WWY\\&Mk55Cm󴾬L.R7s(>>/φ FӦM4h999ᗁJPkNcQs$Vv1Z@e|-B g<P=V&8}qu4Gva2z=/v'kD[!}sf\f[Toy7Z_5[ >}ݏ811sVЧOf͚m6Z/{ JgSn}iErUT^Xk 64^1[#Aw`dYdMD\f9ȎX{bNf6f5hZ^ɱwgyrx"beZm{&r/ӿ^/yBQui\`̝;/S A1n8^|\ 1qhSQ=ztQ&7K\+7/b m uENCgk ow>e_a79777Ōu!$l вl όC޴KIIa޼yj驧b̙lݺ88j)T1XC7R:vs=9M!BDGnc5t-.kh;H2K޾fΡ1Ejgx HeoSizhsz#*ph}D5}K"kR3nzhbNg%66j:թS׈ &&G}~p=QƁ;֭eQ]jIt IDATQZ+]?;Kcch;)>oYЉ:ӠP9?{žkwo0Ì[޶1.V^f\gϛqLJwId"PZhO?7߄FURs=G+?9r$O<漎D ޷73jԨc@>p6j y%Э46_)_ I|z8{K$k3nzk=gHlޅ>ZOPPt˝ v̛Jfr"bjaܸq7Nw5ZDD(تqEDD0h ) b.ƹ hfbUs3R[>9 }dmZfM/غ͸ s߽cɭzP},_Q#FXtQ+,رcXWFobq^+X}J͕ ͟\5{[bD2j9͌K Hc݀>C[f>ckݥזCmwT!BS۶mi۶PP rGH׮UN - ȶũxW[ogs5U9cwt-.sNd Y3Z {wtgoYeV!U׷o_GA6[h܌e.T/&p AmbO>響`1D>_gn}jh6bXe, &c{fܺ*xKJ{AMe&YTlBhjlq,p->,L*suE/gfΞkqQ? EiH7:c˿̱M>h<+͸;U8!jpƶqbbm.JUw0жc{w\[^- 5VW>ڸ5FRs2VqyɌcSاABTMjj16sNA=56m g|%S~W)qˡ;z%gU QkaPtRnx{WD1ז3Ga~3nqݸ#OW1ܨڿQPR.SHrxx/8uQ-D'!Q;3>{"G.~9sBT]qq"7}1BiL!B7a[< k`3jFC? e$Zy1uԶ8{oɸܴں{=: q=նWYB=t &K|f\ !.++a7B'Q<_Pu/|g].#k}I"!>k^oHA7d!Dlܸnlo'6lvNBr[PdB!§j[Fӷj[~g/'ph5Ash۷j@;.;v !dR߁7M`$Dh-u%V)"ѷ%|g?x -g?3}*i}.3cߺXZVBTlɒ%p t}f͚ %DZدn6[ۺ[ou;?ͺvΝ|b ڶ{_BoÆ mbP "8*{e QCvvvA⎌V72@ˡ{FHu6c߽cRX#})!DMfQpGO!k wukdY*GRS3Θ.yqtr= Ìl_͞_f9Uq#(>Yw qe=qs-&LN:!s:ujIW3>~kJV… d-7A+A-s2>n5"鳟s|Œg?OQI#Yg]aKֆf쎊̸0;Mq<DZ=qж6mW^!333dn~;o_MP}lݺ~ L\H.Q3}FC?l%ҠUf|`dYdƮ(Z^q{rc[hy}D%Ϟ[bUBR[Y뼢XgO&藺r]w+N%(rssyꩧVa@f O/;6Xy ZԟZ.jf!pf@KDZWUӽ7x5{N=ڝ sOd,x4|'[f#;ְ,%fOmVd_ϟ^ k…̞4UNTQ 2n8<Ǐ~amܸql߾~QlW^%%%7~( V@Uhz?ެO?߶Z|n?}Dik}Q./^8/Ŷ1)c8b뜗ZurW?擹a\Q{rkuc6Рz⎡'n=$3'|oً4c/YL8GyD_F_s* [gIT7,8 z!^Z'.F>)|Tl%\NOy9uD%Ǽ\x|TQ )xr.{}FnuBCy{S..%zXhk7#ѣz?)B~g41kZp8i08B./XRىCN0scT~qJ\ B$ TaPWpƠ^ܛpn- '6!A]ּ/qLo1TrnYNA t!B J*ը sqB!%FyZ% !"@"yXOB!D6"|!\>اf1 !P f6Yp~ jNB!Suv@=' g~=xA_*BN@wU:x*=5."z`0BIC_ -[w"~1|\5ȭBm򀎔PjSw"p] &I !( ;r Pe8*Ԫ2'C|mxYf|ߟЯ=dݫ5O?6mhAQLf/6 Z/2ߣMw޼DDoI묬,Nʌ3|u/CY rԎcƌg!!!|D6k֌;v0UoҬj+rD4~veM68U{\&.^+Wk׮'!ĉڽ{7ڵȑ#ơ_6 Q=Ɓ~1w\jժ/+?1cFѣ!/p1jISܨ)xҤI<L5>UI3c+ZF8[͸ERtDm OrBaÆ?Gy8Ϡ[TlܹsIJJҗ >:up3j=ynA}r`ܸqaWE˫Vqrt478u*,419c!UsM86{Ք9NYsa޼yժ16 8)ScMo߾=O?t@ 7cmm|KNԊ69|5݌7hHiAQ!NT||<&M3~Fե3ws!.;wp9wp6WZl|Mbcu B>ڰΌ[$%sIs^_'^u%Nr]X^F믿ҥKC=tc7x`f͚U#ڜ^{\PCOj}8=;uĀ.o Eե+.kO{vx5[Vmh B8.W_}D 4ΐ!CjLc Ƙ1c쇆m!B]; Zr:d嶭f|FFN=%%O3um3E!O?I&^Aʣ>JTTT{W^dz gk v~Zjs(sglkvLb {! &Nƫ[.?77͙սz"%Bwb}f|U4I3 fܮV C5jB!#F6@kA>8ڧO`椅kVqFuVv5G<wu;Į!O \:Ǎ8ҹs`tnHF5=c" YfU}@&t.;N !ujբCC}@]d@PM?kVRRzm9&"::- !'wo]@} ˞efEE3 8xiӎ#Binl;:aZ{8o3M96]P\츶\?.k۴E!zBS؆m%#73nh\S͎3w`x6F!Bu:nc?RT:[sی ;w8p!_Jw~2C7g?bۯ9aczZ}g[׍uTZ9Z!𗬬,{x ٿP t3kUGc GB!!33vszǎVZ&%sQf[+N2'ߊ]B!?^fbl׭[{jgt$&kZйɄoMk!dٶXfZaY|yP!9:ZRs6'm}w}B!NƎ;عsRA^fYhp􊖭hd[ER24oiƾ" !'j̙Xl<Ï?ȠA^pE݌ܕ@va!mX]]ҡ?0WIzD: !D8x^mbcciԨ46m=hCMjWƯ=Y雹y Z&%jhvxuJdm[ڲDҷo_6mڤ;"##㏹uTK.eٲeC^q`ƌddd/;tfx^5 mٚV 4;V;"\!*D5rH>cݩ#h<1-e3p9_d-> [yKZ''BJqB-KJJ[x"^/wq1c,<]woU.k`oM4\Ҽmkr*8!+Z6=BϾܧ1Ok)z;v,/r 3#GW_Tlrsɀ9n3f iii%4k$$!'SJ;ֵI2_硃vhHjt~Q2ʕ+ڵ~$!8n ,`C)\dj>3L0A_F~vZn,Yb?EuM5.&(:uDjj Enmt}%11"@֬Yî]жPs 01ߟX}\/^LIIpp&Qv(A5 0FE +<0_^B!h#B sN #[nAy'UA64.*7c' !]J4RO5B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B`rN@i@ ?X\8/T "'R[m Q97iN sN@HYIqzNBP;qBn:MI5%DOALFt?t%6 D#hrQXR, cF K,j$?FpGDdn+FԢ јAzFнn=5t!Bң@#h9~)+>Fɶ8 ()D )ryO˓@jUo6BmFPt8]c:"Dގwf'}a4#|oـ * r\g4CJmqBb\Z^~tP$Ni(PO&˔Eܡt8naFߨt0؞xm=v@-`j_I.0+[Q|> (/QZ0"&ߥ9!L|TOB`(0IȐusc+-ň6U"$]7&Jtrt۱s9z :1![$DתOA7kLG  ēHb'a|C[Ҙ'VԤDZ\z71ℬ}] 6G5:v&-e"$ a t!BV<^|R=t9u]s7ј'QYհq#*8]u@<4ȡ1lՌFw"Wp,Ҿ/u/!N\g"[}AMU3ߠ!=ɌTvrܴl8~nbŶşdrhj SeO. W݈u!0X25\v,l DCM3]gt8!j-)f?[R9%!s]JB54\A^Qt8nJ kLG 2A]{`oQ>ذӅI }ޣ111nQ@3osz* iC;|y#kLGaGٓau\gͷLAgU (7><ǫiC[ p3/bVN7&Bw4#87\tD`-.ob_PCʆy~6e^4G&,ܽ*l!B69/kEy^}}>pW%'1qsz ?A/wŕpјmfeiLGbQA5гPE;lǛ7zgێrzS[0~%K^A~>_1!N$`y;4#BT1p5vl(e6p:q.>ٷ b /uJzQ{\{1íbKJ&w9ՃЪ9|z\5)Qs!=!ԈBTo&~p7-nA-.јyotJuLP!"XYTYr 9|b[,DUIO; !N1'"[w"BƸT!IENDB`pairtools-0.3.0/doc/_static/read_pair_UR_MorN.png000066400000000000000000001005671345765554600217530ustar00rootroot00000000000000PNG  IHDRsBIT|d pHYs.$.$* tEXtSoftwarewww.inkscape.org< IDATxwx?ZTEXbWk?;zQ!`EQ JB893M#{vy3;̙s(`[B8 T3-"B82-B̟@W"2y8?ۏ(L ?* ж+J0L Ҿ"[O>KB&AuEL  5 zQ^BITAM k G AAAA" P\צ-j/JzL^ڊ8Q }SmaŁ|Vk959~Mhx}r22F}``v8!)ˀ_l@'?c )T3Nۓ dϢκ:mzd_ejY8=~{Ş?g{в+v/i2:mO1e6|q9uڝZm{|[Yi!I-]Ԏm [9ƵZ]S>-߱glhuhٿ'33{ 1Wy _fnȠCҤF"5omTKvAsskO<(˯)7`OZvtShYusD^x墰+EZ}| :$6.)g\e䉡C~l$:1z^*aCkԈj=XԨcV˓: ?]R6'>jԒwkWjׁ #KdFlׁ6-O-]bLKxU 7=\);yӠ!l8s޺ ʛVzg1eFVmӒ֙!Ӳⵛpi9Kђw +_š%iyCkDZ_-Bէu$m"jNݻxk #ZF[b[wGZfŚI\QѴuhi;9#JpZ>zR$ß528/8Q.tF5)([rh (UD#}h‡_9Y,0*)Vڏ|H([AN^I~!KKQ/Ӳ|(2Q1qAR ;_kiR ej-GbD9MmiVU+LasS2e~wJnjDK苳dT 2ՠ!,:-vN|ѭ߿Î_&Fl=v2N:aFT;L#ZϛĎh-NݎgAZqq<ҽ' *n{VW %-A.0}6l>-Bv;ڦzlڍz:e|0@Z޽˩eKidLˑkVs'˴_$>Ɍ5%36[dPYN,JOu/dFxGÇh .jqQx 0JZкEqݰ4 hQ]s_RMiYu'%pCZ~Nw4=zcZM)NCӾ#hEH :@hU !߁y\bkK&ۖQUR>86Ϟ ePu73Zy͡%ݺ-[y ZGzmNz1-BŸ=hP |,#+hiXͮe>yU(.zP",wp]#jUi9oZͿ~0vXZ7JͿ4e+^\KK?eNDZ4ߨ$BfB+OjSi-^۪xw̅ABٿ6}N>y`@̛ÁqZKKH9 CZ~cvÞh|֕f >g_mDˡ1[tMιֈ!rz^yxџC]Z/^hDKh2ףMiGsp|(#_%:z He9UCC 洼v9FH :@h+%Z"{VV~eج4S!{VYcbU}XMK燊 ǦeHmqsmgلQ 򵖫 BQ׃i6 -:l5h 'sq&qjgPR~S5\QΜڏڅ6-P~k'kXZ KPX7eQEkzJ+3Z>mZzJk#ZJLRPaٓMV~>-N?A'r1k=ikt-A#e,(ppm A|Ȳi?q&m<JOgG^4 ,qB?oy,ue[n|ZrZj$)SBSZ&&>@jj?zQqhyթ)-,xy"hɊ+q\:tk0Z..eW.uUŪMtiYjӒ*V9[#y0뢏?؋Ք9)aTT??bOvǟ]}?Z.ǟmF>on~=#Zv>ms\ n~fm?gwgDKYsIr$ôd@K<풒ؽh#l<|I4Qn3_ Lښe@iINy(5*i))׆6L*pUnGj3V(oL˦ϟeWήe9Gn\E:,ڳWCos`b6Rk-b ihRoZʹR}atQ)G:-%eq/^qL^>pٿg6x+8iX+?IڠzִM]76L{!)ִ3,0JiY;?yF(]WFK3(%gwn=GˮZ#zT~*zy`HE@Gm} Wy(i2/JYW)^F@/lMQ/xl ^IcfSݯY~K,ҒŲWiiw,zdٻ3Fp}[#J x|='x/ siܣݵDǛ\7'S{[X-tk^7ii?U<>,l-]KYLzEna.9Ԋ=I+,侅?Ǽ/` 2Z( aMAwԡfw oKx'u;w1--/ϲk>̌J@ KXcZZ\_ךIc~ fo`V_mqfolfچuV68+-A*zғʐf09k -B] k4%3Z*a_xWڿg6}9-JX̦ϟ3-iY ?{ƈAgŊ*=h\F |rA-AcynqLې!$qEEnsYbAN'"?@I[Wm?в)-+'Z{2K,fi|)-Gе\g<"f<;%ֈ_:{deHK(^G'K&c(dSJchT :䞗rV{/oF49g]|{64>j1%,ڌk^aϟ_r!MzjoUv9ˈA[#u0FLT/ۮ(]4fRBkF?r#n#TҒKxʖQn8A}t 3[m- ,/)?-|rm_WhA9(j!XAȠCx` :BC5 F$KTLv   DeFޝ_@H.`:!r À+yuO<\ t(jO 56@[5+^uBeyX܄J^@QO{j֠Nc+Ы,`iɫ ~kU@,x{ .. i* QlBq<́4r㼥|{{1:vnչlfRT|>,!T yW0 * B} 8GH{&E)+c8Ө|c~Y9P&5Pu|&[)q{PWIawLLU9xwQÞ? Tr "SP TE?!)-B x/|=lLH^F3rC Bw1eG sU*<꣸Q^ar8} ߴCŹQO.ͤ Gtغ끢 @|>sQa*v p j0Bģ @=QS%:G-{3Šz7*@ 9.GEy& z!@b@_?C='nEna-UY޻}UޅQoI-tg&}Pnñ?ϯyP7P>m4 4!h.I'' Zv<*3c;Št`56mTQ1=Ix9_Р10㉎_caPυ3kY9U(lǢV)B`~P'y}|z-{L >_5ʒ*7 gz%Ȃ7CqFe-%},?5cF H@qz}@t1=pu P^PQz%yPMFme]~nhLq7(rHt,+G2* %!!t@<_(CQO >g2*$y.Gn"&v.Fͪ{W}="7 go |epfX^uQDqvnb&:Gk Ӆ HSនA!xLawp)U,Ti0=\rž7^eP݅L~*2+'"{QU!X G{BT)QaUXаpqVZ { OFy%Bb!3J~}}&ߥS/gzoP9S6\1@B㦅U|ĩju [$N]l2C&e(o,16Tm_B$YQA؇PvJX_zn V1Q717eD_}o_9jTz%Rzϸʔ8 BIDfra=/jGOP R {J IDAT݃J?!ymQ'Pչ |"P9% ByvaƏۋWAÿ y~.Ŀnq=m #evi!BL(1#~!*@=C(CU BIڋ: #4L`*|I BhZ@=tt}_C Dzxx5xsz*jA&vY9Bf3Z\mP8@.E#Us("UI?lO>pQ5BqaP8T1(f D󘇊ǹJۀǰ ϠrԽ<soAf#F& ]BE/PQM ޅ(syucN s6Q(?uYHi2ÝA'Vg3Pj P>Yv\@7j?P9c /TboY#;pc""tJ?*)&BUfܿͰPb!!p.ԀAp.(~ݫh:D=痢2Bp I&AE?C]ɔ(Ϗ32_]|:GpJn4OcSBԍd:o6c"T-R|u q"D`%:rB~kØ+mk(Uk`EjgP8xL M#|GU\<ˆ4OhbNy?l/]~9U| oxKSQNFD;}\ H p=0DzΨB\A &ycqhj<4ad/.k8~&^BFkwdTZwrBE9SQ!hmމpmqjo?R ű9Xf.tc?:IB¼  77 юTX_3}(n6G'&i4GDŽD8ˀѩn|WixP5MQ}f!=B[uL=B`4UÞZ` f05 B]UU=7`k'*AlX)@{;׹: ۷Gz^[P30ڄq{t@Mͣd?xp I۶(=۶S5{ap.WVGpI꣏6G' -exo$T*BX.Ӭ=@?hc#.ůǰH"T1׎//P)2Ryʰ`ɟ'R gؾ7p6 ¾Y* BF}:25  i$Ľ n{:" S#u`ҝ(c`!e|.h[} %GfxNB=xޯը4Ƞ_D~X-o3_#L$pF(yLV"m w<i&*pހנQ &onC>6%XDG@N2,W24_OiAމC NogTD pFT @]lU)P]QpQN?OhAዚ VFgq!x\Koqa`Bߧ@_r'l5 t@y]-,{!;5M B]rTS_o&.8 'pB7^ޗ:F˛qw)m~Yd8Š|S1,!4HDNDpx^ ATŴ `O°1Ay|K#E LBc[ M!55a'oOaTC !0Q lbUEF_ÍxTNY9B9H. AbM QkRx_cy!;@x"jyf8|z,is*d%_ٺ51;{4u|$JɅ;GfABZ k)~}ը""\E+J!45૟y!A%zTe8z_N";g>+ h.*~N 5Paj !i,O TE`ف yp5Y)TP7'TAUoE* -LL0#ǙP п+\LsAcѿ kJ?q_# hBpy؄ﴉ`WR9m$H1+RDBeʐ{&Zom=AW^o.0śdT=cL丩FL A& } Ƭ4+G(;@r"(ՙJ!ޯUJz O!}@x;֢&BGvj1×.6湨xȩǠj/Y( ͓*MQ:VU BuPK?2"HA<Mb3f.WT"'JBƒT=PnвQ i Ϭcb.J!CI>jSy仺5Hcv k5i"y jSx aGr"Ih؅g BXQm8X@x'!>nX  k)8<ńvK!' %4#\•(}s0 P6 k(#'jwnTu/P̃|,_G̕>t@<{Sn>G56SMAeg4| C}Ь@99?bZD.̞Gn MzrY9B^'D&i1pa-e:;½B34Pa[^cCLg`&\%2^Y8:nP"eGAГK {(jۍ*bJxX_yhP~N AjVNDs۠;6RvX)8/g聧ヰPa_USfA(a-1 ]ea-1*lOQ k G\(S}lEM @NBs"p'R = ;5ѥĶPBQ_0Zߥ ڏbVuW- w| LA%MEIi%`  ;Fxu kp t5Bw VLu|ݨ6Gj@oy|@nN BT7-Rfhd<_.yh%{PoN cѰpi)5Z4Q~бj?nw(:}#@0{c_+PcCU!y}-jXKAA{,B\5ĠJ?,dWA1?[G߰w`Xr-gʑ)~E ukO0Yi GąYHy^=llZAjzިȡ!*:sȭU-rg!h*MgL@.1\h0N+wofofOj7Ѩh Py}Ҡ^I֡|;H_xP^TdQ$ BXʅ +v 9.$wdVN@h ܈M+1u GָQQ#~ z LՋXA <-yڔ8A67AA5 t].B x֠4C9Pe2 80YDf6ZRTD\X ; 2-: Q!9.T|adPЂDy4o7jCOF}G_>74uU*"ʉ"IUdˠ~QP@ B$*]'9RU"ײO!B֨aHږ B8=8qAgeXڠCc TUr(/P4T]L;uh࿨Hj ~!"J!U"BĻ(ϒ̒V(i T/TB A10Դ8 UW(jU";EB<*\(bk7 39?ٻAQ);;hժk֬)w*ȑ#y |`wO,Y^zo*PzhѢ3g$%%|3}t>vm_+cS29<#rʸP ,-eWT "?e0TZhQsU+xsECYgf ֠CE(~^zYYp9E-7Itghkڦ s05J2y#> ]G?Ԍ>fƏoNhڴ)?ǀ%߰OVI9e,=ђش=ɧ 7O>IVTK4Sɰ؀C֭;wnpиqcfϞM.]o_ \꽮} 6qy/D I@[{=BNȴ`zp]^ E& } j;u;iyV@lz4;8H2q3,)ZV;n6=j'6HrK*q뭷Ҽys[`8o*ъ?~<<9Ew<#)!9J<*.F|w޿ ABS.P@h҆u,V$'@zL7kd̈́SsIjVǬvlz4;`çO8I闎JL7ll=zB*ڶmܹsIMM5*@Ԯ]3fPn]/e yrhذ!/BE !N?TaOOQՇ= i@7`|/N~F7| @G@4D}?OJ :8&!c˷~6Gwn-ܦܼ|lwOR;]0/ q\47ʱ Q%{z]vyޚ.#PС}%T&NȐ!C@6*0NYiAdoi\[,0c'6mOS8Q,m;˷| 堻ia|mڰEn!Ig2p\"zөS'n&ZA(waHM_iHسx#m"uTkj&XfH>ec<Bbh9ș^*VZ5QS^րCN􀃇ӫW/[aK% :Xz)))\y˞tZڳёuR< #8{#@a+./y6a^8+mb#m"6Fw$7ky3?<#.gy*r/r뭷4Zjŵ^k25EW3DEI4 2&LضeMxA饧M^NmӾדPWNǎޥҹsg;4hPUvd#qr_ F\ \ GE0,FؖEU() #;?#f"hl-GDKn/TwHj7v[kS¼l6|K7iDj+ń;Ü9s^9\.FqLzg?ZW/t膚P( <@TD=" FUX,<Ƭ'Q4 Eّ6OK79&;&_.{DiG8CM!OF hӦ ݻw?ٳ+A@c"ߟjL P?E]XĢ_6a'5@bi㜑饛H^]堻tŻ`>ĸ& 5G )%~SP}B\IH]|5 v~#;VVZgvי4A.C !>ѭ9 'MAS a3*Ž' #\i]P4hKrQ.vMiTDYiOwMwMΖ9oYZi9AȲ`7_=i+PY+`JR,mEXw:&{ (6wuN ?LanEeX*@ǎILLճuJcqh۶mi `=vM^Tv6^GġgVѩC9A щm]ҺB8 4!D(-6Ѭ Jhb}Mt]'EWW?Ih DEa% 5DJ {`]GJ'|`؞|\U#0mGߧsx]fD.4l˩q,'ԠHQFf >0`7opt6j9NwDnPu7&I&bO*6O9/# A(;IQ(%qt`T @>0k9@C[; #ys iQ QqըR_]?"gZp`oawGT ;i[جcv/\bԩS4#Wz b™:htPGD֎Zj'?xSf1LqRṈ|ߓ{pNy Q%BRRRPu P2PVuQ&v:ZTY l{l8U~.e˷o8;11%͵ gL4CvtB"MzjٺKq [Cc"33<\zB> 8ɧ!f=7q[?] ߑmހԕ0Mr#Gy'8?ôrۼI #GxXCYYY+Bqƙ>0[ ū8zGqӳ`:]0m>'vQebW:5i ahuVDAGA{ĺ0/j';z'k4#[V"B9p,'}AMbդgqvXbTt,O9 j4ic7}yvƭI9Jo[6jbbIr;/_HoxZ;v^A8gApQpo]%2 w[+wHL4{&4?n0LjwA_qxINf;߽G1) Bg{S^vYz $KIl#27}#ݡZf4>}`o]]QѪEYs}Vbuq[zzQÙV{ptzA^}(tim޼{Ap2]\sVg`}>P;8!TTfɶK,jP"@$6HrOPQ ] mm.dq4F fvƬ7JVƞų+!믿Mck.rrrJ[WShuΈϞu"2SӦ=zz[>UV;zm_xھm>p#m轸cp~rY,]jϳg7Eݲe˂I'֡ %g6N8NJޙ]c™RfyRy0Vw0i#m}_#* cCfʙ 41!{ee$N;4^ڛ;) O|=kw61gJr&Є$&>t3 Ps?ڟ?eΛ4:mUc:V}͆ 4:upn闎e.0G~#''ĆiRS/+VZgv]|ߦ+B萑ѣG^9\.^~eFmZJP0a |XUkҤI9`jpR97hvM:iǰw-ljw~lE@4:u}hyBd|䞗X4-_Ntȧ1eW\6D>rH$(gA;f{ Ôav0j*t wHĎѣG̀~wy'SLyϤIAY?P"fōy|wJ$:Rd 0-g>/x{r{Z12WB0$v,\ӧ׼c͛7STTv*Q3qDk58vXg-FMe #iE>Y8%Ipvxo|;0,VS˳s5Ӥ[;>jŎ /b7Z|Mt{B'1cưwsTGʥugɒ%L6\D&II%4ٛAZv[;^?k|pcleW) L5ecwa96 ?}ɟ}>nqW烾g]O?1lذg'm#-(¬Xai B^O&BfL.MxUzyhko7ʳc baeA9!a[j.z0..ޮ"#Gd„ cƼyBguӢW@9[Vq\Zw_XǓҐNoǥvenb+G0{A1c5-:*Ð1k,,:s> 8+˹3j+ txGCYf?!IJrl]˯)0p-e4D>4hJk>?3QaT Ȇa^kcOJà&w5YfJ3ǭ?1KvsѣGc Vk H⟫E]:DDbL+ \Bě?x1xFfhEq,cЬ aa"+--eԨQM[ {8ǥۯAo߾0g.\йy !8z1beeeH=)e\6B^P༽Jl"?e?̄ աW饢8[S/%uyyoMd&Ԧ-iuZ` =ĞEڏ{ˊpQ`ࢬ`p$7 |־1}͎HV~? E[VPQ8?Uf1kØ1cx(-[ƹˮ]Abއ蚋~%yyye$RyKj<4H99i)>`//hFY^Yג֬9;t$2+UV1+d&6g};7=Bξff .sΉU2|p^z)#y阵 |-77}f̘sirk$cɇyfps wر? #FԘC=zu5 mL81ojw˹;ϑ#Gy_zzzohY34n7~x؈$M6~G}ؽ{iӦwzAmB>}v/^0`@1`u&Mxb)UVVz7Ք^RRz ҨQj9x %%%<㍢B**N| S<ktgC!Q |@˖-v\o***شiiDT6mj<ד(** ]l܋M1߫O6dggӴiӪ^/7n ݼ3=A^1'""$(* t;&$+x=weLDDjmrOn'!ˁ/AA0mo4_DD$W8ׅ /0 ״ `p\DDʀ\`ۉԠ1fVn'F0+UT;A@%rԩ29Bmј?08DD"`If{Zc,i13;[ajpq@#̕)7afR4YvG(N$֜M uxk--"""$̚J>`jiìԱs1"E_5`pB].aߚ\=VDDD>0^&LDDD⏵ h>Sz]uyq9\EDDD$\ 0a˹K1kE}Ҩ]cNzss3R񙈈HD9{^znFEDDDB%P>l# |F=DDDֱ@)`G=J1KnD G9@F8bH=Sr9{I'.&0*wt| ^Sњ";*)ƺ$w@s 2]Nz`VlWK#>&LO1x$P.fšD ̠tA`, |Zv5H~v"aw+]ENJ0ED$NomX+^N :xKDD\ D v"RxxD 8DDDή/.동D"d:fj! x ɹжm[RScP^^бcG]ADJ&M<4FkCzz:[GرC*~h޼9ii=s޽߿5kFfz,"`ΝY& eTӀ&ֆۓ^FuTPP={B7\a T5e&M4aĉ 8+R0ԟ.u5)c' U8W:^XkWP1ω l0ݞ j>`Fkψ>'}̺%/"RCeԩ?6Y@  dX^&O\uU 8?ŋ[  Uq/z439 ɓ]L'z].$%1<7x`f0{OA駟UW]̙3M7bf;r/+fX IDATw/jٲ%f͢)}u%:'3o {8II |0a  oq"7ZnͨQ`m:XbJAڷoLnN%ƌÇ~ȢEMahV, ?ٸqc׿&զ|g7wlK:vKˌr\ngryN("R&Lwq~唂8&RS)xie$׃+#"X5jĄ N#HΝܹ\U=s8쳝~V.̵Wjذ!wY?zLвB`ǧjͩ-[E5Gp=z4-[t; &McqHIIGun @yũ@o=jɹiul*r0.ݍr"zv9ӈaÆ駟Za71?U?C`ugsWX^VM\R6%TKx< ;wTs ,>Smv*dffr)F :￟;wZ>εfggsgF;;\ĐeS:wM5E$q%%%qI'F7x`5kƾ}MCѠC$Z!C5q-tfHW;w]I!WvBƙī^zѫVqS :t(SL6] a}Nye_W. :ߝ\6m|;p0sWhED$񤥥q饗:7V.IK+$BXuDye%-юqq3 JDDׯkgk :5p  xoclӖ>?|gnD߀AW]J>g9hтvڹKTT61k:6,rK^4NIj""Gzziͩ T)S. *}ಉְ3Wc :K9֝={V[brj&vjs$rS<@k ۷p.;Kfפ)ED$ٓTGb[r\~S^Ys˫/xi2{Ѫ\""F}u<@3͛G5h*C& K)}t0x<nRd[wZH_ | 0S֞ekצv|vv"תUЊ-<@C疆 „20e Mv|nc{Lˆ{45MF#W6qQ},]L4Փ|HyH8x*4h@Z'~7dd 8Hdf-O">IչT6ѿEˠeeo/JƍiED~r=@sKIIIT{`grQt]FU+)(GDD$~kZ!]I[yu7IM嚮ҵ|uKTs!';p&.YY\ұz"""8">Is$>'WJ^^X4hGFe̞_#% DDDp;[mՄh3hݖ4xsڠL"""uzٳgsӾ:.l߾<"&M9[lfOYZr2<ގw;0a7xYGҿJOã_ݝۇwQ^Yc%L9}: /4V.vf@ċrLu;\vQ;^xݭ\R3`0y]M(lӖ-Zp.f[E:3 M”V2 V""u`/_vpuבv*Qz1cst|sq]wxHdf4N]tnn.ܱn0zឯ VpOᆴ,"r^|EFvdM 7v*Qx駝V/:AK_0p@Sspg2Wwƿ6mdK'e٦IĿseԌYf1{l6lv*Qk99gڵknރa%;z7< #/ CD$߿?OnrM71m4Sz :7=V.,KgN2TSfw̱/o݁)wa+-׬BD$ތ7.}.ժVyy9cǎunZi46 Nqq/ {z7l-*bֆuA KG CD$< zo^0oV^x1߹i#PF\Uw=gΜzsqǓ( G ۴g-ZײDDyg\٣ |C ߫z)֭ =C0 ֮]ѣ.CMVcD]FWtbłۣZn=tAC3D>;3SFǏwu65$2, n6]L':RS[;+,ݐw&ٿ)HE$q5R+?]uos墋.ip[ƌܴx B6&Ld~1=}IL[܄w{a<R!"xЃq,4v(30|pƍnV2n887R:`lڴ . gBs:efK+_bHNv<vk'""믿fM/av&``O9(H2M6qWR^n/\ _C*[p,-5zhFxKayy,$6m䇟؏{}>~8EEL_:5+*DD4|̙ c?8?#<ef^1c#87܀`,Qw#+9sYf)E^rRwゲ2^Y2h[z Zee""ƺ` [U(o( ֭[رc?ss1p!YuM13MN6s3tP:t@j/=n8&Oj)4gnf禇Xr s̱ߟ9s搑^VuT\\+¸qB''_Bkj[@zس7<Lt;*\Y9ו*.q]\9 DD>{b`l鈙*QxJDDb*`ILga%$DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD\vR'큖<(R."G# $WN`Kv@vkى3<ɽDDDDD_2rb_g.ئ-}9#&J0Yw@"p ~׽'SĕA:1}Uk`'_)-ogOIONJǻH] ڟYZ~]8͕,D[Дv\1ǽlDc@tR%.m”Z. k!}IN'r23]LGRVִ5Ǟ{Jx+Y$8_Wo5ʖx Zt!Y]Or1:YTcm_6F pq=\NGYAΥIUc0W;kJD$AFZͲ9MjvI1YHJn@eװH丟ԠCǔGvZCL`dۃ^V"1FW8BD&hJ{Qě3KYQ :{j$7oؐtfw :]1$OIѤסcVF#)z;pݶ([K4hFْ';xQ :Ħ{]0Y_$̎>L9D#/5fÁ"HbOIop[O5ʖspR7s1xp~ Nj"6}[6\LGY)ڲtDijNìe986q5ĜTq+HkښvtD$k#stMkpW?׫fl)<[Dsʺ]Yc}&P"zHP$A%a}Ohj-\veK u 1_:snt]V0mrH&YAaH SjT#*鸽9;u?7ht$J C1-tY>p9p~X_U܆bL%?T 3Xiz};][mZ;Ar0gw5E(g_i)/,_b:"Ge4 ־19b+y"V5:M5Ӭ?v鶭.#Rkϼ%X71w3X 9xny%M/8?M"ohN ,ڳtDjm/0 w.# ͏f) $9E@.DŽGo$ q1˫]$|iLEaI# 8/wsO$xiabC  +CDbk8m5 /r1SŎ|sgq{pux t}f--/+Q/c1oz!;_ɑ/|V´4sE2kw1vp׮v/s+`Nh|߇y+Hk\,Ͻae=X~cYغРC|xq?ՒW~ҽ{ӑ5qj8⿆5#="0h")e+xtDj-NJsE[WeF^|UN])ߘՠC$Si_^ԇW(XH ZǬy}0ݕ?۶Ʃ~ p7 .^Ll/@ye%O-4Ob_l|o*%7ęyَ0t0Dc#f%0sZ5f]U浇URJ`$aL}+ ocfZXi`A$|1XO{hI\ UTdt7#'b,#1\Ĝ˗xL]_Pe2`_~y .^`mYɶO^u1Aَm/1ŒS]hH8=lg-aYq[W" ՘խLq.f Mq8./AbzJ^&/ Wvyl6==]LGbP>Is@=PLMŜ3k6qscbt? X7w2oI\)Ü\LC\f$jfVALbfhr(fHktbnokc㹷| ?Ӄ?ؼor1Z+Q~WV sfwӧjcI a;,~ #Ӏ41̏/+s1Zl{~ƞ>p1/3'~G˛]W} El{ 0V԰ǹI٠930tsL6ΙY>6dI6fJ5hk1ח Or3Aqp\߂! wJV%X3H|Zw x~o_3lRwaj߇)C'4oi5ͨׄ$w dРěk3AŃYR<kgA78ɹ> :.IDATH I=MDD$LXm{)nMhAEL_"YiA%Ma6F=#iߤ)⪼ \DFk Nj#"""""" `UτLS3"""""".ѽ :H|Vb נ[DB IENDB`pairtools-0.3.0/doc/_static/read_pair_UR_criteria.png000066400000000000000000001276171345765554600227070ustar00rootroot00000000000000PNG  IHDRuuXWsBIT|d pHYs.$.$* tEXtSoftwarewww.inkscape.org< IDATxyxTdOB6 " "Ƣ"(j[mnhEڪU[Z*(.("" !=3qrΜ̙Lu23'3PHy~CK.@#>0A_ \0p1$"""qw q0A^ EDD`nE ""D==~ .""" ~o߾\yt҅$%rlЁ$k;!$7x`C|s=wӷl7xOrM<;a +VOЯ1hs7pY$&&2eڵkވӧ :2G0VB{Yiӆ)SpAZӣ Kz7FXW_/ۘbSXXx׬Y '`$ume Ne;O[u3(*v|ǚ'=lz\~xՏ=HHaM/[笟W6 ~?{Nj?uS'[usߧl7h_p!#Fh=I~ իC{^` 99_c[Z3fp5װg. IDDDZ8 """Q/pYddd2tP$Y)@F}kOAVbV{rڲb/Uu>c~볎}­V`OǤߓz|'ۄ[InɱtU|Xum󞷍]_b yҌ|>=G6n9E|MƌsoiY."ϟϨQGLDDDݑ,; uZZ 70ueVcҝ~׊fUw:'f.~ƪ N Y7>;g坿QUͪO_bJߜ4٨Q ~1M1ZƬYb*7 8ӧY."""ri/"""W'ŭ]HN׋3up16:s#7ĆoůZu=̪lnu"1slx!UǤw.}߭.@bV]]RȖ^{҅MslLœ9sHyfάYbmYg-ƒ>h L^roT"""ڋ6ݻ7;&`4|v*. CɌ(-N&0v'mmǷ}Vқ3o%y.&IVd[ubt::kj*w*}?)O>iQD؛;LfϞͰa?{6m֭-CDDDĢ%y"""2vILLtq8".ζڃ17̤9NZv7דɪK moһ"%'n꽡7 t&kl~iG7!٪[ɮ3݈:oN=CfKwm۶̛7U s=C-KL{"##I&9w ,6ײz p&6:bkjOlGFcҝEξ>.5h{g[UBC/ {!J׋,O_NN?0/C?Og$d[u-|n^71Mo?A;]VKh'zA޽ZGS9* EDDZ>iiitͱDz𭭎.ե3h\U@n_]7Bmor:fsζ7M_Xu_kՕ6} VTiiEl{[^DDuh~ѥK}طx lI}\mtS0p`ۛKnty_)-7>ѪK^l{s`lU'an}jx}"rTD/"""[;ۻ98Vo78rBWԵ pM ~l{k3Ug7=*?oվ4 .56xǤmoJZĞ[u&qM|g"OAALEHb~8Dh V Tmt>i!ho{ۅqco{s R+>Mz/xѪA#::>5ql;irss88DDD$z)iݬi;Ouζ 9A=fQwU\ GۛMi~Iڮ`ϦYuBz.j%79߹ *vw=M9BhTfhM` :&gX?ݪ;7ZuBzϻo^CWcҝ1>jڟw{=Iٝzk_]Ѵ7& )(iݬ[n=y",QD V էJ\j&ª+v|ˎ^vc6:ZֿNc%9Uox} ueV֥FL]f7KD Zuui![fLYvTiڋn/oNYrvlufqRQ?$C/#ԖXuJ^)^5FO YumYmt9FX7ucFI{Uo4nr%"""""""`UV9*`.oBlT0c]V]W^8%7e dx6Yΰmʢ V&̪o1gj`F޲eC'"""B{[`Y,XDtf< i۾L\ؠujO\<'nյely҂mtDr""""QLe~b VXXD!ζ7iX`'XvytyU|=[lSMo=xP 诳chL6,\omb|W.w&tmxw}$WUUŋ/h?Fp/"""^DDD^iO>aNana8+9`#`ܯHHϵuS#PWcՙ}N%{hޱߔoڪR2(+?57h?88G$n^&3 7㏻= oc7on?MT/K/Ě5k\K^ |aG"0%QpѯrfqNIw:講zg^Ijzӌǩooӓg^i{>l㋣ewXu]y)ёp뭷RUU0CqM7 JؕCMs]Dv( kE>ذVƏ}*p~uOĎ^b&N¦ Kɹ]0lkP<Bmt>6:{Z_k +_?ao$mO8=_,`h$R: ŋ+Le <xG8(**"""rLڋ;.]7̣>D"&=o#wKIwj *gӛա66KߤfNO~gX33 'ij)^.;>/Of_KF߱}t}/3M?9`0o[L2 0֬d/~:&~SIFEDD0GDDDL/ΟgnfCϰh{oˀYpZu{!wyVc46j_R*]/UW̶xl3mg~ 6Nt.r!{V/hfK/tRp?Xm?gy뮻.~ \pDDDi """,O@{:`恫-=[ee%wq=}C0DDD%Ph/""" Q_~ٳ' J3k֬O $ƍwsǜ9s"4"w9*ر ] ץ] <0|p:uވ({na?p5;D nDDDl)0x= 2' -{͡K!<7$w#""(L=؅9x 4+"""rD4^DDD\ GDD—=n=&a큈} B㐃2W@in@H*8 F nG>8#@i:4 M EDDDDDDDDB{j M uޤ^DDDDDDDD(X`oRp{{#^ґ&uIȑQh/Qh{8ޤ^DDDDDDDDq %k`oRp^؛܋x 1F~ 96iz`8kMCDDDDDDho5ʹf74^DDZ53DZka퉰񈈈N|k ?5E=H w5@b%"A3Ss""r-^ȉV[53c9{`=h Y_iƽHL{qU8{;͸opvq/""-[aƽl"͸h0oXϸ?H`o"QN&܁I w`oRp/""-[ZCpޤ^DD{917؛܋D1H&G'RID#npID# m\pIHRh/ޤD:7)ht4-I`oRp/""n;=1e`oRp/KD؛[IDcnh{{q˱m1&"QFDہI؛܋H4huK,؛܋H5%=G ؛܋D؛;EK`oRp/""nj떖7%7)Hi\-.oޤ^$J(ޤmI9[ZbpI[sl1}3&"Q@UGk`oRp/""떖7g`oRp/""Q܇)7)qB{ hM5ޤ^DD"!--!G`oRp/""-56s`oRp/"-%7ྥ&""Nn>I4H׊>BIKn@bOK <#?Ln--@YDDZH]x#c8_GDDbߗ0!+טi[ G(3Y\$KAj WQ-)7y 1 Ė:5.E{S}p?lGDD$6UA7Gqybb}`G5qRE"O4>Pt{,G+!Τ~PCw """>Fpɷ|8 H XtF5ED$0?}oW1_3KHZhQ8>zܯo/gPXDD$68w{ܻ0~|"""V@J<-RF{"B{ +S`/""Ҹ+X{K{(9r+X{MDS`/""rb5W`/"",ւ{"^"F}(9v+ V{" %܇{^&-=W`/"&q GHi{iZjp^Dڋ+7?"""ͯ ED5ki{iHF}Q`/"">--W`/""r{"r0G{Ӂˀ  JtJߙ&nh@C(+h qCZ| 3n% nK IDATk]4+ ^Jch)w"T*0 Ht{ ǠXP ޥoJ`i%aVH9큈HD{p^DD@+98]Kcn$'m\xmrzboŎo)rU?伮V, Ltǝj{ ;Πv/. N#dؿy1aI֯(k۶MM=?9ZgC~pym۶̝;!C7fRTTC̙c?L<9Z~؋:q2"`%v,Y$ kh1s{ h#G Awr< Xvp^H@#x 큄zKuB;a tO3<mڐ:jT Ք~{/)!;UmhUg<[u%ӭ:>=ܡo}V`xv ImCJZdm! ٻ ^`Κ4j(ƍe64_"&x 8нQ5|f͚5\ÿ/pY`{#ka|i0dpŸg 쥉?v{ 1cVzif]1RIcȱH"ܛ?^Fl7vKWqn$L 1`'Clf{ァ^"NY߶h0%!hL'{\*B+]˷Kϭ:ϩ_]ɮOfYuBzYǟx% IZS7Am<}-mʔ)̞=Z0;*1]fg޼yp * ^/?<%%%'?i;4YBNNqqZ/-KEEu3'$$5Cuu5{ zc\J`ߵk%ٵk}=qvn6~ر#MlG馃`F+h0sF͸qh.BL<իWEMSc:$cMx 8/ofztTWxJx}70|< 0;vҲc>-4h{/w^xf8䝋90i(NJ8kaH82_ӛ3'a#W^?K~OgcsuI!7A/xZo7{]f+W$++9s&^xYaսlKq϶;`wyn͛k1nF0 [`߳gOyN?tFu}Ph?`p']C2q&&Ob1_M@F^y1b_| _ PQadcp{aÈ푸I>{ByK_>h3klxa.ZP]UՏN| qmxT|k3JWϵ}0%_-'L@B‘I_s5ިJ&0 zLRwF%\ IAA|AK.SN9~f[&Z= X?@z!To}XhQv'N䡇_1"X̰{ѣGӷoN!}ŔmoBMMmw2S.c[nI -WW9VՎ>F2a5G^xU:t`̙4~r 8k5J;`,F˙hv&c7nnK0ǭo`ck/ekSu\>Nc0,f\:6]Z-lr:sժfd)Xwp'VK :9 znn0?`ɜx+u c{9e3ָ7*No-馛2eʡKOOW^OXT7܃ó>Kn`\ID|O{v!f]i?[4 9qYeoª ϽʝVTeuS'[u\J `ǨʤwM,=L~Drs4%%xZccE~7o^3Xeټu:߂%c5|׵>R[yhk 8Y~fyζ7%E|e-%K?:-6&1BAcsێvy\U`XjhƌCAA6m2]M7b|%33~ሸxVV u2$L%@FS(Ъlg5i\^_yC/b{F˜Uk7"6~N AFKo^'SCq?֘e3:lsbV;:kؾ+ٹ-NIbxMi=dԸ0ªk׮<?4uA;k*9z!I!]_$XZ+olW@v~Gj<^=&x|)Uw7ħfqTǪmP`]-_ oM ~wv/,_nŒMJ yb&q=s=G>}#8gQapD\ZZdCClRo<4ڶju%VcՉ{9v~zG~}_4Ӟ5RSj;Bf(%]Mu:NH#<W^y8 {2nqM$FBc6@:`Ű7B#r\/1t}Vݦ@R;dmtBKېsV]{ KJwQ^/Ǥ;~@m_w|O`ë9nKi׮v8;{W_͐!C~Ѳ(|xިl$4mtfDZ*G}8ö)j{VfqtR;R6Ҿ5M?4 3.9l{^]/tGim3Mݺu뮳D6q۷G$ZJ#?s-i0~FۮOf9 w)۴[ !3Ϫ*)4퍩x|>^^]r% "shr=n$$$p׺<գ\3΂eiꎣ!9Uזy֓VxڍN?tN֮]8iii<#dedygMi})qcH޿g96vmɗ morom FۛүYuZHڣ"Ӷ/9퀶7>yul{c*[[CK"Y8p IIITUUc!1fٛR?Fpoϸm3(]Z)k۴~uՎ>RZlߺN}&|)Mgf;O[:vC]eNfoZ3scXyI7/,99sF͞7iii 7mo=\z7|cCbj.w1~c90XQ_/Fk(b{jf Tl֪^r#7߲×:ijwUjY?=h|lsklx}ucҝx>k$ N:1f̱޺޽65 X {P`^篮`'ȴ͠(jmo7'9WocڏME޴6 ^($$$0d-n 9. 1 &T>Pd/M&FKz6Z| =^/Ƴe3qP\Oo7֯\:o(:'/?`I @Bzϻ qo^C`=&.`@mGK{n9޽{ӻwOꪫ.+ :c{kW˾bu .Dsc4fKKfUB|;!)BK<34>}/ڟ~9M?}w6&kE#UE[uI{XޘW_}=tV?:DR7`75Z1xgۛ)w `u)^5ת;y@{ۛ8s<Ԗ[uv("GhKw5j[qOζ70zM؄Ɠr3i]wl,_ŇTZuÍK6:~tǤ[3B3: 6'nj.h7bUZ1,m($`sF}tm0iMyFufKL U_w/cqt 7q]/t%zAۛ-.?5t F3'm­M{cҢ{x"W-qv: B dmMg Ӿg=^j'.C3~6f1P9^zo?v km{<? ݹi:PID]pm뭺[ö7ۿ𣗭:gX?ݪulG6W&5аͮoY:^[lza?4 $URR$|-z}z}/?trпmf6i#;BtqƮo5@+@MR*"rrs9}ΡsYW>}4Xd}muNo ^S$d8>HojKpݕXV]]/iYu8z6\ݰG-_RȾ1L~'5z27Oxi`.c&W`jyf_J^kfdGۛn:go}Th]r@ۛB{ۛZ ƤE6̱n`oi Zr#&4}#S[loQ[؀{`ue{#.-_[hcC%ػw1ϱPޱcG7!⎆mo7x|.V_m*̡`Y&m!JKN% BIҏOb`|,|Z3qx|΃;Ʊ,|PW9ޞH8pv"Ě߾}vww=: ;*1^}Xl|y}r +gSehNg_hXo7yڪӺݩ^0It[f?CU666|h*5Ilazrp;ĹalW`̜Omq)jS{ ߄^v'B}`]@f) >dmd'"GgϞ=ҭq4šf۠H[|cfF(cj8ucf{o|"9CBkv|{}dJo~M&chsҍo6:;HYumY1gޝHsr6nkj3V{pѱĒMI!{EJb~ Y֯(\8ͪR IDAT7͋ՎI yt9gڟWoT{ֆh{ܪ7 Q{veγV}{ JѰc-JKܜ^b5KN%{PJ8.K {?PK}U't"Ӷ3u+(\gյxu_."ruDup֜*rX |&B=š#T| g8mC WΦPYءOȫK.f;B˦;^_i^yѻ>{hGmh#"N eC 2D%Fa1ix`&ͶEU+W ?| uK՜6xq:/\*UXu㳵ܹɱwP~gs5|ڗBsx| ,7a6 [F|@"-V !34f򷩳IjGFSlߺ֪=>y.2Wxܡ%f/1zW'$uSI>{m˜g* a#^SV|Db[gp5`,60fax`85'Kr^hBo"t}W:ue?u<ENծ8'``]-e8w$ڴic/hTF,Ÿk:E8{sZi*.vvW^>XSC(oF9qԻW:Ƶ=LuOV$v&shã߭uPm򥿺/?r<=h.AiMj{^x1wnݺA%?,P_]Aq_yC/s6:])46:idxUWJѺOϩT1NkWʱK+VUnՇ;O$η[qnH p9(0f93]՞B~U6tz)?d=C_xڄes/4kgƽ{7??/  M u+JT3t^7!`MF4_7w7`@V׋HjԪ,1:ctaι(Ÿ P~Ny3>ڎx$}gfH&Ol/SO#Qy"6,qpl4b<ƈۄ*3ԯ:ypUZ 'V1"on;ЦoCvwB2mG=pi#ڎ|"ijéߡg0HϷj;/LAuzn0Vwcb7(vDX`oRp{=ߡVz7Nt`{+ #ى] + nUtq5F߁[ q9m,?XuAwª ?:94XPߝX/`ْ%K6OO%qfa7]E"47)W?N\rQ|c=h[IʡmΡKٳضPO>Fgct61` 5.x>[{&:fp_&lݞCgoRcu{O)Yr9/0;]2v~YcoӊfADMfprgk㓕ш^\)M2Nph,}a8~B_-(۷0BZW!fN^W/ț7Hz۳sޣ@W(+ټybs[T˖-c޼yXK;!=.'{Z̊C{9;oӤHwd>/'zu|V (X7zxϒ)$uˆMgSv7s{[1Ap'߱M}lmR'?w_|13gΤ}N6cƌT#TVVZZի֭=M16Y]pnUWctl+1WwtNn؂Ws@^csތz,`͡K%FlW:ASZ\=Hn:ve?#p`&$7)$xJwWo ϹZֺ+MA|c>E7P^1J͈GXch=h; 8X;Y~kCNߑ~1:M$1nĪF_比kw>mO,ؗ-z*-ctJvLVrigU0mb*l/ {,mf˖-׏VbŊ7~-"w4{XpN6VDt%f:8 v2pbsPkg]Bb9kmNѮ;:KJWm_<*"ÀJ1qJL4(ƠYD H5Gu} -{F:w|"mG8_=1F1:Z!>icNctv/.wՋB1:j< 4Wڗc~ 7E fVܻ\43- >r,B Ni mXbٽ`"-ΰ6;V]ٳu ǑfR@|J}Z_6?Jvm`|m}3X"w tU,⬿5g:#.cc~1sL:uW:vmx{G/:(29|/'jx/Z8p~s|<D( MZqvp`os5kČ^¢ț769Fct&L;߰w?ooϺ$pιؽx4D1`|\p̛7/$_D%&&G1bDj* xC0~܌1_ՋN;-lMK)9Hos57`|2;^ |wB {&ﻏ1:HϼLJ*!w=(7kXwߠWY+Sv7cֻ_1fӠkP?|9gZs}Hڵ3c vzόR=˖-zm"T+V 5U 12| ۸43(B`O,|F8fh)ϠqOWNHݛ99~.%Y9S^¦Y [6fܴ_"Oaa!7pyyy>9L>S+22>I| C? 뮻n) &aY؛BK1~HޤԵ)WUɦ /^ys?tf62z\Ӫ 9}G7טg4w#O3i;Q־e)/cz؃}O3>$ϩ>䓈 Xvus5w]G-zWOpY۷| 3beTNF~UhߞuyVwT w뱩Ud ͺ`|-uժJs+QsƌXϧ,_ϊR/R`C휨OfoTfή7avC ]^@Wa1?.7ٷbUkܖv_c<NHI!߰գn~׀7$.i>{N^OZ|#6̛7AQPPt+axj.L:o'-ŋ/HQQ6NՕ[肢#IrNMJȹz޻l*|gծL[h3Q[u1:z B|mSPRf-.Azh2k}0랪Jn>c,X~j?wG8 w1gϞkvmRFbL$q>HlL^|Yvzm6}\j}ػ oe9;f793P|J܉x+9eE7o<2\q )99/$#k~/)Y7G<D`rp+K:u*={t(--塇p!pPLW'c|&L૯+p+'w@:@P3Cۖ1BBj&g>WUcIߑ`{Z_OMkx S2zsYM13аeVzU%z5L7oO XqS .K.aڴi4hB_g͚5CY ƿB5Uv=9 }IhX`oҊSvoٻx Ưqc\8vZa&k贼^6M|ɛ; +@_ٺew?wl2 oM쇞VN\'[onB[nžʾx,jbL}Y FNN{94]Yh֭>TR/SxrYKmoSή9ck|oEuW-MoK/$&OLE c}$ؿ?^z)?0G ֭[`]LW'We}deeDb*Z>paUl#}˦SYY8&? kʪYluѐ,ۈ;Kqϒ]Y+ awQͰIhD'x2'/yk%K0`ӧOQ?~'?S;;}8fo#U+խaKB#F{I! dzvK~7Y紿I<qrVK~i7BMBWX+Zu!c,LowB2mG==ct}\PPݻy u p{<~_RYYɝwyϋ:^?J#%"C/]6meR1s%(W ,ZP***˯a#7|ɣ~R*Ӎ<7۶mc̜9S= 1b 2HVl cFO%+C}+zMڒ >{Ө琀wTkl@í wx@…ד¿qxUALzC4wsʂ 7.=~` F,zE]̙3i֬۶m>|8*F⊕@[orM7S#Xr Sͱ7j 5`$8H)<JpsH>l*֯1Sfpگq1~R5p+ys?xNIѨRͽNE`ط|V!E1FݨY,)on?i;.Bxp'Y+sl9_Cmz9t(`p>*  rwSQQpkz;6`j[O>zڄGRfrpd`/*,R_ar~FWmqSƛ-[K5;9 ('L^b,`=5gݧ8fЮlhځ>"6i(+޾x{Pv !P8-p%$QTQ}nGB[Ɂϻ|ҿIk(X1CѶ'%H|>oj^Q>%bl>8+Q7c%_bÇg6clHWUU7̔)Sxi۶ݝo Qn jG.^/s=Oݛ6mHhxl,gÎ\2OY1_$>;.?w$98 NSp'Buh_]0a|_~>5ܿd7c\y6\.zAILL#9s?!t̛Oj^BhЀTXCݻI ߥ͍Qߧyf-Zt!5j(nOV\i\trCکx !CHI \p+..fƌ1` 쇱V %Ee{`/Shc\9,U,X 0Y 1S1FIJ1vpvs7cQBln4 1 t'p}svd?V0.$G8aH@ oNq{Fq0/`N{ =HEJ`Ƹ p' '_(F]S?XE"Z`L[H h{1FE1?q IDAT~O˜ߝZ{J{^DB&¿'R'DR`or~r*G%+n$H10F,0F$Łk4lj,n /8^A h1{v{I5w̳"åǂc^(VC/sAT~Ay|sH(0BFe"4BLcgtkZeC##ٽ|_)v/8&d F 1^F>Ne1$GX}5" "ޤ^DDԭ u^`Ι'.Pfo%|v>EDD&Z{{DB$Bx +kZAʕ.%ʡjl>௄wDߩ tAPL6:݈H,ޤ^DDh M ExB)YmGy1fڻRK w16ta+WyY""-7)(Z{{1+ x0HqR0ED 16ޮx gh M ED.ޤ^DA35fddW6mڄs6mv!{t--%ϡVr^=Co;!S(Y$$$вeK9u<<툈DhM ED.ޤ^DjKޟ^馛o~CCUJLL(zloV`PU]>;ybqO**n%,\IIO\=x|fz˂~GsfUW]`KСCk_m*:;JD$ԅNrDD$եNrDJ}k OT{Gdf- pXqӭ[}`_>SNrꫯgJؔVDDյ^DD>8؃s+Ր=ࢋ.k׮̇1g/VO!p;W_ ,ON>}n%l:w̍7h?tFx/""@tK'T[}V酿RX*X%| (D 3s)}'a,;믿ޙ"q1}L}[u4'`ʙ3gwޮ];ڵktaxbz:ޕۅsT{ƴ) Ǩ3?G΂M?pz:U:о,IsUM>h؟P̸guܙkךg(#17#W`/"":{S{gq%wS i5?#Aąbƽ8뮳H*0;Tb"9ϸW`/""Q1Ch{癡[gP+1pJˠ,`ވ>x7P+"")T}P ED$\GK`o W`/оycǎc\ײV82/BBSp;zE\\ц|I%7;W`/"":{S0{Ƙ}ah޼sĚV@[Xo*]ct(!==&mk;WD. Vpm)X{ S 57+,nA-F9JԳ5gk0>64n^6tHw}S ؋zBII؛N6W`/@@rrr-Iۀy5Ω9FgbQ CG}ޡ9 =7lp^DDX M'+\n ~ >>Seר֨30Fv>4'J`o:^D c-7op^$2ZNRs|&pys>̜\bQp<P:ՇH49>{ ED$+t^Hs%EEٴ48#$[Xh:ٷoޝ^caG_ EDӱX M ؋H$-T[p^$:1|Wv\7ccYS;1m"0<< -J[9aR uYF؛j ؋H$וT3W`/=oٲŹnbDc΂>V_4Ok GRRH8Xi^BshU3+fp^DD-/K*dXrCĘ<>Y%a+FJ`oRp/d^+""3{K ]GDfPkgW`/=~y㜲co8|Wc<S&J~,68ԊH]`晐#K=.0lQD'x}V_$ژ<@nn..e>^dt[8'młX M %TƎk/ C~N.N"""""K; }FXSQdr؛ `̸YrD؛x,a.x>2wQXXpgvq58F-Z~~Cz+ zCaYesA/p3hs, GM6e˖-$$$8UJLL:i]ˉ_{\E𚭾x_8pZNmu%FepJJ"&-Çל 1cSO9FX]}L0,9@oU e{-_,Pp/""""rܶ4?صk|DL[}V'CW=DC3f;a~iƌta|r&Nh?o؋#>pWQ9G \+5*GDDDD>:<# <ή>)qcl.k=ka+ͣr~ 6l  wcl ѣsЫ/9ؒHT-7ق\q_[`o#""""r 졽x`ΝՍ~o+}QQ'N$8Xk qK0vHx穬䥗b7~7oП|j M+7)9uSxxGQ4iB)LZn #P`(FL8"a2Hw+fcN|嗹+ckZ ""!r)Fo`oҨSs'̀U@y஻W_%---lEh{-m7}ctՕT̟ǝse]وV}as]v#`eW7@-L6+.qeﭷw*)S M EDDDDNўXZ?LHWUqӍR`a!#uN 0] {`9>B FDDM0{S4M EDDDDNL͙v0L S/R)?1YpyM펠Q9㍐.""""K) """'n0x1~z+p.0?\'""""m4GJ}iTA 8.799*'#qLn w(Q`> EDDNN] ؋D. Oɉ^HdSh/!9 EDDNN EDDDD"B{ )Sp/""rrb-W`/""""N7 +{>{PPt/'Id:݋=.to=;\w<Z*Zi/!큽fB`o+`[+EDDNHW`/""""]^nd a+Wܯt{'W+>Zi/A烸p^NPvѺ ׂ^D0C\ 8hD\8Eq@oHlh|.𺠪}[;O_k? ¹J'< $z8N:D,݋H-KHDap_'{S E"N_mӍ[}0R@WdQ8lzdsXw<#!OÉtJ ?*'6U`/Q\2CQ` /WB(}:3g4K/X\Gzx,իǣ>Nشip90ڙDDD$)NH ؋D0F /O\\{ɓ:tKNcϚz|` Ϫs, t“O>y,7Q9:ӳط|ўs"M]2Ū3;]@z[2v>O%ṁ~w/;p'$Ӭߍ7m[̕HkE[WiKV~yrogAIIyh%o"55?opEr~? g.x_NDDDDq$"tTj8*GH `[շ{.%F*09omN{_{ I4yisҚnixΥ$5hn} InԺ=M7:_`^wx*FFX{+(ٹBJxT؞?&Gֲ%)iѢo.F:NM.t꫘xW7>""""^#‚{5DRp^$$9Cjj*ӦM[nv%z+0Vs-,qU+C>w 94k$NΟ)<&}C=K Zz\A| Ο?ʪӚNZ+~`w&v;5VCm^k; VBGYfC/x\߰a_>_5^xs]`r؋Dfӽ{w KyS;3vňrBۖ}ҺМ $R[ugw iwx;C큷uEOj믿su=z4fbܹ{1\W""" KX9+?'{"Ʌ лwox.1>|3hnձ3{~M=O1V'fd"el:uV"K>qiVv#U%t3Kv뭷eˬ17;ُ]vv6ӦM7xwҥ 8ژ8Nq$891*GHĺ`=n:Hty +sKl oFPùkڪ.NeQU'g}f?.0F}FHYQ)Hd)6~ު ̎}`׷Z+%2n^}U8LF5kVN;~ @ 0W`¹^HD-Z0tP'{qֵu0v%\6! }FWU_Ҭߍ)g*1L$U'F)9mhs6-dv>z [PبG<Pg+;gKݵOr`UrǍCo-t+oرcҥnmTO*0xٮDDDI 1a ؟p E"ZO#l}HynkѢ]v_~yڋi]Q9 OQ(G(xgf1x`[Wc`2Wqa#DwEVm9` idߪK7%o#zκܴ~5FSVIjȯ VqĢn^vNs q! I({"Ql󃤤$zd/"!bwa<cੂ%Ka_zX#!~\8a }U^<9kds) iYGeEu{F4Kț;+:.96aVv|'(O$ 28EDDD^"B{A^H԰ wԉ'{1F&)j*|}nHHk`ջN Xܰ;߄;a %y@^C,J}Nј9}GަUoT܍.==Ν;)KRp>D+*ڶm{D7Ʀ`rsj%ܻi**-dߙ2c IDAT4}d8z\aĩ.}nNʡ;ƥ챯wi1:%ly"1rsCDDD^") CT{"QAf͎vH fg;lu 0V&8ת륓}@6,`frOER[YO_ WCrǿCwҪMpӏAz"}2ǩ>DDDy %dp>LN&W`/222C$2] W׸b|> Ӧ:tyؽt@bFC޽sdXΦJ>#/Y;!6#ylV}qI)F;"aÆS}KD:^}Hp^$j%$%%9هHdjklfqÒ/%7hFϵM?֘]BfoXyvø}>61>Jz۳z̮OʡۭƥY2Hj( OjEDDD^"q r<{XUlu:Ъ9k]x\>_zwBRzt^M0) 6eUfI pNѶ5.wO?#R~Q %#W`ﰣ E}]ZzOĮn[=gsmuW{0DBzsuUqhWͶ}hxe>gS->%6W=p[&V'qgX;+fo!iȑSEDD$)WKp>B)W`/8p>D"O!n qQI\Am[YΞ_ZuBFC 8'ThS3K Z_kXuI^.;UgugXu|j&)>2V#?T"""<j #=W`/3l۶h=xu>^ lGR^-ޞS*wI⭺xzd M/۫J?YuJN{%>6~<>O`%;[u=lt͵dzĨc"\KĞA:B0v-`Æ N!Ylu/>h3˨Uvi)e˖C8[NivmδjK|wtZ }0kl *Wشl߰ln6VR _zhzDbŋe~0HQh/Q[}ȑ`ƍd?"o 1n*;̎Z n?M>sy;ہKιC9Jvm`[76:>O6崳}l1&#gymm\ XpH`CHxn>_~T[qE؛9˴ ] CF=~-?GڹVLZtJ>#M0vLHϦU7?{5`x&`%_q`VrНkԯyc6>>Vj߾}L4~h,A\"""R)}$t[ikκO:˴ ]7?6&/̿>.&ܿ[*XK|Ju{Ѷ5Ī3γjoe9'3{$-[ƛouː!C4i\rӭ/Lqux(H8"""R|Hii8d-€{NHݨn>-Zh;qNIH=C0tP+[ ;wo}vZi/"""G&Ơ6lO?+pK"a ,} {SاZ4~P؛yVֺi-:ZaBq>'pE_!g6-'GAV{Ϡ;InCA۾49oxpg}ٳ&8)w1b>`KkDDD$B( \?pW8ەH8I7Sj _2_)>R{3,nkqV]w;@>94՛Ӯoݕ&dݞs؛M_S^b.{\7UTT裏tdc2rH _~iӦѡvDDD$ٴ%P x9rd͐A$67m^`/Cn^AuVݠkco ٷ|U'7lA|g4KѹouGKJCo{8;K$z7Y~S8***7n][oO :n/Cn宇D']ϰ!冻$ImK$I&LlQ~ <xbo1G /۲uLI$i0$I(>^$I? %I.+}$I?K$I)$IR3$IC_ S %Ifh/I$uR_S %Ieh/I$uB)$IRn2$I!{I$)K$I>^$I-$IAz`bp/I$C{I$%O1$Ir$IF S %I^$IJ_{I$o3$IZ>^$I %I$N`bp/I$M$IZ`bp/I$=Ѱ$I”HSG`[؁}K͕Lvg똉 Gex$IR1$IҀD<ݜ: g a)gBB6%I$"H$IS7"9*fTo/9lʉsK!I$:C{I$Eo}9O^$IC{I$)MoاFpo`/I$u$IFOاdpo`/I$C{I$=b`$It %INpˁ}Jw{{I$ka7 I$UgBS%v %Wg`/I$u$Iԁ8%]_#p|7$I8$IR'FDL"M4B ӥ9&3H ꀵ7$I+%IN8" C/ (W3o`/I$X H$I`-LN*#PX>-QFa>.%I$I!IqH@U>3o `"pZ" `}@5K$I:gK$IHӷG q$ZU+qE`+-(8B$IR`h/I$`}G %I1$Iѕ>?] S %I3$IN`}w{I$k %I4=اbp}$It %I=اRpߓ}$Itx %I$z'OɅ7{I$ %I4f`җ S %I1$IҀ>/S %IC݀$I895@glPdġƱ$I\$I8#y:f_>0HIluLI$)K$ih$IwK$I ^$I %I=7$I>C{I$)M %I`h/I$߂{{I$)wK$I/$I[ %I^$I=$IRr57$Ir$ItK$I^$I\ %Ifh/IBEu'KIJusx}74jBy~T+U lS=b-qMtR prMa7!I6C{I_.܋U<{W֌'/ˁ5xy}z8'&:#pR؍?$Il!Ir06V8 ?v;T ɣY:fɏ^]0Xv#(H3?o#I"$I5xrRW7W7'lS؍c4ذew_'I^$u$i |/fر֜=W\qEܥ{KK`^ү*ZcɼumEc~[z5~{P"ƛ8&j+(h &4V7+,&˼nDD Rz~!@ZV@$B H$! V0R"< hس+XqFῃzo[o!s0'cRϟϲe(*ۧݻ_?>ҿWKy^"I3鼿d`WWPFF|æN!!O8C&̤R3}s1XuZ VXI?ZAtPm.-vWn |,U:X7|pJK[{S7/~ ֭[#?Кtڰ`~a^W=o%KpuԮ7[N$%)IrSŢEx 5d (U39F2OTzwQ&o~سUvl>#oC[+z65mlxvx$Ġ3fyģɁ@[LW,e>oNT0vښDS]A`#|`nΜ9\GBrҕ$I$I bΜ9,_Q8KH@PXUO&d@ݫ <5^^N݅H1/WؿksPN-'. [%Rv'Hி BXa1:Euӹ… 7}it'>u9bŊO~*6msyy _[Iq$IRd@^^կ 50KUƀS]ןq_eP:y ֹ?u>fkA=l =) z56e7@iZee7f̲ٻط|_2dTϜz7O]w?S_+IK)t^{x,$d~f5`TyBkMm"1&_蓾dv[o(-hg/RfEP[p9C&YGMbY#秞3{lpw6l ݒ%K4P7}]woزeKjӏI񦴒$ `Ǒ$Ie Ho׉si ̴ *U̸ϲO9-sM{coN8aAPSA[O~Nt`"̆.cC|w2z4꩗ Ѽ梛naÆza]r%,[l@ 7ܐi6$Ih^$gu3ghM&||Q j*yC9MḆ7gQcoٽ:m}I?^^Nrh/v[#/n؛6u1fDy;F{|Ͱ[s/\j[$I!X$IRg榊+R?H贺XVO'V Uӳ'zឌ7ڎYMu5A=/9P}~&$O{̂)pLn%clo75Ƙ43Wsrƀ_έޚt -@x]I09G$d qg؊&2oY,j~ZcYihR}oʩ~%o W:Tʵ zAH}A`)*aecL㾪zDjhg^M3z+L/ƹ$Ix %IR{1y0{G2\[yO>Moɱ72JgfKhn 2f~挟/Qv!t؛aDhu^p&8;i`;w.%%w>5^$IR %IR{̚5+>pՓɌ]-x~coFz'VPi GfE38ƻ51e߾Q>'IF7o^&C{I0C{IԞ ћ0aB}Hḗ AZS}u,?gujMJ3痿Ӽ/SHZ_J*~:8㔌zo\_f#IS2ue(_\'eh F9 0ӗvl$fT$s$= a!eW)pFZ>͚%iv*`wQQ׾y -x%N$Ȉ| ģ16OC9cD51oM l{2O5De!@᫭M/@AJ~^$gAA.Ȝes0;~ yWa85k6NǭwǍ}J; Mu{xqf2yM u"Vr#Hă} zs̭΁`x&.^eת|N@V]]^%K^$<`.R.+O?nR ufwqt`yz߉/O"-t/2Ӄ⭕4)HQQ }=ؿo|D"LoeN=L\t-#M4c,ΩH/C$^$gk-[C iCz,ٴz=vz\}y0s@6y)c͘MZ ??cMZOvȤٌQPZ {_B𯃺j8;i`yf)>$IR %IR{6[.>pDiu`59F.3;0>eĬ34ltP~a Wϰ[ $OmX͌%7Ԕџg㨋:cΆ~f >/#cC[zuz`$IRgh/IzAee%7n )Gsgmiu)4*=Db1FŠn޿X3jy Zz`NQ] 2ӂ701e ictet?t?fÆ %I %IR{^%֛O xl2ۑ2s]5:5F疖֧>eR{{$$|i9܎u:<W=)s+(t:o=EPcT{ҙ s?`ϓW7ԔџsE쏺)_"7u'\H4M6;4yyyqD"=y;L/&y lI4@K'w裏%)`6W 8XR? |t 3Pۺ}[1dL"(c_GoȘS/ W}4;Hҙ Z,۞c?})@БL\*}[ֲ{z@?k׆J_*~;P /+d|*΃$Iy%$IׯI~D"mr;RH %iu9vbŴ .W>E zKI[o;d§gԱ"s6'g~q]F63O5A3/٧{H477zq?q 7$$I$I:* H~ۑB482~+!9F p8wR5c_L$'|1󆲥Otl-4VMgWug#ϔWP7`#gnưho~jn=\2}-ڑ$I}$IkD"}B6k8x "cN`.mMkzа`N=@Go˺D1Chm5[c{DsɎs6Nꏞ5wnSO~ݻ7]k3̝wɗ%{;vկ~5}S9O!#IC{Iԑ@0bժU\s5mH)vK@z]i j>D3nWX3jb%9bC& ~KG3ourNK'KB+e?Sشizkǁ{I~*݀uٲe,]_mm-\rI7N 8KJ$KC;Ңw\uUԄؒ X2gGq 2<9_>>J[hW 4;=ITn#[~P__*NԆ˗s󪫫9y7ϐZ$I}LK$I< 0i$z.]ʰaBkLꪇz?G+s7k,׼Km^7y m?< , nKk!Ԟ)J`rj… x}3`bv_u@S*&:i $&zSKK$Ia-Ll 1d^da *3"P=^-I$3 %IN꫁}$I %IN}$I %ICȕ>^$I]$IRr-O1$Ir$ItاK$I^$IjG)$IRn1$I/}$I; %I4-O1$Ir$IԢ)$IRgh/I${I$o3$IҀ7P{I$2$IҀ6{I$o݀$I8y'x&>0 fgx$IR.2$IҀrs J`2 v5h"{cI$I8$IfXXo1*'w$IR2$I/)K$Igh/I$> %Ich/I$> %Igh/I$}.)=K$I]ch/I$;}.) %I3$I+}SK$Ich/I$upاNpo`/I$u$It c`ҙ^$I$IR't>^$I9$IR'>^$IY$IaHR`K$I$IBƮNܜ?zFK$I k9 IENDB`pairtools-0.3.0/doc/_static/read_pair_WW.png000066400000000000000000001004401345765554600210150ustar00rootroot00000000000000PNG  IHDR(G%sBIT|d pHYs.$.$* tEXtSoftwarewww.inkscape.org< IDATxw|SUǿIwK  D*8QQpx{{ (n=q#dSfJKMܓ$'IÓ$\n==&V6M T-ZFc"4e9[ V-0EFCՋB4Fh4Fhh4Fh4Fl|Yfc< 8aR8c޿1.,-%cLJvAk\/|:gxP ?<{Q$6+󏏍ݰ3ix|V}}`CO#Jm?mICFvIj[4ⶃN&iJ_s0'|izUYT Fq<~ELYAh&EHZRՋpZyBs:NA=Eg.Zc<4I3+]w+y?&t:*>w+\tȤөW+RQ==t5Xۄ'Wծ+Mb 񽖢l1 -]H?fi,.`{FMP lDĶM0wF@[0M|?I1}"BBxm*ٵZش L /"`0 -j-=:5 }&IN]Į0Gy`xղafĉOO+Rq k\&J@(؜Úo6G59%-Z$V4B8FLkM`{i)==S02߰(+e7h-*+"偾@>-0 1lzsj-;8yrczem%@ܺr+3es?i &P#E88Uy=E=e vVϼ2CKٓ)ag7P+EKJ&'p8X͔n_'J&'p8X-nZ,!d]B''< = {w. OZ; sl,v;/rk!Hr l&!JZ@Ohv[+_]FǓ=e:!GU3`B"c5e:!|^Ūj-2ШX5Z_C͆) \`#İz 5o*LM-׭w#ϯ\Β{hTvbrٽSq:>-*pIM`طWX,d^:EgVj m ˚n s:^=~=sr?])ђ4v2^⻫ɔsG@lX9{CQ+=Oth( 9Peoy9w-JZ|Fp%b'6_h -g{)޸T+=/{d5>6߿u Xȼ StW!qwE^bp|CK%C_%6~8+1F9V?e/qh;;f/LFNNV/tb8Je{^6ⴑnJh4uC'><-k)Q^Us-># DT{W]&ZKdu1IĻ_xJa7AnB#r;ek1wL׳oSlּv8ď7.c ީFˮMzZv:qw+Rw gN5ԋ>Dˡ cR3>CZ6d3 :_U89W ؿa1>xЈSh9u%=תѲ9oaĭ{%>Jf[K-YGh)ݑ+L10}-MCHAaWK)ђuEE# JjDiY|Hﰕ3m2UK᫤ʀiS*)GmRɞ2 K]KX9m 2r,̰Cy,%gdH;<-Y_s-rr]E}^W5,Wݛ(KemQ-j1\SF8\o*[ <_EoFWQ2'a-QySJ(m%zNzVeδ~aDi+-b ړ5IAF$%sYf9/]m QyE>ػkR^ڿa1N3Ar#0ch99qǑzx%ZJeOq^G x(ݾ=n =q(ruk}忾w?z&i^:mjCA)e:^k6ܴtF(ҢQN#!y[%%[VȾ%{P'=%=qv[ֱ5eT-y\7>mCU32ùu!4&^W`ɣPuB7)8X dV5pR 3;:h)m.CW&1r`.zFPClc:gu{;"Lld,|EA|I wZ!dMzN|e| ;-V.=Wrd;-z^{aU aX,dN|(ٺ0/aԅ_MP(+^#>PegY).j-㹹->elVbEx@=^=Zާk{Rļf\l4qz/vϢ㫤ܓ0-mֽ+ZE9{w:kM fk7wi\KLZO__VLM_<Flv;w/h6:4N z{ͮ2!Cib‡T`)ypْy4{;Ji3MP@$i,9+DW} OrA`VV|-}ZB"cȞ2ŏIUlZnxK^Sgb++h93MHHD4S36~UU4lFDW*Uܹo +Ľk\x8 9HZ|\̮gc6d%?\35e?qr̳ɣ&_zƍ=a`%톝E1r+os&*_|?bٳ;5ZCˌxwMŮ,ݻ :α7x~2Tkw D8%Z|f81A-.qp"-#&->br~'giIDUp&_AKZg'*ҕK42Z."Gy$w& lE~-u7O,[b Xx`PhyrRVV{!GВ<8H6i-&<`],;GmbX=ϵC -m|MػC7n6~.g&kd~i4w|l':GOh&-x̤$3cS; sd`jFZ~3zCO7FGv˂[ _D:S̙j\n@S2m?Ɏ?>ޣuQt}2v-/NqyF|mOTSWц\dWefh;aӀ i%􅮚2VRiSW\%$+_4j mEL4ӽvUy 9Ӧ(1t]U^iSJh94`)9&+3hjcmQ!/VGrqu{+*ⱥDvLTg؁8D+^vWa.w]IB"p8X-nS p+{-(\rhX#^WciG:uś)ђ~굴x{)XWg_ߢ|bўLs1<1#(2\^rC :2^[ϰ/!Z~dHZ4ADT-XJ'}F_ɖլ{v5ZȚtG._8Zڒ=%ݶlᓡ@KXLkL.vmb+W\F㎻eV{`>6_Q ZKZL+|->y#fNh KY9m˟!2EقJ˟&ZiS|FC 7?*lEo!C+-e6eJL(7hƍUh48^FqRߌi4pp)"5܎hR|%0ܗ׳3܎zh4 .mp4~C&ruqcY{0?6W?'"{jwυ CFw>_^hZ4|[*=PčxBܧ"!ZDg-0H"&yMz"p=ֵcTYko#ѩ8{<:mf2k8L͕r+ ^Aki  M?s9@m3?Jb-fĠ>f@Cj@dZZ#uE}j4^$ xN)z[SC "+ᣦ&CkĩulkAd:Ǥ?h QF>܋6#1hZ.ϑijCC?{k t l^n /"sk v@bB4-(l͏QipdM j4Ѱ$`+2zS!+J3pb-(XeaEĽBM$p:ic!r_6 }!gu-8D}ԥ 9Fmψ@j'4!Mx} a?b忝 YLŖIx1,54dr$ES iyb-icub-Ay=5Cc{{Ws1"P#~=Ո nU4&"Gk f] jz!.®ο} 3Mll^ x3Q,+p75=ty+Te442> ?{ IDAT74DD$ [!WR%y K"pޫ%b!kY(UǛ'#-k38jx+PMx |j3V 3 j{X@&'9Y`|>X&HC_v*ĽcۯaƎQ_'?K0z#n~E;o0 xa/ ٛƛ<&6}#Ev :4#NmbЇ@?X0~;+0 olLFSk.Z{6ԧ??OT sTߎ<}HRO[m 8z!&[w k}*h$Δ x.m"{qᜨz{Jς~.BuuZ4E";t1Q"?pt~}-E}M7`]tRCqSM@z 5}%M5C{$1QZK>ۄ!&WV84W< PUM*~]眸~Q4M9)<S}4~IB4~KkĪjpU%PQDWqf"# >@ogގL} 8+>СyvVo%\Y8uQOQSڏy̽7]xEC _oKqz"| 2<MfG]ԜzfoJaFm!wQs8D)^w_/ Muѐֿa%юݩs?0ڗsbj4N40@brl~<&"KNv>C1fɭ1lj55qm_b-NxH{ ?V ̙n_Rp5aX :pl`k":983T xPd]jw^qhT:"S1w"n#ow9=?T玱[{&&b?nP-D5O -C{P@yAji;1*VLeU c\'.U%ك. Fz!~#۩yދk4Ƃ4:[>q#>Q錦ӐSEև.߿x# z@{ Y2IUh7 'nRES;=+1*~PF,Gڇ?`W":TXa}>TDJw; BfF~XKb}ZVZ##q/Z5kbmBP-ٙď%t $0EG(֢ Rq>?K:&{XKc菸tW!##JF+Snlmy6R&ʹa,B/T D!ggSgˁ홊Z4k̩Ɋx;iyE&>uJJS89 f`F{t承p?Ex!"(uHw+ <ܝ &,.S'M P TzvGGcX<63Ư6X"SႡ}N?Dөlr\9^-[hXK 7&xQH}׿F9^CJlWyc0F.ULEN8o-*)fx]i8UBZ(a>#J/B=PG`ϝs)N*ghh?[׻M943!aq{EԞ%4撀9irG[~]AZs$fτg aʨYNF ,LyOU^8"L![ ݁0Do v8 r?quC{Mab-31B.jiM@Utmv]D"LET>GXC9 g"i4`Ie o3 eW@Ǣ=Գ{[^X }0'i04wU!d@j4D' vyBe$-+.3UMp75'%* B&P8~}+=ƸyYO'T簵_9ѷ)X<O˪wQt 2l5Op8pc&F.907B΃Gt\s{pjB\SJ dvGhZ2nFWa6|":9oE6 O@Ll{"2I5_Z]rb-d͈ںth4r4`YD9971ja=VDHiVj @=0h4Krb-,@dyRo47Ka6M#s(:')@r豵x(bĽ.4TAIYc`b-&Qr1obV/h|kHWd)My̵?E<'WÁwOr>7#[B-Eق/Q[i4- uC*P]c* E:nG_.Mi{;sX_?ZJs/vj3TCs"ƻi8Yr" -Dghd{w"b96)&hp`'" .13sDK_b^yB7kU\G [iuqZM?jvq g!&F iMx艸0A\l|Cc b@;+ne1x8*Ԣ+|?5G"@1VZD:e OwB8k b,DK"" }E d (Q~[i7'K!@vGLbrl+@.NPj :Q0_ c>b=z9uc%4mG,VDM0dlv4t|ˀ3 ~F F<z_a,OPw聘l@p)j4M착R!ɼ`1R!́~o&(iy.FlؐJ*0a l 3gp8\_*M#A| ,,/ӱcGBCr5xHf8yוjh \s ~R ?8gRhϨQX~=ְ1hw NXwBGVw\> XGb 3/{iwCO :H>:tG}Dv3`x).6١,w5ɉs=iӦlqqg8&f֟ɛo5:rTRzgV,`atR}bb**HLLkWOo^'z_鋧yU6z\IC]Ē$ >?f<_U^‚I $Eδ)Y}{TI˺w6Osi#/^YWgۥygϞ=·DdKeWDQFgG/2guƍc~cB>`2a:.}ߦ_~~cR-Xqe,_FE@F\۴E{vM 291cY9[6ڈTV~:*Ÿ[\<#9تOJJ }QOxx'M&}z_,y|,P=2x9ީ0;b*?kKWwɜ&YT?oiKvjws,}l,E]ow97W*'dvؑ~n7[os3d·O@Kv}q\'Nd̙XV-=AFP6v;3V46Է?1#5&'n۟mMeRh|l3ӈ24xa'ze:$2g߼HE.#zɉ ƛ&'lFN5^ZCetGLN98OkLj2't}'x"p>*|Wj38׏szd6MRŃxo- 䎝L׮ U,wQ689Fŝ}[1kkV̮ᰳo0msNƹw&' W7&'R>49a+-"eQc8lX!t{oyꩧl Q hD ә;w.Ty Νˈ#XҸHD@@ 59Pf_˖c@ަɉE|%߈OбF <6|HUXw)qQIN:xlw>~zӈz#q+;<`ġ񤻏w|;M%$`<#nt{bbҤIK"bWnݺ믿䄓$fϞZUaAq5ꍈcƌJ'' 3k,Iir%<|)ň&gLFa݋d}qʑ۹W ̆Nt8~=r?zMuY!doe%[VqlY6\hu$mh7SChh(?8G(jaL#惀(%|w %͸qox-: ȤS(/"III8'/1;!!\mukr~uvMxKeWY֯3ޭpKKqDreKj}G%uqxgaۼw(ٺƈOJX+YT}=~yLjAFGqhL駪i?f͢З^b3dܹtPwA ӥK^|E.蠚(+ 8=CǎyWD`ϔÖ|n=HLFIu]u23f*g8:[T >S?8S9r$s%}Ʉ x7l6H<|gN( ¸T?W8O>Yw'.I;ή2>ȕ7}$r\44RrʫVz39}|9}r ,.M`g/K2k[~2c/ CJȟGv1>4 #;Mo vl0.gИf~3u$%%qwr-8G}V)̝;y ._~~4d'Kǧ4$U9Fy j4F*㘩2AbT1כ>cӗԝʸ'S*c/!}J?3~7 В&'@ tc:٭]Vm6^^-ǒcftlWm{\KUcHc>_m)*Kx'c]ޥ?R#p%DH-;ENqf:u*ݻ׸%MN8yG\6xg IDAT00a]Nྦ)crh4GTZL)k2K7j2^_#q/qbi#v*X0bT`2歷"??}@BB&MRZJ믿{:&8ѵkWNFb\;,#N?ֽ݂VɆe'D:nlISKn_h ΛoɯZ .Lϑ ΰaØ? ~p}0aB7%MIe^2j4NTF7SJT^؀TMCTƮaz=7G\6mpꩧ駟:=A/XNm+-FܷM"Ǻt?;<n7-XURѣ'.Jpܷ&Zk4*͔5VRH/qtjwR6_s?xOh]ϾQsw9?F9##Nt 7Æ cذaex." #7X@wq7TFeҔTNc̙ 2.þrjňS[u&ef~3&\`:Qq#0FYǦ煕ˍV7wĚB#436?fcY!&]ͥϯ\fuzSh4@CL)7~y3.SfÞ߳/Ǩ'jw6ԋvGm~1a 9 ; b8+׏߫񩌝u*F$X2r36~b#wwP2j#FϲT`c, T&=u6#>-3⤏ہJf'Ŵ.ג)ftW_\~o ۍ]Ɉ L9M`S)eu}#n;p2bl=|Rx2湚45%WRo62dqS1y_L9 a1JOOwS NZ(xm* + m ol-)㍹FK]bB[YL\x80n}1[F%?P)9Rgw9VB"[qe3#N22S^Qf.hJGOKRXd,ƻ3ߒoJЭ)qgY)b[TF&ٵ+S*cQL;r)\NdR'{l]!1zo2mn\gTFOhdݞc< {i)n㝁mδK9+Gjjش z2GtcSq1_I#ڥط*J3erutLJ#V$96q@ö;L5Bv/ƈc;"x@k+P*++7:}۴5eC{([EpFvqF[ntN F\bgi'U%lhj5凗M';Y.xSJƄqP0HS+xgJSy |ϟfh|oI˄EWF{,Lۤ8wULXԴȳ{ &oZ$ I0e-p&~#ḍX:6:bb!ݳ_\( S]SQh4$4*gde256]ޅ5T **r?|1Iiy ڍ#.2(X>ψ-!dwWG4PTd*|G͈AޤtpVm{y_L>+`8l޼}Yrf{{i gՇp%[S{htOvMrr͡YlkI !k{wL w1u:{mMvrT]2re-Fln;+x-CEQIL >dZPI9#x/(ٲڈCIweo>#cXB`m]Lt{;TCn@u>r҉;b5qNa|F7#^Vy۶h$ m;⍲OHd|w>[nz܎5L CS>V齍8oTmQRr1LοЂCm)xh׵hX΢ZJ_si-ZbUhDIrSܧM"wn#g!1t>eANƹwT>||K_zbUn*ٷ l525J Jv\h<͂!B+˕eX3e6W0ms[)+Q^k4@9;<#rv_@ELLD.55ٳ;525[j䘩I >hZh<Ŋ+ҽ =A/!ŋ iH뱩sa.} "$+]2rw1kl>DV';[\P_kY ligt>zx'tF v &(6%%%̟αW6&~9e.%*4wQU?IX"B@DEADB}Cj[u[åR֥P @YdAd Ⱦ&3wL2ܙz3<,9sssgr$Gg*TZ1ͳ>n>p } ] ^ nÿQ\?ЮL=8u8W/32k<ņM$>`NS%&L(؊fmNu-yY_iwg6?a2¾p6&nIK4K_{^ 29 kaY雑YW>h!KAU`lڴ P30 1b;w{NiÔM>y HQ4+Yx4'"'OɁfʑ@fEgаln?.2+2kaɻPm&C^gۂ<3q+f臁@ziʰѯѰծ@2駟Xn]'AZ8s#|>Æ Fa!*&79~U\ӜOW,Axz4dV4Y^-[1g}髽Eed2r{$}> x}BpV[ZȗkV&..Z2+D,}QiG4|spY~/}9x3㝃NG;Ʃ&.V Ͼ*Y,}]7?@pkv/!ƶl>1N?|Wuq7d f/=k׎wde-O-%;v0nZ̊4SqTs7/3$^U39?-3_cƾI+ s}:E3 }F>pu yCؽdjr :]CcLU4ſghrײ9l%Yg^f^ -n |>sⳗ8k{raÆOO}b 5j+WF:W<1ŋ4i]v$g]S\xxVv /گ4fQYX4gULXk̊uʩ<5$o9-ٵCZ\lacgD`}l2$Pk͗92I3Ηعl6CHKa =՟V>yg]+>|ۜȬHfs#G:nݺ'PF C|>Kp#vu[h\V=p h]4 ǭs/ Z;Ml ^/hZ=g~|4R%S#/;0ab䳮 eoG{~2zjr'TxD$9p9`Gw=dVa__itՁvOٹlv^_H]m#kǽzshgYn/b'zo}:xwBz2Drxi||cְ;,tw.iڌ_$QQ]:jhs׬vsmK h""d.Z9r@x ue?@)8K_ md]Hk+fuwדhhg)..:kzRxb^u# 0@Jx9KۄYuÖ/e=1իsKϘukT-G癁vzL.`&֎q__ 8xón`گ^ԾHw.8?GǾb|u?Пy|A|I< Te˖C]+HByfw]-gYp8_ xcw'-Zc-Y~cVwbz\֬y=ize "YpڎuCҡO;:]C5_ `[0,}hZ>-G_U*!C0{lqDO2nݺs 'B~_s}=A1 4 3< Ksm!3h{^'iHr:GR@{Y[eyuO<;^7MoV'w{V/$'HKㄛ4#V}+w.>zu"Y̚5v7 5@W^y{7%')nʵ^˞=9pʞHTm+Vpuكfk7k)ukƘs) EE8ɽ䕐 "^fkNז7%8)Oz\H_~v E-.[ӧO/fR)qw2w\"#'?N~8xЛ1Gړ7n-vtY R+ȊоJ˨߳v:縫"-{#q=}^O2kլ9mg6ȡao9N <^|fx`&֌~x{uB|ɡ;8oR"{Bw2D8=NǬY ԙh>x wu=(Q,Y/T}yǸˏh ;ޤAf&6j\5EELܰŤGM:u]L/Lї5jЩaײ!^|E? "gÆ 4iލ5+0J1b={M4udTtvm;d$NEEE|GIAf˳,qnׯ29n!оqM/LF5jx,ݹ;]UF+m%{|W3xnk^(.{,kW&k;4zG/f5ФV-r?>X"o*#K6mXhQ'&￟W^y0JX~=[}ΛHGcC5ȧv&La~w}̜9ބYr,H_5t֭(۷od5j ӿ^qqqJ]6UDw^ :5T,N:I=AQXXntLȞWHjh#VٖG"'jժEjɿբ߮](*J^:5k,8 [ tfyQDusdرcGh/%(}-.pFD'""^ S"Q&ewcݐQ,#>fLFc R<S' ܁LFܺNX-]m e,s-{ciͻ9|g@`u+DTm\ " 0\%4,K_K$Wc8 yYYR j`2RaB| &+y") /L1f F$"""uD"""xshT1w 08DDD茹+LR\m8doxHua xm6f`SMDDD$r;^ 8Y*EDDv]Kuc( ?0"""Nј"N """"^kMHHTn]fɇ UW~)2/Uy I(b?O]q|]G ` 1n_<8H9/P1Eg H28 ?V鵫1ۘO IDATg0v{HnxD(>xTj@WB&"Au0fc>C`jS8{D|H0ҋ:1 +0""|:(: 6E#R p5ID$Y}7rρQcƣ(￴@`жsED$u:R< b1Km]DII=05=EDDʯ Xu 39Q@*X20SCHߺ:ɡAHٽL-[Rn<|J7oEEl^Sg~N#336m{ǚ5kزeKhh"dggӰaCVMEƍٰa14lؐOcİ|rvY &d] |Lp8m۶MϚ۷bŊD+ x`q_*U|}k֬Hrss}Nj_ zm(ƬgL4/`Yo^^}U_5Bߣ/xw-&ֿߕW^%~iC> >ܮ`ٿ*رc}͛775WwNq|t!")L=i`{.>L]v^`uxHTO3|p'' x 8ĸ tԮ]/.]D*IT^?tܙ._~y`*0ʻ$ 'b~fTR7xoۨDQ]Fw'q:p-pL$J*ٳ'*ӦMs`V l.*k15 @>}]r%̛7.7x>7=c8F1qDMNWC*+ׅ?cJ9 \m= 'G2~BYvv6}:a {- 7D:u3fLJLNڵkǸqB]($)Ե@FFHye{os~MVj1CHRax饗HK T8MԽ scM Gz/i@w#4eO>9T:c̘30b$׋kBz!|MVz;M[=P?N^z;:Ðb} 8[nuGC?q7{H :K|4}Bwr07v?a6M)>^z1tP뷘+:l4^}U>Cu2bڷoog;? nv.8+RI \ |of4L SBJ֊DFC\"3:twjkV W!gJv qHLdddбuh)?D+` gY8%"1ZWK`ԤI/U ,k4h f?n`t9WcnRI*YYYWqK`LT~XWJaÆv3+23='ZI@ծ{)`jEB |-xH#Z*R$HdvOF)"t=wd殁 j½DDF͚5]M@ULD<o~M\`I W"NtBJ8J3"9;zsUbdcݫ8ĥ9xHCŘGJn\hQr$"I!tEΝ;]M΁C" l>,xt`ݳ~$"~Vv` A9Z Eƍ_eP$M΁C"-3NNu&"oÆ vs[:.gժUqID8јmL>NJCDЮ]XԞGbիCIi*D$Νk79~rz~ᇸ$"K7¥2βJeIjgϦصOwΕZ̟?8D*}.LLG؇%"qA͛gw}LPzg̘נD*=2T:_܎CV"??sE"w2)9wŞ05D$)M6-zLPLuz,X.VDbI"cذav8Cr7-|>ƍ+t0ʪˀV{ 0jD$fj77W7c6I| <};>GG>gy={x 999s=Ub*~-+V&F:Wn0מs뭷zHepe! h:3qI6lX,V<u::p#F |* G%###QT&Ѥ2Ɲ2J :'x0\,XIO>u=vsW`'Nd…z$*ُT?a]&qKRڵkݻ7:/qq1xJ 4] 3sf>8&RIEh/S? h$%߿~yF osOhȔ5m4+kPQ8{^ꫯzHedOZbC]<qIӧOBMN̙3K.m۶yJ\ݻ]3\sc0EZ>ZXbΜ9tڕǓaHקO6olwR| bC֯_+`&Wr6n:K=8o 3?RXXSvo߾[yCs@bzw}Wb`վ8jŌqKR vcnn.*clq}0a= +vFIX{. |4}z衘(RD8jC\{10xw3I6jȐ!ՋzUkvE‘i,Yݻk׮R.tfmւ+̶9-q0l0f̘aw=`. ?^&v̙3ի=I>'\U_+M7֭[cHRV*$`վ -Rcv0DkbĈxFY"~ix{ ՞Ht0N>Ν;'Lw5w6S@8%))L-N=ۑ%Kڵk}tޝݻwK*0|p&NȭJ׮]iذaʦ)כGMCVE0`5%!ׄR_pٲe̙3'R<#vXw" }SO>뮻zðիy>}]\Oȇ$bL`Ro;v_W_M&M4HDa5xJw~Œy[aPu?| ~!W?jfG07oXl]twߥv-{n~m bgN x?ҊKt” ADpfׁP&g3r4f.:{丄6I^9""6<;1`WubZ tBh;^I&I^>asr^/n!Aa; fc,'*"zo{DbV&: xxHMץ<njza TVDD0.M*&cBR kgЪ@#Z9oDuSKRW6f%IIdUf^QA֡""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""4H!=!}M"jZ]B~ {H*-P-?@qC)1/yF$:~GkF$"K)3q|U4"J"DDDDDDDD4A!"""""""xN"""""""9{X:DqM#^ٵ,E ( pXu'g ϢFDDD\6f3(K7 iw^`9Ms}>#e ?|e L: :HhfS!ʷ;߭kf[oXc|'Bbx7e]39kDv-⑜rp5 &Q\8{!'E\uC5E@(kZGqM<,Vt N\xR~T-G ]DDD$bQ\sfu9Y㟀M\V;w&H$Xe&3iBDDbi j#?,MEpiڔ}GŎӉf IiS" """gl>=sLp^,cGqMN5n ?u`mF#zׁN vG0EDDbٓ LG3kC.esYE0 "z]1$-MPHY|w *us1Cuw@9bY*"""c/KugXhEu(^csC'sæ:'`;ýfRZu|6p/0\JCGu(v\WEADDDbpPt"XžoJϹwan(c<)}9 J97hBDDbm*աH#Xb:f#6j9'mU n/ Gu2 """gg4D;0"1HlJ>_CP8廀VSg=}y<&gߩGQ1$MPTpp}"rJ yqncv;tr`: ١v{6eO\1\&IœڞȑHli"}m֡w185:Y39ʈ~<9KN'@IwxEbG`?a/!'3whO|k? N"Qۡ""MXfY?Pػe?agw ;Vi1֡jo!O&(*R/H)"5L!s::i :N욘mAyQErpglmC_$P~&TeP<؞ȵM uTJ?#=W.,ު V԰u(N!r G:`&:L|L"0uP0bpb?1+Dy(Rq4O yֱ]ޱ+\]"79=w1sm=.q,VP IEG3"S@uz%<8{YwFvYV_&S÷ϼ;DbG`סxJ4"gA87%Nԡh)6c2:n dvM&.?p|N8A&[/0u(/ED΂Me!yCP@]8`֓BɕÉAHgHh\6Kv.fih-QvdgSb7}1 <;Cfb2}Di.pU "): jot$4gfUu(/Sɘ; 1ո}38 fʈ$4UUB72caE doGqȑd-oKȳδkjEc2p\!tiG)XBDD 30>`ub]BwC0snp^wCDDD$1Lz+0`HV]`=#ֵ1KB}u1IԥpÌADDD"@:[}SE+B q]׍)G """[?'Fq{s80ch!QQ\;cpaƐ4TBDDNgt>){a+ϯ,C!fҬV!1Hlm #?K[Z=`D_± w`96?f IC""oŵIE*k7gaR>C^9b !L61|M ןP̊ ҷq!} -e o\~M"Uv(`&>`N9b)av="rdNdm7jČʲ}X!x ̋LQoDŽr$mIDAT |-dDJŁbKcT( }WϘ#(ZdPnhB8&0)ݗywzoUYL\i\-%hRL¼G'pXZi}Ga؃Gz|i+"n;z^8|;䧯dDz$zN}B}bQDz]2[`5 MuSZnZzCi6ZKYa~c M51'uJAC>˿iP 8 8GVXD}1]'-05Fe{a?2Rv 8ۃۗ[ k+㡲B,jMZ|C?TL5[MZu/]C-F{- "CK8YڗfRm]N'_mpk3Z8f۵49 AQ U(qbӠ7a֚/RZ\ZEMgykՊ3#8R@pD4}f%8T+)S%<>3#$-/]M񮭦'}&AG,<*p(0L1{-:!5#{\0-:iAdd-Z޽*;(91!/>~ !*QM<ĞKM-f0o$5R,@=5陁Jug~~8PG?.EMר%M> Zg/Ex1Z}ϟ3/zv >c؉'Gc'jѲgOd|a'?&Ѳg6/zܰGP]Y|U*=_uǢ F.] R.سnFe mKaۏvʘk=l-]Hovh;;@bo5|^s;LWfNLVs__ ISi?,-Z PewWQ.5h)vUqTH Z|C0h^BJ-vF-R*9cKCr Z夳 -17Ѳ}_&@R'ܦEKLֽ8gEyNS,ֽxE>t?O#wTTe|QXIK"sP3A@UZ%0Dz]J;֪*@ BVϝNY 9 tZ҂=Zkڳ\KEi1kA~%4 g%(4OsPrJA:Peu,: xɘNTܡI7({hQZG,UE["ۥǵ\^Z QVjIeLk,zϘۨY#Pt5Psd_vCpVk)ڡ{⏯]o JC?^JGm(߸˞&B޺jL+fAQ@ޣR˦J; /G@Zzy5~{~\] +x=^T)κ^lznǩ/= yx?)|,/,Ds=9BbWT_[Su/Kn׀ɽrwq/#5iyUœњwoX~R{zѫۨZ}U˾~~.e? f';6Z&]ԛOb׊/ ۔i٥-[afnVq/s':6Zr-_e%ic-u P+-v \ ؝1oWI,㹋RMZZco Pú 'Bj=~©ufϛoV2[orڊ?f}rwqkܝݛaV[!mKLɣV['*zE8zk jP]ʄ23գU3{zPQWM4ձm-=km?Lc_?d|a?ǞEkXav OѢ0{>fmzG ђƦr{!ċh꠸5rMZE]:c\-7|j;j"h3m߿e4\Qc 7mK6R|Mm}.|{;b wP_D%vOQVWA8H{XҤe(5i ;;ӌ@iY={qsY"0Zb~#Z:M!' 2ұ'mzE-cZD%{0޻wm^GKa$?}e-K]I @>{rս L/[Kco1=_`u, URPk;UoRJ;RK|M9%S+R8H5 MTBґ{6GL~w{}?od 8iNZ\]VC|E ;+>>ڷ;o?YƕKꠈ-t8(~v۴e&1@u#NE˶B+euty-]W̮ɣgT4/&BP*Z|5-A;8x!DrEOوUuP*x9TTJ_O8Ы^Rʄ@c8tkzd篇2"[[aWEKQN:+kv8HM|`u ZT%sb79rNj;tNƺK1z~G҉h36ٳ]ՓxyM>jKBqfEOAǑ0 8s/K5f-K%|Bs‰j49*qET|C&jE/`&-YΉJ8 {ϚbK;4Oђԃ2H-cGwЕZ7Iz"wų-OrPX[xoyu s4MZ|C$I*]NդE[sQ|YQκ6}M):jpSa>2Nr[G _ctӖ+up.C{w%S-CnfKjD^1b.ÌըE[ K f6|ƧOkzyI#.2-K:jOIv/:j'٢k~詸D[tmwo*z.7Z*r쀋[ <01t@O]WZhi7ҡEkB#l+K f oWƲ-1('u\IK[zOCPHҲ}AXCԝT)n-&:'\j.Ee R TC38iBLK$m'S3RZZp'W<7L}v-S/ZA"9^nZ֡nMEwWĵ򆯙*r Zhh[.$h1.gh;~_L7j:Mɮ_˵|‹Z$u9/|{ҟ=`jk[/yAD0SXJU`-Ajv*ծ?*6њMIgz&B[ U3E7kLDݙ{q& [CZ{sGUjb WjCDp +fzAPp=A 4h E='U*k;~ޓ T&]ZاHNF ;{ػł=JǓ. ;}UfZ$;N4,|ע3I8~ao^<ӢE*y՛Bzb4=XIo^^1]2vE1Sj^]t@; {__e\-ZZtCW}CƧҩ7X&'ؓDfi!=j/&VftfurRzhMY^ʍ锋 _i)ǽ FѠBu9)/.0=!-Jžş@y~8DKoT1-euGham3Z,a@䶛vMx.-]jt5*qM2{S, FQGP1s *QQvãqxmMWK`߷TZ(3Cj)-Ԣ%,GHX-g{JOڌ6fi%jŸە`şE]}T~\TşMWɛn(Emw/E(Ίr l%:ԫǿ_i9P ]Bt".AAh[ ƢPBb*ZOezOZ"^7w!fn`6*;Q8x\_C51$Ե8F~-Th*Uw<^:*#Q-a5n-^!bx^݂Iګw9 ġjM !Գkwv*@<p :5#BP?븖 vvF9+XE]܄ƱXYKsrsYWA8G5d.<]7P7iA˹h'* ]WǺyѬEBjq+P9^B8PNA Ѩ ;3Q7cW) >bĒjs]j>t{Cݠe6]|pToq6Qbnzmi6 .Զ5 W>.'b4FO#@ u7R@8xnQ #Ujk-4vi!c%`%zIp|lijVE&R]a&㰧U^_xP;=QG+'Η>o5~uMgY{E|ER P/nxf-M0S5k dFcgiw/TTɃCtN wҰjPAqϷ8C=|…} <GhEB%p*UӵA(,DDɸՒ>¼'YXZ]ſ]"E8` ~Ө:@= Np1%ݴx 6/B=*Aݜ >cżvޣYK2 ԬEL:&cKk53wlwÜm:Pa0 ַ$ЄHB5qj:u  :PxAQHdz~G]Cӑ ǼOuլEL^>Bu*9؀fߧbx#w$֠lԸ [ ,\[Tw]¸8[A7P$e~ 6.|ָBcn<4奨B5aAwbprwEV' ؉* oE@yѪHC̎3p0:|Ҭ3|@ՏߨWMʦF^7ƏQBUBQ0ɇ?x6㫚uǁh%PѨHtT=3z^ƼK5[zZa)J.:?P-_9zLL6>[5Z%E%R@(Q?=fqѿPm:"x0TWTHFMujWk<ⴰ5*A7N1|ZZj۸^E#&(dUZ&pl3AaVEM~ԱN+ǯp9QQo^h_{n' Ql 4}3+P|k]z4Iݟ%NAT=!ժ(0).+oHtެ@r/q4ɥ!9{Hx6(cR\ԱU#Lk}WhU8Et\3wiU?܃yYKS*PM}ռT oIZjUYOs5oPmn!Bi LG5gp'5SE" 2gZ.zhҔVX:-D(\I[)/ԊU:CRZ% 'Vz4ϥ^L 3bL\ Ρ%L~xh֖ I^WVEBS#RE+"Tr)ALB19H3R߿HBoGZ B9VEBS"ܑvmi \:T4Z6u fGҶ_^lT-A0m?mu󼩮Z[o%<} B}gU[D`=꼙[H3p 4vk)0! ;ʻe}}SP3Y?($ϯĠZMT69Ƃ Btu<WOID^uZYKA+੭EhA(Ѩ:JMkTa5dsa)fn'c͹XcKi n'& Xp:$h^7w80͝4U:Z7SuT 'A"{9_tsZA/k#IsTAhJ\9.(3"-+0hU:FE Tx{oᓵ*ob~4k'a1p\NrT  3Ղ T "\YKSd*h  `&A 0Yo(ggcPzi⯄êyo0#GG_ A<``'}i6C} us$twAn-B04kݨ+KGAkF]܁yS VZ{LTDrA/p|&+9:E_*E`f-SY`h U]>kL8O@  |+G 'Q[4k$#~%n'Q[' i:W859~FE5=)j+xCը@#*r`+f-:* a=MOG_lp{?(@ TaΦ@[T@MSu\-u} ?AW('s [Y [(W B#^?8Lt&??]v/8( O[FDDGxxsM`Z'4Z".呛k<R15 8ĵ]vhBFRZZ֭[oCMLU;qVb<ZpI'q-0tP@HWdggdح{ !E:?WqKqVF˔jgYɞPV:xmY)Qa1t#ظQo7y@pD41]ef8{*EDu,g xbFݠ yOn]t%&95q*!J޷z+w>E pW;*r=ʄ5@jj*sƍ#2R 0Ú5kJNtbz|: ?WœtZu _xwdލp`AD|'Zv>(ݿٳg7uѷo_JK EX39Z0|p/^Ltt.w|rMʕ+j\ffwp-駟f#$VչY*:6Z5y;\ CΤөx-qY ;*/ΰ嬸 :xcI(S32G]ElTӊ :Kxg]BF Ts;)L4;SN9իW: G:Ή .g}A$y:㻖N>\f_s iJFs [wIGP=/m,J}3z& 3߳gvkA٪;ߏLd?uгgOM3s4cP էFׂO>>( Gq˖-cԩۮŝPcRW} D,VPP/3f >%?m;0{w8f9Q^\fvD|gF\dFRQ;I=6~|̵r6Dю {Ӱ:ؾ|Kpp0fͲ.j ܮI;Ps 焕$kwvݎt./7E .W^ >EOvd|gG\h['\P}t(\gɣ9' s nXsYQN{O=$5G1_K wum"xgaE7T^kȑ#YhQ@8'\3aZ#(ǻk /[AYAڻwSuV &e̵U|Le"u-y떱kp$;2½d,zܰ#%8<>޻v:_'ȑ#3C9Sű(x(kSM:0|LII { ` K;AK/I-A|Ndz9ӰN0[ ڰCtR.+KOvTB*,WAw}lT@;U߈o5R_?֧1-s̘1̟??D W_eժU_޵TK#pƯս{w^_iA0y21ӯ՞(ڹo^5= mtV6߼~D"SF$@;q?xD'[4%}QK@Y`\y啺xsr 7 D|ZOPWN _&$=#Vblhh0l6qKټIx[mr7eoQVGƧf骨$7q_L#W\qsaƍEOV3oyʖumu2N=Cj!tt@ et箻uֺeM60!s`5Jg27&;xgOvTB7cۆmJ[lg}E?hsP3OMc3vX [?7|YEyVgL(pAUt-:uj_B e<Ѱeڶ:CW}k eL~v2Zgҥl߮Ill,-s9wv-:?J8!CЯ_?ZAh8+IFh{ESZ`IUxm]a' 6)ݷ-Nt^t8l>6sMJ32l[]}@pp0Cmvut¨QXpk9g,:,+PFkFY)i?`ء-c|hvavae4k˜8q"s-& S%T* xH=d:Yvʘkm6y$1,+% ;eR764|N=>j؝Nv)b~DRRIIIe4{;<;p8Gpki۶m%R#1euDՄ21i-*'I`P,6#FeUASVǖO5쨄$?ٶN{"uo^h&2zB"[k٬HZ0;_WCӖNNo79-[Z߿?wT ejPƉ}r(_y״SPXX{֩EfȦf;gflחYawq1vllaԛG#{NPh8]ƻ98|][ Y7ѢLGa]t(EkIϞ=A%߽PƢ}]PF~3Ao]t.-v=A_ 7㻆v)}a2N#|7-|?>evǓ/%"a E,1xmpP$H.  -XR&t'a:("nS4xHmͿy=3)ڱٰy}e59?oG!M)^_x(miDx+̶ x- Z%.اAh2莽 {Q/װ#v" ;o2rζ //)ddzmwy9!ѭ ;mN6Eml[= AyyyVs. cCi^EHT+- 7w ػ(/(pY A/`My,:)g\MXT06}'p[d76qan@ozFRgY)iq!մSо}j[O$Ih Yڻw,+5C{0vڢ g4사lR'iϿ~҈lu6-|DA5Av hmmb~|vpD h{öԳ (DtV"QCtRX$œQ Hza] A4999VsOM >m۶MA!Q1$ְvduRϾ6Io$;C {mȣp)mo]a;BB:ތ(ݷ-F AHxAal|nH@qh|ag|<; ;23/0u dURViH>>s>~ev£ IDATrP+WtlxHm$7ɖyNdO0SAUv{*Hʘkj;>C ;(Io7Yba?$B3'<6'^ld% } R^\@Ƣ'lHuabNﳥ9'Qi>D"k 0'J xj*kZW)F~پ}{m x`Ͱˋ^[+yL{{w&vO} jEh32pFvx=f/_n5k(vZ},KN瑗h @at;/tGD2t0B7:V{*[hdӰ3>yVS(BQcY{7~YFdExlO-> @~ v)GRpzp:)/.`G8䂇}tyۗ}hC5sq V%q[NhSfѢE["##>}:aaab}_̟?YfY ͉xg35jxщxgr6O~dwOC*dv%ۖCSHRWyʑJ}N4eVZŒ%K^GL2N:s~mX Ap;(OFff&;v@A9gƂb6-|SM~3ؾJrUǡ pãIHٹr~вK?2d Bׯ>gE9I{ϨTbʔ)b[xٳִ;w`=:>"Y:~HI(~;$u3r~^@HRz-q`:G[T0μ%%?mK͚]c|ETTN B%-`3k ްKTMYڎZSA>pXlY"v-rM7s`ɒ%7W y衇#2XRnz) / $e{˶uw[x{g֞׉[osTÇ[ϸ;pQ<>r-|'Xz rMGPIڻӸ16Ȱ.yYަO԰W2 RRȦf4tyavlE;˗/w k|u =z9Ow2˗s"B3qt;ܰ,kD•jP b킶 RQjqnsmb9?KA9Mp-XGxhKȖ-[:t(כ]W%KpB9XER.b8 / ik Aܿ_/\ uPb6/|ԶNN#".ɰ7} %pLׁ;bao_6 8KFn6_/ɵ^k._70rHC|AK勶}t%Y%43i٥aolm梩`~'GPQs: ~N8 U8쳭T9B`*طoÇgZE ۢϰtsR_KYYYYqnԃBwmEHT { Pi%liަ|gO[ATBWoYMJGJ"%s ŻR2#TdOetkՎ=.Y^FaZxG;~_l|ɓ'ӣS_())aܸqeLjT*9|w?H<ҥK5jw.[lFGݻŋ3p@ 2gvTbG}?i3u "/Xt̽'USeAhz|;hi?࣍/jTG_|"k;@g_H;NKmX |d$gpVA!r[fp)S8,e M;IR$٘v mWR@=![%j\8s$paHځ7{Bs wBZPVT\\LD$~vckp"0|FǝCeر 8S#=oj 'pCM5u3f`Is p .~_VB-k/N:"+V!B D3qD DuunJyGSaȑkғ`ѢE_-S??uK,i?~ѕt)99wuWu7l'?}P2KʶG'ѣG3gvu׮!Hm4٨0l[c{ko5vjuM<ӧꫯ&]Wwmg.Bfg3f ˖-cҤI?Fڶ3Z)spBdAr9%g.a·J7^]lԩ7i(hpbv⮻*Duu5/8Z'/͠O>̘1. T8'K,:x+0C7$ګ1|_dڴiL."_^T|h58Q4h7n۶m0 ~ 4*?Ñ 'ec T!dS-*`!7zv6l>W\qEu00 -weƏbH]믿e˖rʤ{ au!{bpTҸK8rX^f)UcIO l%\B(u@UU?O7n\Rqj2rH^cCRcƌnr0'7Io|)#Ӂ S_x*ȱ=˹8Bj._V2KҌ'6`fϞG%lp wqIn+@t!X*SG t˻>,>:7[8ø~p"qNזUjNL^z)UUқAdgD< < 0`SLI;fjjj?~N$&Mħ>ۄM/&~_fw.ZRũ"0 e >Da2.^*/W^y%-b֭iw]QujjjMr4*%_!:S Ls1{BjN}UуKF=z4#GOS%7*Æ cĈŎOL' D@Ȥ";`xԞCT̤IYU>V,jfgR E N}*җy008,=cTjx 2` a*vꩧQd*!C~T9N25v")q HR9 P4Lr!ŌIڵބՉG.3vظهXj::ӌC=dOTRF.dWY$I)pTqFrD5082dHQD4?j/ JԐ],j%ⵤԣGw߸kpK~ӌCLN}*>9hJZMV Jm9 3f.Aiš,U zdvT :TMXܧI>R訽gIxSA$P χ2sdOJ%*MuK;JJɄDn(Ľd/%d+nli?$u^vtLWk~ xÒrχqϦMKy sW[(!YR)Y:e޺7X$gR:h3%{/n=kx;VJ6Bb?}${H dVH*)k׮j4y>$l}LaLGu+$۵q믿^Ԁ$5x+jگE^CRIYreܬki?UICRJ^DOQP83QC()$+q_,vLN|8*g۽kK*IoviŢ,M?χ4#SF`lٲdtSϓ#IEw;Ox=LeJZ·1@!)MS7r]̏ڹKJ*)|۶+d(&=/_TrS?󸩌RYٺu+rKuwZRHԕ}ywVk6H%3f$0ꫯ.ntR%ZF멌a*:͛93Yn]ڡ0tPZv(E5k,y睸kFKrڎ8ҍH ބDnm> M/GI; z5\{v(EUWWwgݣ4$J} ছn.cv*nR%1QE6m4nw,W_};b>o⮗Vv/8@SO0nܸCܭ~2qaʫ=SN흋䩧gk{2qUWiSz 4ް]l^ӧ/:h6Q;w?aa4`>O~C)3g|d {=ДrWT^G} 3jLTt ^{WL7|k6 xgrK.O>Ç+Nrʨ"KYvm+i_Z&,\O7Zb2 Խ\| py1z芛,uQ$``Ԯ-B\*K7o /N6?(z|୷b„ ,XC-_iӦ1k֬7SI:g75/OHQ/=uTFuɓ'K8mpV'K2qD}ݖ*A {ygS Im{FҨ^z)Ő2ԝDߓYW=!uдix! _~t1a„nSij3zL<9z 8?pLҘ?>cǎ' @5QE2g͛wM#{4C8m*sqQWW^Th͚5q\wuqk)ܘ4^}U`=avz! [l-a4`ҤIq0wܕjEDӢ*ܤL2W;۰aկr +6[jB$)-MfJ;.&p"_M;IR\A vځt-?7{G+#6nyAMIRx80bm&L:hʱt{iV W*=$ilN%gZA73c}Lځt p紴C!RY>!URL[JYaЍ N;NPO2PZK·\Lʯ'F̦3{iQ)?D'y?W0$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$IRwWvek9}/?0pg H$I=wPHa$paN$u[l|PT?vm_T!$IRo6yHi2a@5Ti@$I%`X?಴*$Iv00< 4R6W=p6ewT$IRO)X75)~PRDTC9?*6 $u6n ]1{G퇁w0pb্gC x'Gōqp27.y)`=U~ H{/P墇%h_`XBXj%[s ֵ5FaKts 1!P_T#R.'3+"xܴ\?̉!:fnHĿif'' n7 :+>qrRUv{4?vwزgLIRW%ܙh19na 1&@ko BnT9g*'Rq% GOÑ*A2z^@&r pX}l7nƴ\;ȉacmO^R 8>HTb:Xi}ǟE=T$ueQ;73!.ۿk8n<ϴ-Մpл1ď'pJc"S ~R)~}wfIT$uvmƃ?u#Έ'd _'i^TK*mf %GUN' L0s=`Ñ*0>P$+n$ŭzxpm0P$g`ih+-$Gz.!LJQoBQdɇ'}HrBՖ{1+? aQd5"#v:]bt ՗LEd2P IWճ+a}R ;]HeI] IRW+(2/-OO&b3p}VO\G)Äh;d.+⤱^7cNI1I$8F۵-\$Ok"HJSo{ѣSR8 aaF"2ɬq,PZ!a L|ϵxq{3, p&QRwICgZ[R18@y'/zmա Mu(9.oa+1,$3՗u(=`B{,6Ek1Hċ&K5J bU ;ӍFR(:<}^@I#,h>U"^#h68b3a:FKz4?x =.o%| >s,"n!= =(p$<.Ij?_aa]XXFqaZ?/֞+7%['TIvij;Rwq05P]\%WN<K 4ʐJ!ŀ\o"{=6n֡ho88c"~9ǭ'ScBcA*kz \J,R~EXWnewIEY^E2Β(JQ\'=:R2+fĒ)5_8YX@ ȮCqdvo2'$~{` Uߒm IԝgyP_iLzc:zeΓ{׋'8ţO" -gAlmGjo|%,O_I1l$L3i H RX ܓ,R)ZW(SrHt)q tz`9}7_m8NX%i`"=/ףC2Iԩ{uq 5pR?}{]@ Rwdyupv2crg{}aϞo8vch?R ?[Z |q2J{^c:{eegۻ@=s@%i #r^ԽmnFF}xWFO$F}3 BFk·Z>T 3($ImăB%@u0_:ɛ ޠPH$)sfϢkO?~z̺$\@9&ՈUI5"I*/E2>ZtRqH\fT$ImXvRO;9ţVmJ# 4 0- "w)i"Uc#&qIENDB`pairtools-0.3.0/doc/_static/terminology.png000066400000000000000000001007201345765554600210230ustar00rootroot00000000000000PNG  IHDR(sBIT|d pHYs.$.$* tEXtSoftwarewww.inkscape.org< IDATxuUHx qu!aq]` / _'5JB L2q;3]=3ԓNUi:u=DDD$Ӿ|i8LLEjiOg)i_ ˠR%DDDD}@> .\`8pP ~Òuizv? 6f =E kX5=)zɜ6 l]]0:wk50 uWs)|AXVw;`Y mGy8յ=EHi<,)#)<'<"iMeiql <l>DzlLRH|OhHK)6M$eDZ60`Ez̹w8 (%xtMhl6IUx {1MN1a-&;a _&aQ$y?a _.rd8Eb-| 3w/θiFhL7xNrUUDD$[N1x ;9u3pkϗnB&qXqX2kjG{Udn lݵD$ MbwX՝X@,LU s%"0 Kbb:7\/vݷxZ )os yWR& [E"""֮Rd'_b-.dmWa'y ű=-+ޥ++)ǵ1JjqI8ߗEw=X"vdOްmb*ϰoV+öJISl x`1NX6bT:EL4q|۵] E UqkMX+@#]&_ 9)fVa$3BDD*=#Ǧx.Mpq'.\A '"QUZ>u{607  6FrxBOBǞ^` )QDD$]'no&'_i'N 6c@$pn]]xJ9XAd6 m&ـLnu  ZaHl`c',}mNvƦޘuEJsMX6ԋ]7(D$3UpY|%aZR>NbDJhHK'_Yê_$ I2Ů}q\G̒ʀ( -$iMO2α.l}#q]zEk 1cUڮUUDD$܅aI_Im` ߇XpW"x'M$דj^ZH ,|/ %;̶79u]ؼGĔ`ㄎĺv 8O9 wj}]uUu{MqO 6׾1lV5څ2+30x#X%Ve5S%EDD$T]`8%6a}lV $9g޲G Đ&鏡ve})>Vb qMh%0;)k؂uŝU6 뮚JߕcAD,<Ϳ^d8Ǫ \Z퉧x{.֋W,zk}l9Bō5p6> ?>й&cУX""""""""""""""""""""""""""""""""""""""""""rCDD*u l7<`Sn|¸DcS kMS |p;0 `+`+MM1mccK|Md/nx:Ӱ9/EDD8&\6OG*^8K7s9Di2دy)v]=Hck%w D6N>dc;`u~a'pd_2TDDDDKc'XcXM~`wK^Ijo׽:p8l @m6D-v*"""Uѽ@1 vE> aÁDkChͦ Ķ Kja-L]wdZGGYtw."c_d .8He 聝xŪOTENGw+cc TCld)w(> Ǘ~w;64Xx$NGm%;#=,"""IWU l haZ7LBmWн:{@rƐĊ?l;;^K:EQ={ݓh\}Nsmك8ILBD4MߋNF8Heq :SU[u;o8fǰRb*aZ?Z& 6BmNs޿<$urVNy3PU v_>P]ԘWd&L-֝+j.9p7lD&9h _`Ķ8¾kIs*iHl\⑃ʕ Z$r\y3Z8Hp%Z]bM5>ȴ7`x|JS/{b*cX+)p6 VhϢDJ/aT% ?cJ 5"m[\ە`]K^&/^D,'w vxdb-@gcpzP̥XumE*ee,`2;WJ ͫXOa)kF/M^S|@uާב>WіT^bL8$;J&]k9ʄ;%C'=u6\?T(VT# yx<_?%rL\` p6=Ǫ-5q>7?/N XG[y8\zdp*^Ŧ8̵*Zo`i6_PwOB:@Nu_NTm y |._J%KiwU?__[}CvSXqc1+X?Sr{k׼L}x˼^Ϛcus4yPweW]UE$.Z^NknoO񱫪\GyE7@Kn4XnןXn<;wf<kkEXÛ5}ҽACJ}>2U^һ_B=S&b6Oba &ϺۛXc1'I#'I#@3,i\ix4`˝4G;u/gX aQ6@Gg2-󽰮~qwSaEPKX K~&o™v" *VHg.| \ %5Us`k{^uYK L26㽍7Ggh)l?-sЖp.c_Y;u6qGF3)gBRZ56( tʠ|2իc),yy7hh0bn4!hTϵXְkQ[yn=Ф7Nf*{,zΧ*.Mgه[|6-M,s~d{כ=VHvr6ⷜ4͆@$- h&͑y㐊D,{?`Q@>6.4sd {b l; k8Ʀ!Jbq0~e0%g-%TG9m8-vŵ9}N :]Bgm}`TbӽL,fK}IwUuajn4>UwrY֭?cٮϓX&_5XnӖ'{%5oU?}n"}d}7-3ZACuU,RsMbth7)s">p"wn^5r1e{v?:GFn~kJ7mӶ'nxjh*7vݴ>=,53KqZ;µnݕ~7E^ZqLLXW" \-RٌNssEt5@ҸKJwъUeNB;ax;b'CV@w8ρ@KlLZ88 KBIc1&O_]'D*9[sn~)tX܃}%{t+BHa-bwz9l6a[.b< s,n`xѸ/R+[.NA޷/#LԴt\ฺLؤt\l2YȓX4j̥ݝY}>_1xKFիxmAEUiϮ}Y0i^=F=IҸhG_LkBK$i,-їju=IKK1vz\6ړtw1=*4תO˞LYX^*#pl:Xw˷+)M~翽ku|K0Z;.*X}X+sX5VGb1=qFxYX46u`-Nr$ªf]G!۩Jw^/6c4ɾG=5n.>?ye_¸L,:枼EЇ]; &A`U:qkwnitW?8.۷  <<׭Iឳte^a!_GfNCz9Sn'=O̚Azσ=t'̢S pd /\ǎ+cToco`yXz RS)Y_mˬ]Nn/yZz˂WogN,&xl]?ɥۅfvZ2i Ǻo#NkaؘA4NǙ  DMNHZ/_Ǧ_KqAh҈qFaAN8 yDJZhtO.+A8-c.ђi 0Pm36 op?㴄v>UJnNw DI\\gԳb9{n]{p_ ak aYU泧B М~;DՒ^{9ftI,+/k~|3<+@WX'\CއxK4jq`am}7a]_Ǿ/b'z7 K4a=, 8v IDAT51y NmܗwА/C&=r7,+hI9 `%9ֿؘ&X@VW2v~54Kw>y",Z=5JJذ <:lxxrwƢ"n48( \W >`je4y;h~5{~@ d])[qh⌫w$Vt.U Ou0U]-+@CCO!x_#9bh74mЛXMe;^I,(qd7S DRg1eXa8Lg1F?_;):I6`2c!s.ڒǝ\1{EsF` 2մp+RyOZ7}v@:uO?b S֯s5/SS?O(32Kundgm3WҢf-no O yy| j^ȵ`B9/a//!zx43"UMBǷDNo~2RVx%y:鷧Iٵ~yH,uWzm\ŜgXɾ>g4Ųy G__vntCዂ`pf?y9P]vÞGLbRb󹟏3@"ݍ؉ oc쏛{|+HqXf[e8.Z:c]zHw\!V&[=+NT 莽O`Xx l?KtnԋU%8G)5mY[tG/xPŋB*͎ޓMI?]w,_е;[yvcxV1HH ꎿqCq+EG_'eĭ# fϟ譿R}w5>y0:d|cgeK?xzz2S(?5?`OP~44<¹Yu0[287HHuwI|z"\c '`s=M{DZ0O)E& Т6e +TG`-c[< [Xf*oKpD?GnJ$5yZt.8M[N)3cFϞ\?>ѩ'ܴfMoъ:I,7miOZT*4O\ܴyT*pw'm_#1?7QgSg~甜7jon˝̴fN,9С75={&=ϥHGwWWI { WWjp\]c~Opm;Obim:t}p}X RS:rSp}XGj`ZbU"M{^> :NK'9qIbj@r=VЛMߣD}OhqIl'DID8f>Z\︼G/*d,]\[z˧˗^9Uy:G+Bgh]rX6"٫?Ө%c>펻& pdObKi:pDObis44"h}l;ޓXZv>-8%qַ1%R\wPuom$I$swLBl+ؕ[YS˱|TGɈzLx/=دkul,kng_ ȶ4 -@sGE_͠RU&i _X4i%)@N..|PZR'/"spO]0pWRɛXpG+/My<[!tl!.GcW"uY Nx'ca%݀3\'pXvNy s*= p;#qG/|`v.:EZ/e躿vx1JIhŒ=hiYw֒,6EZ*#1תO?=K^],ڸ9\4SpOdVNឭUp~ᤢ;x`dĒ؜{_KbU7acjcn-@+,9U%c.] 898Ϳ$n:Ͽ/t_ӱ֮ΡX+xNyGeSQ?(6$t|~a^%уX2U%a-*&9)g-X"wķ@qt*cn%"}֣iu箆>Zh _ܭm$>nwi)͝Ê6Iͼ|.ѓF52?1̝m8]#/Q{Ҵ'0Wl~Z,jf>ReB,\ҽ=4. ml~n.wA牔:1"JRvsL<&R%Xg imp&IƲd+>{ډ4?TObپb> >Hհǁ~F˯,X;VM,+/2VWcΛȪ_ xe~ D依1Nľ|݉%p{&F|9Xx1=%.$Qi0N6!t, O##V"ك^ys0zDZT5$pNP\r.]'t.Olr8΅5k$O#bnpޗRlxBS/q fӹ3+"P.(IoXފ]8L*Jh֧D.H6#mZwj#^/BbZ`$`VHf$8ޗ)o۱EHvjMoK$]a'bS]KyEDDLU&;!/y ^ęT$4J wشYSlGj"""iw?6@cA*?S\E26;0&I^-{ZTrmQXahM*@%-MX H3q>{3nhRq&}+<|>o."""ZyIi4RR=JWXB¥H,y5N@]EDDB^ iъTٲԉp8ӻL.8hnE* JشG"闇0Kώݱ[H 6DW,a'ߣ7U-vCnW80ȅm6c]S{dZ=`,e/4"VXPyt2(J_^% iE)3bH$PG;ZQM1A,췥A 9ؼ9 ] 39sHEp $o ӈk9ؼ}[q>NDTZw|Vc]Z%sj#8őƼH-pQiDXqg;TKE.1#NmˀDxgщHuC܃0<Hհ?r1pIeq*TJ*D'QH3؈s-ӈ$SK} hi^:J'ȭˁ'QIX5 ކ#t0pAHs"*&{ǿrDDD$C |jBk9+ƺI_|P8p9lj?bcEDDD<*>4`3VHCKrӈ8xD'JEDD$KnmRB + fﱊ%X^("""Rp]q,߉Z%{ƒEw8ˠҠ8{@%9""""5 STV*;܊ kd~B1T`YA肎TpNx=H"i|>bJKŒB&l\JTcp.3JGH 8 *N4"q;X,*4T$- mMxmj.MMv$NBV>6NE؉ix7^%RNq>;Fda-`߻Rwdhe@U2a-6HePPUBp0ca e}o^'O+,ɜWp^DDƺfJmlDN`]jOE]PEDDD*MR) 9XR_a7H:=O@?Hq1}6J)ɱKUgߛ$u)68_)"""R)MׁTB3D$KIwcا)5Ēj)WDDDD\bW%5r+>`#(P KbKºwA-D H8%jweP"Y3?BJ"ـ񊈈H)XKT`=Ήvq _cSjdB[6b{Kd)'c."ݷDNz$";rqEN4"Q8 x8&_uM}8>$t:VDDDDn$ De௄heP"Y#p% |mk\[CcEDDD*/8'Q|y=c''"UAp 0Na|T(38'%XI,q^Q zbŠ9Y\p8T,"""řG+:<މ*=JR^މnFKtv#8""""RN pN}<(cTf5o.}/21fEDDD$I ӈR5pAdPs-` 6bk`c3ZP.p PHhDNtj$DDjU$m 4V Z!8To_ G$mߪL"]\nL_ƥ8p46Ie0wG 8HEV]$TAD f'`VuIBUb`.""م@%""XL:Nޠr%`==it8NQ x8Mgv5DZHr™sKcfa] w"v_)0XGxehui &R yD~ u_$.;; \ao+؅W!{"]WgMık9(qu^RU)ʗ85;Q۰5&>lDYc3j}Ú,f[ /,d|p|ԩV-j v{f-X9X vur`!MY97P+?mۓ~Ɯ͛~]`u-*/D`AѧX/dĦku%UAyع`o"JXBӿ>^z$NxͻPsQc)𬇱TE$3wyGU5<{_*e!\;*IAM.aS5Q'f, l]6'w .޺k'{|>6q]_RZX}s 7qǀA!֒b>Zfm[~k$_\?[mwS3?<^[8ߝ8Tjm'^cNhsGQ5W)sȯUA~E: ,|V~zQCf\o}t:ָc#do}Ѽ|;j8E. \+FQy `4 x{CЕL녕t%͹@m{ %s~gQTlTx.juLÝҡm ?,;?[v߮^u׫PEE4nc+$|ttJ}ۄ$.qTY-NvK?|µMdž_>ۃwЎ|F\H~W0h@>$}er$ekI|n5؄99X5IyK4WArsSOtKq18 IDATk6^uã3gǕܲ6Y۶ըt +l Wrslq={F9H%k;gv'~]IO7(ޠ4wTp}}Ŝz~;"uڥtot ߸ # {.Ԅ {>ǹuC{8ni3gT-#2R?WDq^bm()8( J] uןuJ}7ึ_ZTuC^4Dzuz^Amڻ]떅t=tVs|K Ϡv֥UDBuzi[Z\F8F\ NձGDӰ74:Z*V|?Wq;:~U_Ըإg뾞㻗;uĺ>Օ@(&/):n@6} I^C;ht?b! ߝ`WUC {Of`䞥lYGDhT>?`Xm8N^`) l?Дka{>x&cɔ80 ptۥsb A:u9Cuk7z^NWrN@¼9!Ǹ[O껺>6k:KKhw,kXFH-|tIH%VQKqٲh k'}lK3\]Zv݇B?Q\8^JwwH߿֥N:;uX ~nIx)6Dlv>;&#7!X2ޥ؉/M7,ok`MN>wc'Ki῿[bs*{(߀}hb3wX&/Ӱ@XmL'XQ? ?vb'˱X%I’Wl K2/` k"̀< ԟII 8W¾#<!)񨁽w5Y W,ڸIL(u@R|L`BWPA|22 nI-n%8g16pr ]IVĩ~-qVhF>vmk邝ODHD5Zaݨ\ڦX N5G`kcX8Iǰ>Hk/XX~7;)kp+O0N7aD`s? $˝OLb7wҸ{ޓi8[Y5,&Ih?ykaK,ig)N&K^ XK$.9XDj-k'EmDO.IaZ9+ v#O$K1u$NCM( v~5.tX}o-^L$|<֥CZ]]Zm ]]Z(#"j?ѹWi +L6NI$}>+b|I[,m\\Ѩ%mtoY3~v]ɥw,֓fslXbvk8v q,lu{`FSnt$NW W'r$֕~V,g_l,Tf<m閮cƮZac^NǒA؉ms&bކM^]8{/|k}{ͻu<@#B[v&X; 4kY<~ mP]ӕyuZO>dlpv4+X;{oyXQQԵgh0v9<`]eZOo3̿Ok,i @XDjY"={b1}gf%u{'m+u>LD`c{Ӱ߱ag }$vw6dGhcDtm 9Hֵk ~-?qqpU/%ckI ͝]{viڥ5^ lw\_Sڰ"^A \ܲh*k'ۂ_iNwi=2j4r/{Hztqb=R%{g&gujq8@)xBbmh(Phq/?J Vܝ !F{ M̽dٝyy;ye{L 3_'yZ̀9ۖn6>W@` m"lk=w9AC'\n`{lp}>.5o?n  3cVEXh27agUOpĭ1NF?N¾؀9603s\{wUv+s!.?/e[!7sB 0b2ƞ7띰0=T'^SnŒ+YM2\ dt& =%v'[aEKg2UXyNTǎ7Ѷ%x7bw>Gc^k٘%ЉO ycnHFx˹9j).*;RWWsߔwCE:i<=oTJ5YX߭8{Ӝ[=3z6zAgvs>Ҽcf j;l+gX=zqy}vY8W,b^g{-zVdяߋ ȓ ē.4qg2DP}e;Ƅd2=nL6"r_0ѐwM8N2DfOgŘ yrr5V]YU D)&c-Yhp\HbC$`ȧO ᎍC=5J߄"l2*UuA#ۧlxEZ3CG=9|͓(-`:WpȸW$}ߦm|)-[ql!DVeW{}/z *.kN.o\ ^+p@xHQs|}ws++{[;Ef=&Z|]J~rru)=|ae%cA,SM/l{Id +B6MC"ǒ:/݊{|ROq[gvl9>([FԗdHO aZ4, t><1g/ن/ñ e+ yI ?z3xpW9ï[e5|l)".TZUWsAֳBfγy/M!D 7?1Dj]PI·v'6,qARm*y,vD,*:짃6=9Ӭ' x6Vh8b&Bnj o5DZn CD]rۦl 2_-'!ko ?npVƗB7pwa%!Jo9׆'0̛]W*1ᪧ >/ ǰl*U Dp StFsW W=oy甭N_DRB0dPCg>6)[5~r\ntN8|Š弹k+CZ "8}@Hm&ԩ}:9zrJYnTU!RQֺc@ u|'m{B՛t0APRz/T9)Vnח|ĭz˞N ٧/kU\ZNc]ُMk1`Vc~EI0*ac! i}@Hت α؀6V%6*Tu u od3xNHCd{W-yB?'&$o8˃}4x܄v)[ՍȽsabP8CE2?x+a6=qϩ װcKBP:,_BPN]W; n.l^BP<1.S\Bl=ŷ\G~J6FQDj.yKg}66^_).sR|]{D Mr㜏 Φ!9Vq[R>ꢁy +caU$xWDzV3e.|-y1阡IZk>.raa¤nj>+$GRO l2baaI]Tehs:pq}a\ NH=gm1d ঘsk"oZ'0Z8m;< &O7\O:q ޘƇ >.DӦfzf=BPwIg<>MѠb A]|;>BۺH KFGjk,1JiN0ib\-UP_?+>ԩѥ….ٰӧׇ=@tm$-'W$ԂBlo ۀ'pO<'ҺzMZ}/d&v(6Kf}]}̘퍍UIPdm598;3Ō\3NKf p2[M !8"JU;rcgufgؤ}qva`K0ћX$P{d_2 0)Rb\1,d8e1y'\k^A=z1sVpWwDsxjqQ!9L^2bN=79!DO`McBP[KǘlZ{K)|ݷW3Do IDAT_͜o:n_>ګY@kȏ WayWenr՘WhoKk37E*ͬm6>pzRDpycaB[y}] m|&2nޱצk1uv wPT3]sO}:1o1qj,4s?–B@oƼD.\id6'e@̳/l5 Ԋݱg +r9*S2/ X4X\lT7dn!v>as;Ha^M')3N">Dž{ٜOKאd]!ݘ<ǮH`iV,͐v#{8ó[@}[536͢we}6 KAi1^ڢ-ޘؼ e;JK`$GasgjqAaH^36/Y 2ǧP J1^'!\ &sMB&/W`F{C%1o|.k3gɔu'{]iܾawɾWce#) ؽ8Flc0Kӭk߂%cсW1G ڏQ9s'}࡜>hp|}Ɇ kT՘}Dܱ>ACm$¹ÔU6lQZ#MsD'ثHӸ}R<'k.v 52x=>~&sՔ 8yWF]E|kybVo=/ҲXeWm?7_~4>Dw8a?K]|gbR\O3@ąe 0Sív(ÓTc 7zyEaQV325l"qϯ01fe\- gK-ui8i|k m?q),l\c\F4yh;`4nm]E NTAuX\T sčF3  ֭.`TʴF:I=G BPUo>/{˖_0I\k EE\4\THǢqn}1a~to|=RSGlGuygr!YTlhesKxwCo{ ,M);g#~I!%f?{SZ1_}Tt_rxk|Etgx| س_ZF. lX2;K)H cLJ]('pzbyu܁m`d8F1#CMXR8T!['15ݽmX ps2Ae@;hiy˖p rѡRm+erI3,<ظ)1JSc.eX>.] b ʻo!"7+4J1A25lD$ٔ G!"bBO@)ats_:RoXw$\oˁ/N!BSw'H G->Ju_4k~5qߥ4{Zz)o~m t~Ǘv*pO<%UXyȂmڲ_׭;K?gE_-!D +uUaЄ(`d8m7qޟܗmkVfypl]8*}YphތyW+ ]G! Vrm 96_%[viG42E6BZ?/`s_*Ǿ46pϒ<%w ey싨'H_u`)oӉo82O-0{ev)ڌt= "yvs_p?ou^׺jMBT`?UtLiyoP\”F̤n0 2`qt=W'Py%݇R u-v/M7CS f7$i c9 Q4lX``J=.":K?|?5J.{·d݂MWc5Aqg,'m mWaFx=l1twt ;cρ1U1#g)*.ﱗ:c>B 2E6Tb~(w +x Y星v#}[%ߍ ۦ8Nr"ݽ+1U6Oc^d3v->vnÌdNaqZc`iڵű7awйU ј)W؀̛,,/xoM;1юd-趮$l@> Ax\MD̎~b`v&(>=Ga3rڗh ^E%&̀6m9{O^y;B }-+9"`_B@(rO=0#6cy? }wYl}<{?ѿ LJ\p*A<kX~a7;tbD|)H $9/xrbɸ 햡_ah,1VNBl"yyo+[Hv}?f%3`qo= phu _ϱdcބymw&Z&}" cܢTԀMB zG6܆&|DPUKi?}Cډo4h?4c|L0XL$cDyaFFdy%,"1˟/~>/c6p  M6c|M0_н Xv*#|L$o9]H(`:WTp\@4h,ZQ=RyG%y}yq?WB3 v3 6COfHZ`s/{˃27ܷD*&˟Hn4/> v+NlXWjکa}6SW42LfH{-(Bj Qp{[$/ %XUMi WOF Ir/Jl;YYCR i8nƔD3񌷜M(n )-/`s•Kep0,\&BR~4pi$&y?U_ h.n@K|CmRVA&fqcRш)6#s\"|Xyr3񍷜LnoGX8XPjAFC_N oyhV敽ˋ\S9?]MW!݇=S`kv'j&Բ1}ңet](XJ[F GK+C'qY@ BC C7ړh[~f ' ˱3b8gOXCkR+lo9u?;&X% U]ݧybLOXX2Vd7_t PZ\C;BtT*:7!s9%! "FF$e a=~ ךk|#5HbR'3rB ! Қ>a&V424.[Àq27aQ!s{h{?&s3&?;zHci]Vбs;'k'( JJ(kH]uyD8D. ܺT1"؏$+O[|]KvB}y3*dwÅ7mu7#+f5+[͖5YDZ|tFi؉dA]ј4 ه%hʂT4:57#YB(}537S!q9e<,e ÕKM7HBsvzbt͘2`_CtH][z+aFQ3Wɓ+A\1u,Vt\o96JoyzN{'Oenf ! Mk2M͢MOQ=>(r؏̞R,.~pgBsry[ޓpaze%&BYc~-Xc3Omwbt %hd?aiD2Z2B(3#=F6E6:-_ [AH_1-Խ%QqUm1tAf휲Qa+882CJxo#WB2ul-7t1g8[O<߉3xW ElFc`cM|mNWVGVL|oz˻MD lƛmSN۳%'QZ|11gy;`Y*~NxϘ+Dc\o0ˆ=z4+epb )¼Wؙ0T\!X,6Vң V"WCv{ ΀r7Y^7O"FpAQ@nl/Q?LJW+m$zT#lXüA5B.[oavBSo/g8dLK0ǍZ`c~ 0qE \+GB ޶K;Oǐܓ3}OڣIeb^aU;ac00W R$?-z!&i]gLC <'@Sm{$1;7Vo'baBe7QlKH22g H]n̟܋b 懊,551%=v7`' `}V`63Bʣm L  ?'isV\=baOcP=l&g0cY~N}(BdǼNӰ~o {wos<?>1yq=3x_cֿ Iy 3{kG܏1C+l~mC[u'0Z@0 4*N.oq_E#qۤ )Ag!DnW,x/opSiXLX9xS-n`nr}9ɈF\n3b(lh&]Shc.w 3t9޶>X=1q#{Un]g>#XƥC0f4Fk.{?oҴ˗ʼ@dSX)!D/#R9߈*rO܊Kij9m1gBtYUM02k>vI<"ƅ8mą%*Q&-#Zy|iMVXXXY`p%9 `1!Jx3I5>Xa\q .5C \3K|Ƽ]1v&P i.1-R5L`#VB@/K0#az ֝]nG}v<`c,{BjoK1Ö8ļ3bI0-\[ _~6`ϝ@Yփ3t-|l96u/Q}ayKM1¢VcFH$e_biR 鈭1~r{Hx#&B0Xyc =OQ!$L65=3]^Y BѴ J}Nƅaf=~ˏ͗P'ǩ_.ljXn+l_,'r v documentation" by default. #html_title = 'pairtools v0.0.1' # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # 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'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. #html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'pairtoolsdoc' # -- 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, 'pairtools.tex', 'pairtools Documentation', 'Mirny Lab', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- 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, 'pairtools', 'pairtools Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- 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, 'pairtools', 'pairtools Documentation', author, 'pairtools', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False pairtools-0.3.0/doc/formats.rst000066400000000000000000000212701345765554600165260ustar00rootroot00000000000000Formats for storing Hi-C pairs ============================== .pairs ------ `.pairs` is a simple tabular format for storing DNA contacts detected in a Hi-C experiment. The detailed `.pairs specification `_ is defined by the 4DN Consortium. The body of a .pairs contains a table with a variable number of fields separated by a "\\t" character (a horizontal tab). The .pairs specification fixes the content and the order of the first seven columns: ======== =========== =============================================================================== index name description ======== =========== =============================================================================== 1 read_id the ID of the read as defined in fastq files 2 chrom1 the chromosome of the alignment on side 1 3 pos1 the 1-based genomic position of the outer-most (5') mapped bp on side 1 4 chrom2 the chromosome of the alignment on side 2 5 pos2 the 1-based genomic position of the outer-most (5') mapped bp on side 2 6 strand1 the strand of the alignment on side 1 7 strand2 the strand of the alignment on side 2 ======== =========== =============================================================================== A .pairs file starts with a header, an arbitrary number of lines starting with a "#" character. By convention, the header lines have a format of "#field_name: field_value". The `.pairs specification `_ mandates a few standard header lines (e.g., column names, chromosome order, sorting order, etc), all of which are automatically filled in by `pairtools`. The entries of a .pairs file can be flipped and sorted. "Flipping" means that *the sides 1 and 2 do not correspond to side1 and side2 in sequencing data.* Instead, side1 is defined as the side with the alignment with a lower sorting index (using the lexographic order for chromosome names, followed by the numeric order for positions and the lexicographic order for pair types). This particular order of "flipping" is defined as "upper-triangular flipping", or "triu-flipping". Finally, pairs are *typically* block-sorted: i.e. first lexicographically by chrom1 and chrom2, then numerically by pos1 and pos2. Pairtools' flavor of .pairs --------------------------- .pairs files produced by `pairtools` extend .pairs format in a few ways. 1. `pairtools` store null/ambiguous/chimeric alignments as chrom='!', pos=0, strand='-'. #. `pairtools` store the header of the source .sam files in the '#samheader:' fields of the pairs header. When multiple .pairs files are merged, the respective '#samheader:' fields are checked for consistency and merged. #. Each pairtool applied to .pairs leaves a record in the '#samheader' fields (using a @PG sam tag), thus preserving the full history of data processing. #. `pairtools` append an extra column describing the type of a Hi-C pair: ======== =========== =============================================================================== index name description ======== =========== =============================================================================== 8 pair_type the type of a Hi-C pair ======== =========== =============================================================================== Pair types ---------- `pairtools` use a simple two-character notation to define all possible pair types, according to the quality of alignment of the two sides. The type of a pair can be defined unambiguously using the table below. To use this table, identify which side has an alignment of a "poorer" quality (unmapped < multimapped < unique alignment) and which side has a "better" alignment and find the corresponding row in the table. =============== ========= ================== ========= ================== ======================== ====== =========== . Less informative alignment More informative alignment . . . --------------- ---------------------------- ---------------------------- ------------------------ ------ ----------- >2 alignments Mapped Unique Mapped Unique Pair type Code Sidedness |check| |cross| |cross| |cross| |cross| walk-walk WW 0 [1]_ |cross| |cross| |cross| null NN 0 |cross| |cross| |cross| corrupt XX 0 [2]_ |cross| |cross| |check| |cross| null-multi NM 0 |check| |cross| |check| |check| null-rescued NR 1 [3]_ |cross| |cross| |check| |check| null-unique NU 1 |cross| |check| |cross| |check| |cross| multi-multi MM 0 |check| |check| |cross| |check| |check| multi-rescued MR 1 [3]_ |cross| |check| |cross| |check| |check| multi-unique MU 1 |check| |check| |check| |check| |check| rescued-unique RU 2 [3]_ |check| |check| |check| |check| |check| unique-rescued UR 2 [3]_ |cross| |check| |check| |check| |check| unique-unique UU 2 |cross| |check| |check| |check| |check| duplicate DD 2 [4]_ =============== ========= ================== ========= ================== ======================== ====== =========== .. [1] "walks", or, `C-walks `_ are Hi-C molecules formed via multiple ligation events which cannot be reported as a single pair. .. [2] "corrupt" pairs are those with technical issues - e.g. missing a FASTQ sequence/SAM entry from one side of the molecule. .. [2] "rescued" pairs have two non-overlapping alignments on one of the sides (referred below as the chimeric side/read), but the inner (3'-) one extends the only alignment on the other side (referred as the non-chimeric side/read). Such pairs form when one of the two ligated DNA fragments is shorter than the read length. In this case, one of the reads contains this short fragment entirely, together with the ligation junction and a chunk of the other DNA fragment (thus, this read ends up having two non-overlapping alignments). Following the procedure introduced in `HiC-Pro `_ and `Juicer `_, `pairtools parse` rescues such Hi-C molecules, reports the position of the 5' alignment on the chimeric side, and tags them as "NU", "MU", "UR" or "RU" pair type, depending on the type of the 5' alignment on the chimeric side. Such molecules can and should be used in downstream analysis. Read more on the rescue procedure in :doc:`the section on parsing `. .. [3] `pairtools dedup` detects molecules that could be formed via PCR duplication and tags them as "DD" pair type. These pairs should be excluded from downstream analyses. .pairsam -------- `pairtools` also define .pairsam, a valid extension of the .pairs format. On top of the pairtools' flavor of .pairs, .pairsam format adds two extra columns containing the alignments from which the Hi-C pair was extracted: ======== =========== =============================================================================== index name description ======== =========== =============================================================================== 9 sam1 the sam alignment(s) on side 1; separate supplemental alignments by NEXT_SAM 10 sam2 the sam alignment(s) on side 2; separate supplemental alignments by NEXT_SAM ======== =========== =============================================================================== Note that, normally, the fields of a sam alignment are separated by a horizontal tab character (\\t), which we already use to separate .pairs columns. To avoid confusion, we replace the tab character in sam entries stored in sam1 and sam2 columns with a UNIT SEPARATOR character (\\031). Finally, sam1 and sam2 can store multiple .sam alignments, separated by a string '\\031NEXT_SAM\\031' .. |check| unicode:: U+2714 .. check .. |cross| unicode:: U+274C .. cross pairtools-0.3.0/doc/index.rst000066400000000000000000000046241345765554600161660ustar00rootroot00000000000000.. pairtools documentation master file, created by sphinx-quickstart on Wed Dec 6 12:32:49 2017. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Overview ======== `pairtools` is a simple and fast command-line framework to process sequencing data from a Hi-C experiment. `pairtools` perform various operations on Hi-C pairs and occupy the middle position in a typical Hi-C data processing pipeline: .. figure:: _static/hic-processing-pipeline.png :width: 100% :alt: The diagram of a typical processing pipeline for Hi-C data :align: center In a typical Hi-C pipeline, DNA sequences (reads) are aligned to the reference genome, converted into ligation junctions and binned, thus producing a Hi-C contact map. `pairtools` aim to be an all-in-one tool for processing Hi-C pairs, and can perform following operations: - detect ligation junctions (a.k.a. Hi-C pairs) in aligned paired-end sequences of Hi-C DNA molecules - sort .pairs files for downstream analyses - detect, tag and remove PCR/optical duplicates - generate extensive statistics of Hi-C datasets - select Hi-C pairs given flexibly defined criteria - restore .sam alignments from Hi-C pairs `pairtools` produce .pairs files compliant with the `4DN standard `_. The full list of available pairtools: ============ ============================================== Pairtool Description ============ ============================================== dedup Find and remove PCR/optical duplicates. filterbycov Remove pairs from regions of high coverage. flip Flip pairs to get an upper-triangular matrix. markasdup Tag pairs as duplicates. merge Merge sorted .pairs/.pairsam files. parse Find ligation junctions in .sam, make .pairs. phase Phase pairs mapped to a diploid genome. restrict Assign restriction fragments to pairs. select Select pairs according to some condition. sort Sort a .pairs/.pairsam file. split Split a .pairsam file into .pairs and .sam. stats Calculate pairs statistics. ============ ============================================== Contents: .. toctree:: :hidden: self .. toctree:: :maxdepth: 3 quickstart installation parsing sorting formats technotes cli_tools * :ref:`genindex` pairtools-0.3.0/doc/installation.rst000066400000000000000000000033451345765554600175570ustar00rootroot00000000000000Installation ============ Requirements ------------ - Python 3.x - Python packages `numpy` and `click` - Command-line utilities `sort` (the Unix version), `bgzip` (shipped with `tabix`) and `samtools`. If available, `pairtools` can compress outputs with `pbgzip` and `lz4`. Install using conda ------------------- We highly recommend using the `conda` package manager to install pre-compiled `pairtools` together with all its dependencies. To get it, you can either install the full `Anaconda `_ Python distribution or just the standalone `conda `_ package manager. With `conda`, you can install pre-compiled `pairtools` and all of its dependencies from the `bioconda `_ channel: .. code-block:: bash $ conda install -c conda-forge -c bioconda pairtools Install using pip ----------------- Alternatively, compile and install `pairtools` and its Python dependencies from PyPI using pip: .. code-block:: bash $ pip install pairtools Install the development version ------------------------------- Finally, you can install the latest development version of `pairtools` from github. First, make a local clone of the github repository: .. code-block:: bash $ git clone https://github.com/mirnylab/pairtools Then, you can compile and install `pairtools` in `the development mode `_, which installs the package without moving it to a system folder and thus allows immediate live-testing any changes in the python code. Please, make sure that you have `cython` installed! .. code-block:: bash $ cd pairtools $ pip install -e ./ pairtools-0.3.0/doc/parsing.rst000066400000000000000000000205711345765554600165210ustar00rootroot00000000000000Parsing sequence alignments into Hi-C pairs =========================================== Overview -------- Hi-C experiments aim to measure the frequencies of contacts between all pairs of loci in the genome. In these experiments, the spacial structure of chromosomes if first fixed with formaldehyde crosslinks, after which DNA is partially digested with restriction enzymes and then re-ligated back. Then, DNA is shredded into smaller pieces, released from nucleus, sequenced and aligned to the reference genome. The resulting sequence alignments reveal if DNA molecules were formed through ligations between DNA from different locations in the genome. These ligation events imply that ligated loci were close to each other when the ligation enzyme was active, i.e. they formed "a contact". ``pairtools parse`` detects ligation events in the aligned sequences of DNA molecules formed in Hi-C experiments and reports them in the .pairs/.pairsam format. Terminology ----------- Throughout this document we will be using the same visual language to describe how DNA sequences (in the .fastq format) are transformed into sequence alignments (.sam/.bam) and into ligation events (.pairs). .. figure:: _static/terminology.png :scale: 50 % :alt: The visual language to describe transformation of Hi-C data :align: center DNA sequences (reads) are aligned to the reference genome and converted into ligation events Short-read sequencing determines the sequences of the both ends (or, **sides**) of DNA molecules (typically 50-300 bp), producing **read pairs** in .fastq format (shown in the first row on the figure above). In such reads, base pairs are reported from the tips inwards, which is also defined as the **5'->3'** direction (in accordance of the 5'->3' direction of the DNA strand that sequence of the corresponding side of the read). Alignment software maps both reads of a pair to the reference genome, producing **alignments**, i.e. segments of the reference genome with matching sequences. Typically, there will be only two alignments per read pair, one on each side. But, sometimes, the parts of one or both sides may map to different locations on the genome, producing more than two alignments per DNA molecule (see :ref:`section-walks`). ``pairtools parse`` converts alignments into **ligation events** (aka **Hi-C pairs** aka **pairs**). In the simplest case, when each side has only one unique alignment (i.e. the whole side maps to a single unique segment of the genome), for each side, we report the chromosome, the genomic position of the outer-most (5') aligned base pair and the strand of the reference genome that the read aligns to. ``pairtools parse`` assigns to such pairs the type ``UU`` (unique-unique). Unmapped/multimapped reads -------------------------- Sometimes, one side or both sides of a read pair may not align to the reference genome: .. figure:: _static/read_pair_NU_NN.png :scale: 50 % :alt: Read pairs missing an alignment on one or both sides :align: center Read pairs missing an alignment on one or both sides In this case, ``pairtools parse`` fills in the chromosome of the corresponding side of Hi-C pair with ``!``, the position with ``0`` and the strand with ``-``. Such pairs are reported as type ``NU`` (null-unique, when the other side has a unique alignment) or ``NN`` (null-null, when both sides lack any alignment). Similarly, when one or both sides map to many genome locations equally well (i.e. have non-unique, or, multi-mapping alignments), ``pairtools parse`` reports the corresponding sides as (chromosome= ``!``, position= ``0``, strand= ``-``) and type ``MU`` (multi-unique) or ``MM`` (multi-multi) or ``NM`` (null-multi), depending on the type of the alignment on the other side. .. figure:: _static/read_pair_MU_MM_NM.png :scale: 50 % :alt: Read pairs with a non-unique alignment on one or both sides :align: center Read pairs with a non-unique (multi-) alignment on one side ``pairtools parse`` calls an alignment to be multi-mapping when its `MAPQ score `_ (which depends on the scoring gap between the two best candidate alignments for a segment) is equal or greater than the value specied with the ``--min-mapq`` flag (by default, 1). .. _section-walks: Multiple ligations (walks) -------------------------- Finally, a read pair may contain more than two alignments: .. figure:: _static/read_pair_WW.png :scale: 50 % :alt: A sequenced Hi-C molecule that was formed via multiple ligations :align: center A sequenced Hi-C molecule that was formed via multiple ligations Molecules like these typically form via multiple ligation events and we call them walks [1]_. Currently, ``pairtools parse`` does not process such molecules and tags them as type ``WW``. .. _section-gaps: Interpreting gaps between alignments ------------------------------------ Reads that are only partially aligned to the genome can be interpreted in two different ways. One possibility is to assume that this molecule was formed via at least two ligations (i.e. it's a *walk*) but the non-aligned part (a **gap**) was missing from the reference genome for one reason or another. Another possibility is to simply ignore this gap (for example, because it could be an insertion or a technical artifact), thus assuming that our molecule was formed via a single ligation and has to be reported: .. figure:: _static/read_pair_gaps_vs_null_alignment.png :scale: 50 % :alt: A gap between alignments can be ignored or interpeted as a "null" alignment :align: center A gap between alignments can interpeted as a legitimate segment without an alignment or simply ignored Both options have their merits, depending on a dataset, quality of the reference genome and sequencing. ``pairtools parse`` ignores shorter *gaps* and keeps longer ones as "null" alignments. The maximal size of ignored *gaps* is set by the ``--max-inter-align-gap`` flag (by default, 20bp). Rescuing single ligations ------------------------- Importantly, some of DNA molecules containing only one ligation junction may still end up with three alignments: .. figure:: _static/read_pair_UR.png :scale: 50 % :alt: Not all read pairs with three alignments come from "walks" :align: center Not all read pairs with three alignments come from "walks" A molecule formed via a single ligation gets three alignments when one of the two ligated DNA pieces is shorter than the read length, such that that read on the corresponding side sequences through the ligation junction and into the other piece [2]_. The amount of such molecules depends on the type of the restriction enzyme, the typical size of DNA molecules in the Hi-C library and the read length, and sometimes can be considerable. ``pairtools parse`` detects such molecules and **rescues** them (i.e. changes their type from a *walk* to a single-ligation molecule). It tests walks with three aligments using three criteria: .. figure:: _static/read_pair_UR_criteria.png :scale: 50 % :alt: The three criteria used for "rescue" :align: center The three criteria used to "rescue" three-alignment walks: cis, point towards each other, short distance 1. On the side with two alignments (the **chimeric** side), the "inner" (or, 3') alignment must be on the same chromosome as the alignment on the non-chimeric side. 2. The "inner" alignment on the chimeric side and the alignment on the non-chimeric side must point toward each other. 3. These two alignments must be within the distance specified with the ``--max-molecule-size`` flag (by default, 2000bp). Sometimes, the "inner" alignment on the chimeric side can be non-unique or "null" (i.e. when the unmapped segment is longer than ``--max-inter-align-gap``, as described in :ref:`section-gaps`). ``pairtools parse`` ignores such alignments altogether and thus rescues such *walks* as well. .. figure:: _static/read_pair_UR_MorN.png :scale: 50 % :alt: A walk with three alignments get rescued, when the middle alignment is multi- or null :align: center A walk with three alignments get rescued, when the middle alignment is multi- or null. .. [1] Following the lead of `C-walks `_ .. [2] This procedure was first introduced in `HiC-Pro `_ and the in `Juicer `_ . pairtools-0.3.0/doc/quickstart.rst000066400000000000000000000024171345765554600172470ustar00rootroot00000000000000Quickstart ========== Install `pairtools` and all of its dependencies using the `conda `_ package manager and the `bioconda `_ channel for bioinformatics software. .. code-block:: bash $ conda install -c conda-forge -c bioconda pairtools Setup a new test folder and download a small Hi-C dataset mapped to sacCer3 genome: .. code-block:: bash $ mkdir /tmp/test-pairtools $ cd /tmp/test-pairtools $ wget https://github.com/mirnylab/distiller-test-data/raw/master/bam/MATalpha_R1.bam Additionally, we will need a .chromsizes file, a TAB-separated plain text table describing the names, sizes and the order of chromosomes in the genome assembly used during mapping: .. code-block:: bash $ wget https://raw.githubusercontent.com/mirnylab/distiller-test-data/master/genome/sacCer3.reduced.chrom.sizes With `pairtools parse`, we can convert paired-end sequence alignments stored in .sam/.bam format into .pairs, a TAB-separated table of Hi-C ligation junctions: .. code-block:: bash $ pairtools parse -c sacCer3.reduced.chrom.sizes -o MATalpha_R1.pairs.gz --drop-sam MATalpha_R1.bam Inspect the resulting table: .. code-block:: bash $ less MATalpha_R1.pairs.gz pairtools-0.3.0/doc/sorting.rst000066400000000000000000000076131345765554600165450ustar00rootroot00000000000000Sorting pairs ============= In order to enable efficient random access to Hi-C pairs, we **flip** and **sort** pairs. After sorting, interactions become arranged in the order of their genomic position, such that, for any given pair of regions, we easily find and extract all of their interactions. And, after flipping, all artificially duplicated molecules (either during PCR or in optical sequencing) end up in adjacent rows in sorted lists of interactions, such that we can easily identify and remove them. Sorting ------- ``pairtools sort`` arrange pairs in the order of (chrom1, chrom2, pos1, pos2). This order is also known as *block sorting*, because all pairs between any given pair of chromosomes become grouped into one continuous block. Additionally, ``pairtools sort`` also sorts pairs with identical positions by `pair_type`. This does not really do much for mapped reads, but it nicely splits unmapped reads into blocks of null-mapped and multi-mapped reads. We note that there is an alternative to block sorting, called *row sorting*, where pairs are sorted by (chrom1, pos1, chrom2, pos2). In `pairtools sort`, we prefer block-sorting since it cleanly separates cis interactions from trans ones and thus is a more optimal solution for typical use cases. Flipping -------- In a typical paired-end experiment, *side1* and *side2* of a DNA molecule are defined by the order in which they got sequenced. Since this order is essentially random, any given Hi-C pair, e.g. (chr1, 1.1Mb; chr2, 2.1Mb), may appear in a reversed orientation, i.e. (chr2, 2.1Mb; chr1, 1.1Mb). If we were to preserve this order of sides, interactions between same loci would appear in two different locations of the sorted pair list, which would complicate finding PCR/optical duplicates. To ensure that Hi-C pairs with similar coordinates end up in the same location of the sorted list, we **flip** pairs, i.e. we choose *side1* as the side with the lowest genomic coordinate. Thus, after flipping, for *trans* pairs (chrom1!=chrom2), order(chrom1)`_ compression by default instead of gzip. Using `bgzip` allows us to create an index with `pairix `_ and get random access to data. - `paritools` have an option to compress outputs with `lz4 `_. `Lz4 is much faster and only slighly less efficient than gzip `_. This makes lz4 a better choice for passing data between individual pairtools before producing final result (which, in turn, requires bgzip compression). pairtools-0.3.0/examples/000077500000000000000000000000001345765554600153705ustar00rootroot00000000000000pairtools-0.3.0/examples/example_pipeline.sh000066400000000000000000000045071345765554600212520ustar00rootroot00000000000000#!/usr/bin/env bash if [ $# -le 3 ] ; then echo "Usage: bash example_pipeline.sh BWA_INDEX FASTQ_1 FASTQ_2 OUTPUT_PREFIX" echo "" echo "A example of a bash pipeline to align the sequencing data from a " echo "single Hi-C experiment." echo "" echo "positional arguments:" echo "" echo "BWA_INDEX The path to a bwa index of the reference genome." echo "CHROM_SIZES The path to a file with chromosome sizes." echo "FASTQ_1 The path to a fastq file with the sequences of " echo " the first side of Hi-C molecules." echo "FASTQ_2 The path to a fastq file with the sequences of " echo " the second side of Hi-C molecules." echo "OUTPUT_PREFIX The prefix to the paths of generated outputs. " echo "" echo "" exit 0 fi set -o errexit set -o nounset set -o pipefail INDEX=$1 CHROM_SIZES=$2 FASTQ1=$3 FASTQ2=$4 OUTPREFIX=$5 N_THREADS=8 UNMAPPED_SAM_PATH=${OUTPREFIX}.unmapped.bam UNMAPPED_PAIRS_PATH=${OUTPREFIX}.unmapped.pairs.gz NODUPS_SAM_PATH=${OUTPREFIX}.nodups.bam NODUPS_PAIRS_PATH=${OUTPREFIX}.nodups.pairs.gz DUPS_SAM_PATH=${OUTPREFIX}.dups.bam DUPS_PAIRS_PATH=${OUTPREFIX}.dups.pairs.gz bwa mem -SP -t "${N_THREADS}" "${INDEX}" "${FASTQ1}" "${FASTQ2}" | { # Classify Hi-C molecules as unmapped/single-sided/multimapped/chimeric/etc # and output one line per read, containing the following, separated by \\v: # * triu-flipped pairs # * read id # * type of a Hi-C molecule # * corresponding sam entries pairtools parse "{CHROM_SIZES}" } | { # Block-sort pairs together with SAM entries pairtools sort } | { # Set unmapped and ambiguous reads aside pairtools select '(pair_type == "UU") or (pair_type == "UR") or (pair_type == "RU")' \ --output-rest >( pairtools split \ --output-pairs ${UNMAPPED_PAIRS_PATH} \ --output-sam ${UNMAPPED_SAM_PATH} ) } | { # Remove duplicates pairtools dedup \ --output \ >( pairtools split \ --output-pairs ${NODUPS_PAIRS_PATH} \ --output-sam ${NODUPS_SAM_PATH} ) \ --output-dups \ >( pairtools markasdup \ | pairtools split \ --output-pairs ${DUPS_PAIRS_PATH} \ --output-sam ${DUPS_SAM_PATH} ) } pairtools-0.3.0/examples/example_singlecell_pipeline.sh000066400000000000000000000055271345765554600234560ustar00rootroot00000000000000#!/usr/bin/env bash if [ $# -le 3 ] ; then echo "Usage: bash example_pipeline.sh BWA_INDEX FASTQ_1 FASTQ_2 OUTPUT_PREFIX" echo "" echo "A example of a bash pipeline to align the sequencing data from a " echo "single Hi-C experiment." echo "" echo "positional arguments:" echo "" echo "BWA_INDEX The path to a bwa index of the reference genome." echo "CHROM_SIZES The path to a file with chromosome sizes." echo "FASTQ_1 The path to a fastq file with the sequences of " echo " the first side of Hi-C molecules." echo "FASTQ_2 The path to a fastq file with the sequences of " echo " the second side of Hi-C molecules." echo "OUTPUT_PREFIX The prefix to the paths of generated outputs. " echo "" echo "" exit 0 fi set -o errexit set -o nounset set -o pipefail INDEX=$1 CHROM_SIZES=$2 FASTQ1=$3 FASTQ2=$4 OUTPREFIX=$5 N_THREADS=8 UNMAPPED_SAM_PATH=${OUTPREFIX}.unmapped.bam UNMAPPED_PAIRS_PATH=${OUTPREFIX}.unmapped.pairs.gz NODUPS_SAM_PATH=${OUTPREFIX}.nodups.bam NODUPS_PAIRS_PATH=${OUTPREFIX}.nodups.pairs.gz DUPS_SAM_PATH=${OUTPREFIX}.dups.bam DUPS_PAIRS_PATH=${OUTPREFIX}.dups.pairs.gz LOWFREQPAIRS_SAM_PATH=${OUTPREFIX}.lowfreq.bam LOWFREQPAIRS_PAIRS_PATH=${OUTPREFIX}.lowfreq.pairs.gz HIGHFREQPAIRS_SAM_PATH=${OUTPREFIX}.highfreq.bam HIGHFREQPAIRS_PAIRS_PATH=${OUTPREFIX}.highfreq.pairs.gz bwa mem -SP -t "${N_THREADS}" "${INDEX}" "${FASTQ1}" "${FASTQ2}" | { # Classify Hi-C molecules as unmapped/single-sided/multimapped/chimeric/etc # and output one line per read, containing the following, separated by \\v: # * triu-flipped pairs # * read id # * type of a Hi-C molecule # * corresponding sam entries pairtools parse "{CHROM_SIZES}" } | { # Block-sort pairs together with SAM entries pairtools sort } | { # Set unmapped and ambiguous reads aside pairtools select '(pair_type == "UU") or (pair_type == "UR") or (pair_type == "RU")' \ --output-rest >( pairtools split \ --output-pairs ${UNMAPPED_PAIRS_PATH} \ --output-sam ${UNMAPPED_SAM_PATH} ) } | { # Remove duplicates pairtools dedup \ --output-dups \ >( pairtools markasdup \ | pairtools split \ --output-pairs ${DUPS_PAIRS_PATH} \ --output-sam ${DUPS_SAM_PATH} ) } | { # Remove high frequency interactors pairtools multifilter \ --output \ >( pairtools split \ --output-pairs ${LOWFREQ_PAIRS_PATH} \ --output-sam ${LOWFREQ_SAM_PATH} ) \ --output-high-frequency-interactors \ >( pairtools markasdup \ | pairtools split \ --output-pairs ${HIGHFREQPAIRS_PAIRS_PATH} \ --output-sam ${HIGHFREQPAIRS_SAM_PATH} ) } pairtools-0.3.0/pairtools/000077500000000000000000000000001345765554600155665ustar00rootroot00000000000000pairtools-0.3.0/pairtools/__init__.py000066400000000000000000000063251345765554600177050ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pairtools ~~~~~~~~~ CLI tools to process mapped Hi-C data :copyright: (c) 2017-2019 Massachusetts Institute of Technology :author: Mirny Lab :license: MIT """ __version__ = '0.3.0' import click import functools import sys CONTEXT_SETTINGS = { 'help_option_names': ['-h', '--help'], } @click.version_option(version=__version__) @click.group(context_settings=CONTEXT_SETTINGS) @click.option( '--post-mortem', help="Post mortem debugging", is_flag=True, default=False ) @click.option( '--output-profile', help="Profile performance with Python cProfile and dump the statistics " "into a binary file", type=str, default='' ) def cli(post_mortem, output_profile): '''Flexible tools for Hi-C data processing. All pairtools have a few common options, which should be typed _before_ the command name. ''' if post_mortem: import traceback try: import ipdb as pdb except ImportError: import pdb def _excepthook(exc_type, value, tb): traceback.print_exception(exc_type, value, tb) print() pdb.pm() sys.excepthook = _excepthook if output_profile: import cProfile import atexit pr = cProfile.Profile() pr.enable() def _atexit_profile_hook(): pr.disable() pr.dump_stats(output_profile) atexit.register(_atexit_profile_hook) def common_io_options(func): @click.option( '--nproc-in', type=int, default=3, show_default=True, help='Number of processes used by the auto-guessed input decompressing command.' ) @click.option( '--nproc-out', type=int, default=8, show_default=True, help='Number of processes used by the auto-guessed output compressing command.' ) @click.option( '--cmd-in', type=str, default=None, help='A command to decompress the input file. ' 'If provided, fully overrides the auto-guessed command. ' 'Does not work with stdin. ' 'Must read input from stdin and print output into stdout. ' 'EXAMPLE: pbgzip -dc -n 3' ) @click.option( '--cmd-out', type=str, default=None, help='A command to compress the output file. ' 'If provided, fully overrides the auto-guessed command. ' 'Does not work with stdout. ' 'Must read input from stdin and print output into stdout. ' 'EXAMPLE: pbgzip -c -n 8' ) @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper from .pairtools_dedup import dedup from .pairtools_sort import sort from .pairtools_flip import flip from .pairtools_merge import merge from .pairtools_markasdup import markasdup from .pairtools_select import select from .pairtools_split import split from .pairtools_restrict import restrict from .pairtools_phase import phase from .pairtools_parse import parse, parse_cigar, parse_algn from .pairtools_stats import stats from .pairtools_filterbycov import filterbycov pairtools-0.3.0/pairtools/__main__.py000066400000000000000000000000671345765554600176630ustar00rootroot00000000000000from . import cli if __name__=='__main__': cli() pairtools-0.3.0/pairtools/_dedup.pyx000066400000000000000000000212111345765554600175650ustar00rootroot00000000000000""" ``mark_duplicates`` is an offline method that finds duplicates in a given input dataset. For other applications on much larger datasets you may consider an online method ``OnlineDuplicateDetector`` which is implemented as a class. Note that for both methods data types are fixed: * chromosomes are int32 * position is int32 * strand is int32, which is basically the same as C type "char". """ import numpy as np import cython cimport numpy as np cimport cython def mark_duplicates( cython.int [:] c1, cython.int [:] c2, cython.int [:] p1, cython.int [:] p2, cython.int [:] s1, cython.int [:] s2, #uncomment for testing probably since it will do boundary check #np.ndarray[np.int16_t, ndim=1]c2, #np.ndarray[np.int32_t, ndim=1] p1, #np.ndarray[np.int32_t, ndim=1] p2, #np.ndarray[np.int8_t, ndim=1] s1, #np.ndarray[np.int8_t, ndim=1] s2, int max_mismatch=3, method = "sum"): """ Mark duplicates, allowing for some mismatch on the both sides of the molecule. You can use it to filter single-cell data as well by setting max_mismatch to 500bp or even larger. It works as fast as we could make it. This methods scans through a list of reads. It then flags duplicates, which are defined as molecules, with both ends located within `max_mismatch` bp from each other. There are two ways define duplicates: "max": two reads are duplicates if the mismatch of the genomic locations of both ends is less-or-equal "max_mismatch" "sum": two reads are duplicates if the sum of the mismatches of the either ends of the molecule is less-or-equal "max_mismatch" Other methods could be added below by editing the code Parameters ---------- c1, c2 : int32 array chromosome IDs p1, p2 : int32 arrays positions s1, s2 : int32 (or bool) arrays strands max_mismatch : int method : "sum" or "max" use the sum of mismatches, or the max of the two Returns ------- mask : int8 array A binary mask, where 1 denotes that a read is a duplicate. Notes ----- Arrays MUST be ordered by (c1, p1) """ cdef int N = len(c1) cdef np.ndarray[np.int8_t, ndim=1] mask = np.zeros(N, dtype=np.int8) cdef int low = 0 cdef int high = 1 cdef int extraCondition cdef int methodid if method == "max": methodid = 0 elif method == "sum": methodid = 1 else: raise ValueError('method should be "sum" or "max"') while True: assert False if low == N: break if high == N: low += 1 high = low + 1 continue if mask[low] == 1: low += 1 high = low+1 continue # if high already removed, just continue if mask[high] == 1: high += 1 continue # if we jumped too far, continue if (c1[high] != c1[low]) or (p1[high] - p1[low] > max_mismatch) or (p1[high] < p1[low]) or (c2[high] != c2[low]): low += 1 high = low + 1 # restart high continue if methodid == 0: extraCondition = (max(abs(p1[low] - p1[high]), abs(p2[low] - p2[high])) <= max_mismatch) elif methodid == 1: extraCondition = (abs(p1[low] - p1[high]) + abs(p2[low] - p2[high]) <= max_mismatch) else: raise ValueError( "Unknown method id, this should not happen. " "Check code of this function.") if ((c2[low] == c2[high]) and (s1[low] == s1[high]) and (s2[low] == s2[high]) and extraCondition): mask[high] = 1 high += 1 continue high += 1 return mask cdef class OnlineDuplicateDetector(object): cdef cython.int [:] c1 cdef cython.int [:] c2 cdef cython.int [:] p1 cdef cython.int [:] p2 cdef cython.int [:] s1 cdef cython.int [:] s2 cdef cython.char [:] rm cdef int methodid cdef int low cdef int high cdef int N cdef int max_mismatch cdef int returnData def __init__(self, method, max_mismatch, returnData=False): if returnData == False: self.returnData = 0 else: self.returnData = 1 self.N = 0 self.c1 = np.zeros(0, np.int32) self.c2 = np.zeros(0, np.int32) self.p1 = np.zeros(0, np.int32) self.p2 = np.zeros(0, np.int32) self.s1 = np.zeros(0, np.int32) self.s2 = np.zeros(0, np.int32) self.rm = np.zeros(0, np.int8) if method == "max": self.methodid = 0 elif method == "sum": self.methodid = 1 else: raise ValueError('method should be "sum" or "max"') self.max_mismatch = int(max_mismatch) self.low = 0 self.high = 1 def _shrink(self): if self.returnData == 1: firstret = self.rm[:self.low] retainMask = (np.asarray(firstret) == False) del firstret ret = [] for ar in [self.c1, self.c2, self.p1, self.p2, self.s1, self.s2]: ret.append(np.asarray(ar)[:self.low][retainMask]) self.c1 = self.c1[self.low:] self.c2 = self.c2[self.low:] self.p1 = self.p1[self.low:] self.p2 = self.p2[self.low:] self.s1 = self.s1[self.low:] self.s2 = self.s2[self.low:] pastrm = self.rm[:self.low] self.rm = self.rm[self.low:] self.high = self.high-self.low self.N = self.N - self.low self.low = 0 if self.returnData == 1: return ret return pastrm def _run(self, finish=False): cdef int finishing = 0 cdef int extraCondition if finish: finishing = 1 while True: if self.low == self.N: break if self.high == self.N: if finishing == 1: self.low += 1 self.high = self.low + 1 continue else: break if self.rm[self.low] == 1: self.low += 1 self.high = self.low+1 continue # if high already removed, just continue if self.rm[self.high] == 1: self.high += 1 continue # if we jumped too far, continue if ((self.c1[self.high] != self.c1[self.low]) or (self.p1[self.high] - self.p1[self.low] > self.max_mismatch) or (self.p1[self.high] - self.p1[self.low] < 0 )): self.low += 1 self.high = self.low + 1 # restart high continue if self.methodid == 0: extraCondition = max( abs(self.p1[self.low] - self.p1[self.high]), abs(self.p2[self.low] - self.p2[self.high])) <= self.max_mismatch elif self.methodid == 1: # sum of distances <= max_mismatch extraCondition = ( abs(self.p1[self.low] - self.p1[self.high]) + abs(self.p2[self.low] - self.p2[self.high]) <= self.max_mismatch ) else: raise ValueError( "Unknown method id, this should not happen. " "Check code of this function.") if ((self.c2[self.low] == self.c2[self.high]) and (self.s1[self.low] == self.s1[self.high]) and (self.s2[self.low] == self.s2[self.high]) and extraCondition): self.rm[self.high] = 1 self.high += 1 continue self.high += 1 return self._shrink() def push(self, c1, c2, p1, p2, s1, s2): self.c1 = np.concatenate([self.c1, c1]) self.c2 = np.concatenate([self.c2, c2]) self.p1 = np.concatenate([self.p1, p1]) self.p2 = np.concatenate([self.p2, p2]) self.s1 = np.concatenate([self.s1, s1]) self.s2 = np.concatenate([self.s2, s2]) self.rm = np.concatenate([self.rm, np.zeros(len(c1), dtype=np.int8)]) self.N = self.N + len(c1) return self._run(finish=False) def finish(self): return self._run(finish=True) def getLen(self): return int(self.N) pairtools-0.3.0/pairtools/_fileio.py000066400000000000000000000071451345765554600175550ustar00rootroot00000000000000import shutil import pipes class ParseError(Exception): pass def auto_open(path, mode, nproc=1, command=None): '''Guess the file format from the extension and use the corresponding binary to open it for reading or writing. If the extension is not known, open the file as text. If the binary allows parallel execution, specify the number of threads with `nproc`. If `command` is supplied, use it to open the file instead of auto-guessing. The command must accept the filename as the last argument, accept input through stdin and print output into stdout. Supported extensions and binaries (with comments): .bam - samtools view (allows parallel writing) .gz - pbgzip .lz4 - lz4c (does not support parallel execution) ''' if command: if mode =='w': t = pipes.Template() t.append(command, '--') f = t.open(path, 'w') elif mode =='r': t = pipes.Template() t.append(command, '--') f = t.open(path, 'r') else: raise ValueError("Unknown mode : {}".format(mode)) return f elif path.endswith('.bam'): if shutil.which('samtools') is None: raise ValueError({ 'w':'samtools is not found, cannot compress output', 'r':'samtools is not found, cannot decompress input' }[mode]) if mode =='w': t = pipes.Template() t.append('samtools view -bS {} -'.format( '-@ '+str(nproc-1) if nproc>1 else ''), '--') f = t.open(path, 'w') elif mode =='r': t = pipes.Template() t.append('samtools view -h', '--') f = t.open(path, 'r') else: raise ValueError("Unknown mode for .bam : {}".format(mode)) return f elif path.endswith('.gz'): if shutil.which('pbgzip') is None: raise ValueError({ 'w':'pbgzip is not found, cannot compress output', 'a':'pbgzip is not found, cannot compress output', 'r':'pbgzip is not found, cannot decompress input' }[mode]) if mode =='w': t = pipes.Template() t.append('pbgzip -c -n {}'.format(nproc), '--') f = t.open(path, 'w') elif mode =='a': t = pipes.Template() t.append('pbgzip -c -n {} $IN >> $OUT'.format(nproc), 'ff') f = t.open(path, 'w') elif mode =='r': t = pipes.Template() t.append('pbgzip -dc -n {}'.format(nproc), '--') f = t.open(path, 'r') else: raise ValueError("Unknown mode for .gz : {}".format(mode)) return f elif path.endswith('.lz4'): if shutil.which('lz4c') is None: raise ValueError({ 'w':'lz4c is not found, cannot compress output', 'a':'lz4c is not found, cannot compress output', 'r':'lz4c is not found, cannot decompress input' }[mode]) if mode =='w': t = pipes.Template() t.append('lz4c -cz', '--') f = t.open(path, 'w') elif mode =='a': t = pipes.Template() t.append('lz4c -cz $IN >> $OUT', 'ff') f = t.open(path, 'w') elif mode =='r': t = pipes.Template() t.append('lz4c -cd', '--') f = t.open(path, 'r') else: raise ValueError("Unknown mode : {}".format(mode)) return f else: return open(path, mode) pairtools-0.3.0/pairtools/_headerops.py000066400000000000000000000412721345765554600202570ustar00rootroot00000000000000from collections import OrderedDict, defaultdict import sys import copy import itertools from . import __version__, _pairsam_format from ._fileio import ParseError PAIRS_FORMAT_VERSION = '1.0.0' def get_header(instream, comment_char='#'): '''Returns a header from the stream and an the reaminder of the stream with the actual data. Parameters ---------- instream : a file object An input stream. comment_char : str The character prepended to header lines (use '@' when parsing sams, '#' when parsing pairsams). Returns ------- header : list The header lines, stripped of terminal spaces and newline characters. remainder_stream : stream/file-like object Stream with the remaining lines. ''' header = [] if not comment_char: raise ValueError('Please, provide a comment char!') comment_byte = comment_char.encode() # get peekable buffer for the instream inbuffer = instream.buffer current_peek = inbuffer.peek() while current_peek.startswith(comment_byte): # consuming a line from buffer guarantees # that the remainder of the buffer starts # with the beginning of the line. line = inbuffer.readline() # append line to header, since it does start with header header.append(line.decode().strip()) # peek into the remainder of the instream current_peek = inbuffer.peek() # apparently, next line does not start with the comment # return header and the instream, advanced to the beginning of the data return header, instream def extract_fields(header, field_name, save_rest=False): ''' Extract the specified fields from the pairs header and returns a list of corresponding values, even if a single field was found. Additionally, can return the list of intact non-matching entries. ''' fields = [] rest = [] for l in header: if l.lstrip('#').startswith(field_name+':'): fields.append(l.split(':',1)[1].strip()) elif save_rest: rest.append(l) if save_rest: return fields, rest else: return fields def extract_column_names(header): ''' Extract column names from header lines. ''' columns = extract_fields(header, 'columns') if len(columns) != 0: return columns[0].split(' ') else: return [] def get_chromsizes_from_sam_header(samheader): SQs = [l.split('\t') for l in samheader if l.startswith('@SQ')] chromsizes = [(sq[1][3:], int(sq[2][3:])) for sq in SQs] return OrderedDict(chromsizes) def get_chrom_order(chroms_file, sam_chroms=None): """ Produce an "enumeration" of chromosomes based on the list of chromosomes """ chrom_enum = OrderedDict() i = 1 with open(chroms_file, 'rt') as f: for line in f: chrom = line.strip().split('\t')[0] if chrom and ((not sam_chroms) or (chrom in sam_chroms)): chrom_enum[chrom] = i i += 1 if sam_chroms: remaining = sorted(chrom for chrom in sam_chroms if chrom not in chrom_enum.keys()) for chrom in remaining: chrom_enum[chrom] = i i += 1 return chrom_enum def make_standard_pairsheader( assembly=None, chromsizes=None, columns=_pairsam_format.COLUMNS, shape = 'upper triangle'): header = [] header.append( '## pairs format v{}'.format(PAIRS_FORMAT_VERSION)) header.append('#shape: {}'.format(shape)) header.append('#genome_assembly: {}'.format( assembly if assembly is not None else 'unknown')) if chromsizes is not None: try: chromsizes = chromsizes.items() except AttributeError: pass for chrom, length in chromsizes: header.append('#chromsize: {} {}'.format(chrom, length)) header.append('#columns: '+ ' '.join(columns)) return header def subset_chroms_in_pairsheader(header, chrom_subset): new_header = [] for line in header: if line.startswith('#chromsize:'): if line.strip().split()[1] in chrom_subset: new_header.append(line) elif line.startswith('#chromosomes:'): line = ' '.join( ['#chromosomes:'] + [c for c in line.strip().split()[1:] if c in chrom_subset]) new_header.append(line) else: new_header.append(line) return new_header def insert_samheader(header, samheader): new_header = [l for l in header if not l.startswith('#columns')] if samheader: new_header += ['#samheader: '+l for l in samheader] new_header += [l for l in header if l.startswith('#columns')] return new_header def mark_header_as_sorted(header): header = copy.deepcopy(header) if not any([l.startswith('#sorted') for l in header]): if header[0].startswith('##'): header.insert(1, '#sorted: chr1-chr2-pos1-pos2') else: header.insert(0, '#sorted: chr1-chr2-pos1-pos2') for i in range(len(header)): if header[i].startswith('#chromosomes'): chroms = header[i][12:].strip().split(' ') header[i] = '#chromosomes: {}'.format(' '.join(sorted(chroms))) return header def append_new_pg(header, ID='', PN='', VN=None, CL=None, force=False): header = copy.deepcopy(header) samheader, other_header = extract_fields(header, 'samheader', save_rest=True) new_samheader = _add_pg_to_samheader(samheader, ID, PN, VN, CL, force) new_header = insert_samheader(other_header, new_samheader) return new_header def _update_header_entry(header, field, new_value): header = copy.deepcopy(header) found = False newline = '#{}: {}'.format(field, new_value) for i in range(len(header)): if header[i].startswith('#'+field): header[i] = newline found = True if not found: if header[-1].startswith('#columns'): header.insert(-1, newline) else: header.append(newline) return header def _add_pg_to_samheader(samheader, ID='', PN='', VN=None, CL=None, force=False): '''Append a @PG record to an existing sam header. If the header comes from a merged file and thus has multiple chains of @PG, append the provided PG to all of the chains, adding the numerical suffix of the branch to the ID. Parameters ---------- header : list of str ID, PN, VN, CL : std The keys of a new @PG record. If absent, VN is the version of pairtools and CL is taken from sys.argv. force : bool If True, ignore the inconsistencies among @PG records of the existing header. Returns ------- new_header : list of str A list of new headers lines, stripped of newline characters. ''' if VN is None: VN = __version__ if CL is None: CL = ' '.join(sys.argv) pre_pg_header = [line.strip() for line in samheader if line.startswith('@HD') or line.startswith('@SQ') or line.startswith('@RG') ] post_pg_header = [line.strip() for line in samheader if not line.startswith('@HD') and (not line.startswith('@SQ')) and (not line.startswith('@RG')) and (not line.startswith('@PG')) ] pg_chains = _parse_pg_chains(samheader, force=force) for i,br in enumerate(pg_chains): new_pg = {'ID':ID, 'PN':PN, 'VN':VN, 'CL':CL} new_pg['PP'] = br[-1]['ID'] if len(pg_chains) > 1: new_pg['ID'] = new_pg['ID'] + '-' + str(i+1) + '.' + str(len(br)+1) new_pg['raw'] = _format_pg(**new_pg) br.append(new_pg) new_header = ( pre_pg_header + [pg['raw'] for br in pg_chains for pg in br] + post_pg_header) return new_header def _format_pg(**kwargs): out = ( ['@PG'] + ['{}:{}'.format(field, kwargs[field]) for field in ['ID', 'PN', 'CL', 'PP', 'DS', 'VN'] if field in kwargs]) return '\t'.join(out) def _parse_pg_chains(header, force=False): pg_chains = [] parsed_pgs = [ dict( [field.split(':', maxsplit=1) for field in l.strip().split('\t')[1:]] + [('raw', l.strip())] ) for l in header if l.startswith('@PG') ] while True: if len(parsed_pgs) == 0: break for i in range(len(parsed_pgs)): pg = parsed_pgs[i] if 'PP' not in pg: pg_chains.append([pg]) parsed_pgs.pop(i) break else: matching_chains = [ branch for branch in pg_chains if branch[-1]['ID'] == pg['PP'] ] if len(matching_chains) > 1: if force: matching_chains[0].append(pg) parsed_pgs.pop(i) break else: raise ParseError( 'Multiple @PG records with the IDs identical to the PP field of another record:\n' + '\n'.join([br[-1]['raw'] for br in matching_chains]) + '\nvs\n' + pg['raw'] ) if len(matching_chains) == 1: matching_chains[0].append(pg) parsed_pgs.pop(i) break if force: pg_chains.append([pg]) parsed_pgs.pop(i) break else: raise ParseError( 'Cannot find the parental @PG record for the @PG records:\n' + '\n'.join([pg['raw'] for pg in parsed_pgs]) ) return pg_chains def _toposort(dag, tie_breaker): """ Topological sort on a directed acyclic graph Uses Kahn's algorithm with a custom tie-breaking option. The dictionary ``dag`` can be interpreted in two ways: 1. A dependency graph (i.e. arcs point from values to keys), and the generator yields items with no dependences followed by items that depend on previous ones. 2. Arcs point from keys to values, in which case the generator produces a **reverse** topological ordering of the nodes. Parameters ---------- dag: dict of nodes to sets of nodes Directed acyclic graph encoded as a dictionary. tie_breaker: callable Function that picks a tie breaker from a set of nodes with no unprocessed dependences. Returns ------- Generator Notes ----- See . Based in part on activestate recipe: by Sam Denton (MIT licensed). """ # Drop self-edges. for k, v in dag.items(): v.discard(k) # Find all nodes that don't depend on anything # and include them with empty dependencies. indep_nodes = set.union(*dag.values()) - set(dag.keys()) dag.update({node: set() for node in indep_nodes}) while True: if not indep_nodes: break out = tie_breaker(indep_nodes) indep_nodes.discard(out) del dag[out] yield out for node, deps in dag.items(): deps.discard(out) if len(deps) == 0: indep_nodes.add(node) if len(dag) != 0: raise ValueError( 'Circular dependencies exist: {} '.format(list(dag.items()))) def merge_chrom_lists(*lsts): sentinel = '!NONE!' g = defaultdict(set) for lst in lsts: if len(lst) == 1: g[lst[0]].add(sentinel) for a, b in zip(lst[:-1], lst[1:]): g[b].add(a) if len(g) == 0: return [] chrom_list = list(_toposort(g.copy(), tie_breaker=min)) if sentinel in chrom_list: chrom_list.remove(sentinel) return chrom_list def _merge_samheaders(samheaders, force=False): # first, append an HD line if it is present in any files # if different lines are present, raise an error HDs = set.union(*[set(line for line in samheader if line.startswith('@HD')) for samheader in samheaders]) if len(HDs) > 1 and not force: raise ParseError('More than one unique @HD line is found in samheaders!') HDs = [list(HDs)[0]] if HDs else [] # second, confirm that all files had the same SQ lines # add SQs from the first file, keeping its order SQs = [set(line for line in samheader if line.startswith('@SQ')) for samheader in samheaders] common_SQs = set.intersection(*SQs) SQs_same = all([len(samheader) == len(common_SQs) for samheader in SQs]) if not SQs_same and not(force): raise ParseError('The SQ (sequence) lines of the sam headers are not identical') SQs = [line for line in samheaders[0] if line.startswith('@SQ')] # third, append _all_ PG chains, adding a unique index according to the # provided merging order PGs = [] for i, samheader in enumerate(samheaders): for line in samheader: if line.startswith('@PG'): split_line = line.split('\t') for j in range(len(split_line)): if (split_line[j].startswith('ID:') or split_line[j].startswith('PP:')): split_line[j] = split_line[j] + '-' + str(i+1) PGs.append('\t'.join(split_line)) # finally, add all residual unique lines rest = sum([ list(set(line for line in samheader if (not line.startswith('@HD')) and (not line.startswith('@SQ')) and (not line.startswith('@PG')) )) for samheader in samheaders], []) new_header = [] new_header += HDs new_header += SQs new_header += PGs new_header += rest return new_header def _merge_pairheaders(pairheaders, force=False): new_header = [] # first, add all keys that are expected to be the same among all headers keys_expected_identical = [ '## pairs format', '#sorted:', '#shape:', '#genome_assembly:', '#columns:' ] for k in keys_expected_identical: lines = [[l for l in header if l.startswith(k)] for header in pairheaders] same = all([l == lines[0] for l in lines]) if not (same or force): raise ParseError( 'The following header entries must be the same ' 'the merged files: {}'.format(k)) new_header += lines[0] # second, merge and add the chromsizes fields. chrom_lists = [] chromsizes = {} for header in pairheaders: chromlist = [] for line in header: if line.startswith('#chromsize:'): chrom, length = line.strip('#chromsize:').split() chromsizes[chrom] = length chromlist.append(chrom) chrom_lists.append(chromlist) chroms_merged = merge_chrom_lists(*chrom_lists) chrom_lines = ["#chromsize: {} {}".format(chrom, chromsizes[chrom]) for chrom in chroms_merged] new_header.extend(chrom_lines) # finally, add a sorted list of other unique fields other_lines = sorted(set( l for h in pairheaders for l in h if not any(l.startswith(k) for k in keys_expected_identical + ['#chromsize']))) if other_lines: if new_header[-1].startswith('#columns'): new_header = new_header[:-1] + other_lines + [new_header[-1]] else: new_header = new_header + other_lines return new_header def merge_headers(headers, force=False): samheaders, pairheaders = zip(*[extract_fields(h, 'samheader', save_rest=True) for h in headers]) # HD headers contain information that becomes invalid after processing # with distiller. Do not print into the output. new_pairheader = _merge_pairheaders(pairheaders, force=False) new_samheader = _merge_samheaders(samheaders, force=force) new_header = insert_samheader(new_pairheader, new_samheader) return new_header #def _guess_genome_assembly(samheader): # PG = [l for l in samheader if l.startswith('@PG') and '\tID:bwa' in l][0] # CL = [field for field in PG.split('\t') if field.startswith('CL:')] # # return ga pairtools-0.3.0/pairtools/_pairsam_format.py000066400000000000000000000007151345765554600213060ustar00rootroot00000000000000PAIRSAM_FORMAT_VERSION = '1.0.0' PAIRSAM_SEP = '\t' PAIRSAM_SEP_ESCAPE = r'\t' SAM_SEP = '\031' SAM_SEP_ESCAPE = r'\031' INTER_SAM_SEP = '\031NEXT_SAM\031' COL_READID = 0 COL_C1 = 1 COL_P1 = 2 COL_C2 = 3 COL_P2 = 4 COL_S1 = 5 COL_S2 = 6 COL_PTYPE = 7 COL_SAM1 = 8 COL_SAM2 = 9 COLUMNS = ['readID', 'chrom1', 'pos1', 'chrom2', 'pos2', 'strand1', 'strand2', 'pair_type', 'sam1', 'sam2'] UNMAPPED_CHROM = '!' UNMAPPED_POS = 0 UNMAPPED_STRAND = '-' pairtools-0.3.0/pairtools/pairtools_dedup.py000066400000000000000000000366331345765554600213500ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import sys import ast import warnings import pathlib import click import numpy as np from . import _dedup, _fileio, _pairsam_format, _headerops, cli, common_io_options from .pairtools_markasdup import mark_split_pair_as_dup from .pairtools_stats import PairCounter UTIL_NAME = 'pairtools_dedup' # you don't need to load more than 10k lines at a time b/c you get out of the # CPU cache, so this parameter is not adjustable MAX_LEN = 10000 @cli.command() @click.argument( 'pairs_path', type=str, required=False) @click.option( "-o", "--output", type=str, default="", help='output file for pairs after duplicate removal.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' By default, the output is printed into stdout.') @click.option( "--output-dups", type=str, default="", help='output file for duplicated pairs. ' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' If the path is the same as in --output or -, output duplicates together ' ' with deduped pairs. By default, duplicates are dropped.') @click.option( "--output-unmapped", type=str, default="", help='output file for unmapped pairs. ' 'If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed. ' 'If the path is the same as in --output or -, output unmapped pairs together ' 'with deduped pairs. If the path is the same as --output-dups, output ' 'unmapped reads together with dups. By default, unmapped pairs are dropped.') @click.option( "--output-stats", type=str, default="", help='output file for duplicate statistics. ' ' If file exists, it will be open in the append mode.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' By default, statistics are not printed.') @click.option( "--max-mismatch", type=int, default=3, show_default=True, help='Pairs with both sides mapped within this distance (bp) from each ' 'other are considered duplicates.') @click.option( '--method', type=click.Choice(['max', 'sum']), default="max", help='define the mismatch as either the max or the sum of the mismatches of' 'the genomic locations of the both sides of the two compared molecules', show_default=True,) @click.option( "--sep", type=str, default=_pairsam_format.PAIRSAM_SEP_ESCAPE, help=r"Separator (\t, \v, etc. characters are " "supported, pass them in quotes) ") @click.option( "--comment-char", type=str, default="#", help="The first character of comment lines") @click.option( "--send-header-to", type=click.Choice(['dups', 'dedup', 'both', 'none']), default="both", help="Which of the outputs should receive header and comment lines") @click.option( "--c1", type=int, default=_pairsam_format.COL_C1, help='Chrom 1 column; default {}'.format(_pairsam_format.COL_C1)) @click.option( "--c2", type=int, default=_pairsam_format.COL_C2, help='Chrom 2 column; default {}'.format(_pairsam_format.COL_C2)) @click.option( "--p1", type=int, default=_pairsam_format.COL_P1, help='Position 1 column; default {}'.format(_pairsam_format.COL_P1)) @click.option( "--p2", type=int, default=_pairsam_format.COL_P2, help='Position 2 column; default {}'.format(_pairsam_format.COL_P2)) @click.option( "--s1", type=int, default=_pairsam_format.COL_S1, help='Strand 1 column; default {}'.format(_pairsam_format.COL_S1)) @click.option( "--s2", type=int, default=_pairsam_format.COL_S2, help='Strand 2 column; default {}'.format(_pairsam_format.COL_S2)) @click.option( "--unmapped-chrom", type=str, default=_pairsam_format.UNMAPPED_CHROM, help='Placeholder for a chromosome on an unmapped side; default {}'.format(_pairsam_format.UNMAPPED_CHROM)) @click.option( "--mark-dups", is_flag=True, help='If specified, duplicate pairs are marked as DD in "pair_type" and ' 'as a duplicate in the sam entries.') @click.option( "--extra-col-pair", nargs=2, #type=click.Tuple([str, str]), multiple=True, help='Extra columns that also must match for two pairs to be marked as ' 'duplicates. Can be either provided as 0-based column indices or as column ' 'names (requires the "#columns" header field). The option can be provided ' 'multiple times if multiple column pairs must match. ' 'Example: --extra-col-pair "phase1" "phase2"' ) @common_io_options def dedup(pairs_path, output, output_dups, output_unmapped, output_stats, max_mismatch, method, sep, comment_char, send_header_to, c1, c2, p1, p2, s1, s2, unmapped_chrom, mark_dups, extra_col_pair, **kwargs ): '''Find and remove PCR/optical duplicates. Find PCR duplicates in an upper-triangular flipped sorted pairs/pairsam file. Allow for a +/-N bp mismatch at each side of duplicated molecules. PAIRS_PATH : input triu-flipped sorted .pairs or .pairsam file. If the path ends with .gz/.lz4, the input is decompressed by pbgzip/lz4c. By default, the input is read from stdin. ''' dedup_py(pairs_path, output, output_dups, output_unmapped, output_stats, max_mismatch, method, sep, comment_char, send_header_to, c1, c2, p1, p2, s1, s2, unmapped_chrom, mark_dups, extra_col_pair, **kwargs ) def dedup_py( pairs_path, output, output_dups, output_unmapped, output_stats, max_mismatch, method, sep, comment_char, send_header_to, c1, c2, p1, p2, s1, s2, unmapped_chrom, mark_dups, extra_col_pair, **kwargs ): sep = ast.literal_eval('"""' + sep + '"""') send_header_to_dedup = send_header_to in ['both', 'dedup'] send_header_to_dup = send_header_to in ['both', 'dups'] instream = (_fileio.auto_open(pairs_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if pairs_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) out_stats_stream = (_fileio.auto_open(output_stats, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output_stats else None) # generate empty PairCounter if stats output is requested: out_stat = PairCounter() if output_stats else None if not output_dups: outstream_dups = None elif (output_dups == '-' or (pathlib.Path(output_dups).absolute() == pathlib.Path(output).absolute())): outstream_dups = outstream else: outstream_dups = _fileio.auto_open(output_dups, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if not output_unmapped: outstream_unmapped = None elif (output_unmapped == '-' or (pathlib.Path(output_unmapped).absolute() == pathlib.Path(output).absolute())): outstream_unmapped = outstream elif (pathlib.Path(output_unmapped).absolute() == pathlib.Path(output_dups).absolute()): outstream_unmapped = outstream_dups else: outstream_unmapped = _fileio.auto_open(output_unmapped, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) header, body_stream = _headerops.get_header(instream) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) if send_header_to_dedup: outstream.writelines((l+'\n' for l in header)) if send_header_to_dup and outstream_dups and (outstream_dups != outstream): outstream_dups.writelines((l+'\n' for l in header)) if (outstream_unmapped and (outstream_unmapped != outstream) and (outstream_unmapped != outstream_dups)): outstream_unmapped.writelines((l+'\n' for l in header)) column_names = _headerops.extract_column_names(header) extra_cols1 = [] extra_cols2 = [] if extra_col_pair is not None: for col1, col2 in extra_col_pair: extra_cols1.append( int(col1) if col1.isdigit() else column_names.index(col1)) extra_cols2.append( int(col2) if col2.isdigit() else column_names.index(col2)) streaming_dedup( method, max_mismatch, sep, c1, c2, p1, p2, s1, s2, extra_cols1, extra_cols2, unmapped_chrom, body_stream, outstream, outstream_dups, outstream_unmapped, out_stat, mark_dups) # save statistics to a file if it was requested: if out_stat: out_stat.save(out_stats_stream) if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() if outstream_dups and (outstream_dups != outstream): outstream_dups.close() if (outstream_unmapped and (outstream_unmapped != outstream) and (outstream_unmapped != outstream_dups)): outstream_unmapped.close() if out_stats_stream: out_stats_stream.close() def fetchadd(key, mydict): key = key.strip() if key not in mydict: mydict[key] = len(mydict) return mydict[key] def ar(mylist, val): return np.array(mylist, dtype={8: np.int8, 16: np.int16, 32: np.int32}[val]) def streaming_dedup( method, max_mismatch, sep, c1ind, c2ind, p1ind, p2ind, s1ind, s2ind, extra_cols1, extra_cols2, unmapped_chrom, instream, outstream, outstream_dups, outstream_unmapped, out_stat, mark_dups): maxind = max(c1ind, c2ind, p1ind, p2ind, s1ind, s2ind) if bool(extra_cols1) and bool(extra_cols2): maxind = max(maxind, max(extra_cols1), max(extra_cols2)) all_scols1 = [s1ind] + extra_cols1 all_scols2 = [s2ind] + extra_cols2 # if we do stats in the dedup, we need PAIR_TYPE # i do not see way around this: if out_stat: ptind = _pairsam_format.COL_PTYPE maxind = max(maxind, ptind) dd = _dedup.OnlineDuplicateDetector(method, max_mismatch, returnData=False) c1 = []; c2 = []; p1 = []; p2 = []; s1 = []; s2 = [] line_buffer = [] cols_buffer = [] chromDict = {} strandDict = {} n_unmapped = 0 n_dups = 0 n_nodups = 0 curMaxLen = max(MAX_LEN, dd.getLen()) instream = iter(instream) while True: rawline = next(instream, None) stripline = rawline.strip() if rawline else None # take care of empty lines not at the end of the file separately if rawline and (not stripline): warnings.warn("Empty line detected not at the end of the file") continue if stripline: cols = stripline.split(sep) if len(cols) <= maxind: raise ValueError( "Error parsing line {}: ".format(stripline) + " expected {} columns, got {}".format(maxind, len(cols))) if ((cols[c1ind] == unmapped_chrom) or (cols[c2ind] == unmapped_chrom)): if outstream_unmapped: outstream_unmapped.write(stripline) # don't forget terminal newline outstream_unmapped.write("\n") # add a pair to PairCounter if stats output is requested: if out_stat: out_stat.add_pair(cols[c1ind], int(cols[p1ind]), cols[s1ind], cols[c2ind], int(cols[p2ind]), cols[s2ind], cols[ptind]) else: line_buffer.append(stripline) cols_buffer.append(cols) c1.append(fetchadd(cols[c1ind], chromDict)) c2.append(fetchadd(cols[c2ind], chromDict)) p1.append(int(cols[p1ind])) p2.append(int(cols[p2ind])) if bool(extra_cols1) and bool(extra_cols2): s1.append(fetchadd(''.join(cols[i] for i in all_scols1), strandDict)) s2.append(fetchadd(''.join(cols[i] for i in all_scols2), strandDict)) else: s1.append(fetchadd(cols[s1ind], strandDict)) s2.append(fetchadd(cols[s2ind], strandDict)) if (not stripline) or (len(c1) == curMaxLen): res = dd.push(ar(c1, 32), ar(c2, 32), ar(p1, 32), ar(p2, 32), ar(s1, 32), ar(s2, 32)) if not stripline: res = np.concatenate([res, dd.finish()]) for i in range(len(res)): # not duplicated pair: if not res[i]: outstream.write(line_buffer[i]) # don't forget terminal newline outstream.write("\n") if out_stat: out_stat.add_pair(cols_buffer[i][c1ind], int(cols_buffer[i][p1ind]), cols_buffer[i][s1ind], cols_buffer[i][c2ind], int(cols_buffer[i][p2ind]), cols_buffer[i][s2ind], cols_buffer[i][ptind]) # duplicated pair: else: if out_stat: out_stat.add_pair(cols_buffer[i][c1ind], int(cols_buffer[i][p1ind]), cols_buffer[i][s1ind], cols_buffer[i][c2ind], int(cols_buffer[i][p2ind]), cols_buffer[i][s2ind], 'DD' ) if outstream_dups: outstream_dups.write( # DD-marked pair: sep.join(mark_split_pair_as_dup(cols_buffer[i])) if mark_dups # pair as is: else line_buffer[i] ) # don't forget terminal newline outstream_dups.write('\n') # flush buffers and perform necessary checks here: c1 = []; c2 = []; p1 = []; p2 = []; s1 = []; s2 = [] line_buffer = line_buffer[len(res):] cols_buffer = cols_buffer[len(res):] if not stripline: if(len(line_buffer) != 0): raise ValueError( "{} lines left in the buffer, ".format(len(line_buffer)) + "should be none;" + "something went terribly wrong") break # process next line ... # all lines have been processed at this point. # streaming_dedup is over. if __name__ == '__main__': dedup() pairtools-0.3.0/pairtools/pairtools_filterbycov.py000066400000000000000000000416571345765554600226010ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import sys import ast import warnings import pathlib import click import numpy as np from . import _dedup, _fileio, _pairsam_format, _headerops, cli, common_io_options from .pairtools_markasdup import mark_split_pair_as_dup from .pairtools_stats import PairCounter UTIL_NAME = 'pairtools_filterbycov' ###################################### ## TODO: - output stats after filtering ## edit/update mark as dup to mark as multi ################################### @cli.command() @click.argument( 'pairs_path', type=str, required=False) @click.option( "-o", "--output", type=str, default="", help='output file for pairs from low coverage regions.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' By default, the output is printed into stdout.') @click.option( "--output-highcov", type=str, default="", help='output file for pairs from high coverage regions.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' If the path is the same as in --output or -, output duplicates together ' ' with deduped pairs. By default, duplicates are dropped.') @click.option( "--output-unmapped", type=str, default="", help='output file for unmapped pairs. ' 'If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed. ' 'If the path is the same as in --output or -, output unmapped pairs together ' 'with deduped pairs. If the path is the same as --output-highcov, ' 'output unmapped reads together. By default, unmapped pairs are dropped.') @click.option( "--output-stats", type=str, default="", help='output file for statistics of multiple interactors. ' ' If file exists, it will be open in the append mode.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' By default, statistics are not printed.') @click.option( "--max-cov", type=int, default=8, help='The maximum allowed coverage per region.' ) @click.option( "--max-dist", type=int, default=500, help='The resolution for calculating coverage. For each pair, the local ' 'coverage around each end is calculated as (1 + the number of neighbouring ' 'pairs within +/- max_dist bp) ') @click.option( '--method', type=click.Choice(['max', 'sum']), default="max", help='calculate the number of neighbouring pairs as either the sum or the max' ' of the number of neighbours on the two sides', show_default=True) @click.option( "--sep", type=str, default=_pairsam_format.PAIRSAM_SEP_ESCAPE, help=r"Separator (\t, \v, etc. characters are " "supported, pass them in quotes) ") @click.option( "--comment-char", type=str, default="#", help="The first character of comment lines") @click.option( "--send-header-to", type=click.Choice(['lowcov', 'highcov', 'both', 'none']), default="both", help="Which of the outputs should receive header and comment lines") @click.option( "--c1", type=int, default=_pairsam_format.COL_C1, help='Chrom 1 column; default {}'.format(_pairsam_format.COL_C1)) @click.option( "--c2", type=int, default=_pairsam_format.COL_C2, help='Chrom 2 column; default {}'.format(_pairsam_format.COL_C2)) @click.option( "--p1", type=int, default=_pairsam_format.COL_P1, help='Position 1 column; default {}'.format(_pairsam_format.COL_P1)) @click.option( "--p2", type=int, default=_pairsam_format.COL_P2, help='Position 2 column; default {}'.format(_pairsam_format.COL_P2)) @click.option( "--s1", type=int, default=_pairsam_format.COL_S1, help='Strand 1 column; default {}'.format(_pairsam_format.COL_S1)) @click.option( "--s2", type=int, default=_pairsam_format.COL_S2, help='Strand 2 column; default {}'.format(_pairsam_format.COL_S2)) @click.option( "--unmapped-chrom", type=str, default=_pairsam_format.UNMAPPED_CHROM, help='Placeholder for a chromosome on an unmapped side; default {}'.format(_pairsam_format.UNMAPPED_CHROM)) @click.option( "--mark-multi", is_flag=True, help='If specified, duplicate pairs are marked as FF in "pair_type" and ' 'as a duplicate in the sam entries.') @common_io_options def filterbycov( pairs_path, output, output_highcov, output_unmapped, output_stats, max_dist,max_cov, method, sep, comment_char, send_header_to, c1, c2, p1, p2, s1, s2, unmapped_chrom, mark_multi, **kwargs ): '''Remove pairs from regions of high coverage. Find and remove pairs with >(MAX_COV-1) neighbouring pairs within a +/- MAX_DIST bp window around either side. Useful for single-cell Hi-C experiments, where coverage is naturally limited by the chromosome copy number. PAIRS_PATH : input triu-flipped sorted .pairs or .pairsam file. If the path ends with .gz/.lz4, the input is decompressed by pbgzip/lz4c. By default, the input is read from stdin. ''' filterbycov_py( pairs_path, output, output_highcov, output_unmapped,output_stats, max_dist,max_cov, method, sep, comment_char, send_header_to, c1, c2, p1, p2, s1, s2, unmapped_chrom, mark_multi, **kwargs ) def filterbycov_py( pairs_path, output, output_highcov, output_unmapped, output_stats, max_dist,max_cov, method, sep, comment_char, send_header_to, c1, c2, p1, p2, s1, s2, unmapped_chrom, mark_multi, **kwargs ): ## Prepare input, output streams based on selected outputs ## Default ouput stream is low-frequency interactors sep = ast.literal_eval('"""' + sep + '"""') send_header_to_lowcov = send_header_to in ['both', 'lowcov'] send_header_to_highcov = send_header_to in ['both', 'highcov'] instream = (_fileio.auto_open(pairs_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if pairs_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) out_stats_stream = (_fileio.auto_open(output_stats, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output_stats else None) # generate empty PairCounter if stats output is requested: out_stat = PairCounter() if output_stats else None # output the high-frequency interacting pairs if not output_highcov: outstream_high = None elif (output_highcov == '-' or (pathlib.Path(output_highcov).absolute() == pathlib.Path(output).absolute())): outstream_high = outstream else: outstream_high = _fileio.auto_open(output_highcov, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) # output unmapped pairs if not output_unmapped: outstream_unmapped = None elif (output_unmapped == '-' or (pathlib.Path(output_unmapped).absolute() == pathlib.Path(output).absolute())): outstream_unmapped = outstream elif (pathlib.Path(output_unmapped).absolute() == pathlib.Path(output_highcov).absolute()): outstream_unmapped = outstream_high else: outstream_unmapped = _fileio.auto_open(output_unmapped, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) # prepare file headers header, body_stream = _headerops.get_header(instream) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) # header for low-frequency interactors if send_header_to_lowcov: outstream.writelines((l+'\n' for l in header)) # header for high-frequency interactors if send_header_to_highcov and outstream_high and (outstream_high != outstream): outstream_high.writelines((l+'\n' for l in header)) # header for unmapped pairs if (outstream_unmapped and (outstream_unmapped != outstream) and (outstream_unmapped != outstream_high)): outstream_unmapped.writelines((l+'\n' for l in header)) # perform filtering of pairs based on low/high-frequency of interaction streaming_filterbycov( method, max_dist,max_cov, sep, c1, c2, p1, p2, s1, s2, unmapped_chrom, body_stream, outstream, outstream_high, outstream_unmapped, out_stat, mark_multi) ## FINISHED! # save statistics to a file if it was requested: TO BE TESTED if out_stat: out_stat.save(out_stats_stream) if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() if outstream_high and (outstream_high != outstream): outstream_high.close() if (outstream_unmapped and (outstream_unmapped != outstream) and (outstream_unmapped != outstream_high)): outstream_unmapped.close() if out_stats_stream: out_stats_stream.close() def fetchadd(key, mydict): key = key.strip() if key not in mydict: mydict[key] = len(mydict) return mydict[key] def ar(mylist, val): return np.array(mylist, dtype={8: np.int8, 16: np.int16, 32: np.int32}[val]) def _filterbycov(c1_in, p1_in, c2_in, p2_in, max_dist, method): """ This is a slow version of the filtering code used for testing purposes only Use cythonized version in the future!! """ c1 = np.asarray(c1_in,dtype=int) p1 = np.asarray(p1_in,dtype=int) c2 = np.asarray(c2_in,dtype=int) p2 = np.asarray(p2_in,dtype=int) M = np.r_[np.c_[c1,p1],np.c_[c2,p2]] # M is a table of (chrom, pos) with 2*N rows assert (c1.shape[0] == c2.shape[0]) N = 2*c1.shape[0] ind_sorted = np.lexsort((M[:,1],M[:,0])) # sort by chromosomes, then positions # M[ind_sorted] # ind_sorted # M, M[ind_sorted] if (method == 'sum'): proximity_count = np.zeros(N) # keeps track of how many molecules each framgent end is close to elif (method == 'max'): proximity_count = np.zeros(N) else: raise ValueError('Unknown method: {}'.format(method)) low = 0 high = 1 while True: # boundary case finish if low == N: break # boundary case - CHECK if high == N: low += 1 high = low + 1 continue # check if "high" is proximal enough to "low" # first, if chromosomes not equal, we have gone too far, and the positions are not proximal if M[ind_sorted[low],0] != M[ind_sorted[high],0]: low += 1 high = low + 1 # restart high continue # next, if positions are not proximal, increase low, and continue elif np.abs(M[ind_sorted[high],1] - M[ind_sorted[low],1]) > max_dist: low += 1 high = low + 1 # restart high continue # if on the same chromosome, and the distance is "proximal enough", add to count of both "low" and "high" positions else: proximity_count[low] += 1 proximity_count[high] += 1 high += 1 # unsort proximity count #proximity_count = proximity_count[ind_sorted] proximity_count[ind_sorted] = np.copy(proximity_count) #print(M) #print(proximity_count) # if method is sum of pairs if method == 'sum': pcounts = proximity_count[0:N//2] + proximity_count[N//2:] + 1 elif method == 'max': pcounts = np.maximum(proximity_count[0:N//2]+1, proximity_count[N//2:]+1) else: raise ValueError('Unknown method: {}'.format(method)) return pcounts def streaming_filterbycov( method, max_dist, max_cov, sep, c1ind, c2ind, p1ind, p2ind, s1ind, s2ind, unmapped_chrom, instream, outstream, outstream_high, outstream_unmapped, out_stat, mark_multi): # doing everything in memory maxind = max(c1ind, c2ind, p1ind, p2ind, s1ind, s2ind) # if we do stats in the dedup, we need PAIR_TYPE # i do not see way around this: if out_stat: ptind = _pairsam_format.COL_PTYPE maxind = max(maxind, ptind) c1 = []; c2 = []; p1 = []; p2 = []; s1 = []; s2 = [] line_buffer = [] cols_buffer = [] chromDict = {} strandDict = {} n_unmapped = 0 n_high = 0 n_low = 0 instream = iter(instream) while True: rawline = next(instream, None) stripline = rawline.strip() if rawline else None # take care of empty lines not at the end of the file separately if rawline and (not stripline): warnings.warn("Empty line detected not at the end of the file") continue if stripline: cols = stripline.split(sep) if len(cols) <= maxind: raise ValueError( "Error parsing line {}: ".format(stripline) + " expected {} columns, got {}".format(maxind, len(cols))) if ((cols[c1ind] == unmapped_chrom) or (cols[c2ind] == unmapped_chrom)): if outstream_unmapped: outstream_unmapped.write(stripline) # don't forget terminal newline outstream_unmapped.write("\n") # add a pair to PairCounter if stats output is requested: if out_stat: out_stat.add_pair(cols[c1ind], int(cols[p1ind]), cols[s1ind], cols[c2ind], int(cols[p2ind]), cols[s2ind], cols[ptind]) else: line_buffer.append(stripline) cols_buffer.append(cols) c1.append(fetchadd(cols[c1ind], chromDict)) c2.append(fetchadd(cols[c2ind], chromDict)) p1.append(int(cols[p1ind])) p2.append(int(cols[p2ind])) s1.append(fetchadd(cols[s1ind], strandDict)) s2.append(fetchadd(cols[s2ind], strandDict)) else: # when everything is loaded in memory... res = _filterbycov(c1, p1, c2, p2, max_dist, method) for i in range(len(res)): # not high-frequency interactor pairs: if not res[i] > max_cov: outstream.write(line_buffer[i]) # don't forget terminal newline outstream.write("\n") if out_stat: out_stat.add_pair(cols_buffer[i][c1ind], int(cols_buffer[i][p1ind]), cols_buffer[i][s1ind], cols_buffer[i][c2ind], int(cols_buffer[i][p2ind]), cols_buffer[i][s2ind], cols_buffer[i][ptind]) # high-frequency interactor pairs: else: if out_stat: out_stat.add_pair(cols_buffer[i][c1ind], int(cols_buffer[i][p1ind]), cols_buffer[i][s1ind], cols_buffer[i][c2ind], int(cols_buffer[i][p2ind]), cols_buffer[i][s2ind], 'FF' ) if outstream_high: outstream_high.write( # FF-marked pair: sep.join(mark_split_pair_as_dup(cols_buffer[i])) if mark_multi # pair as is: else line_buffer[i] ) # don't forget terminal newline outstream_high.write('\n') # flush buffers and perform necessary checks here: c1 = []; c2 = []; p1 = []; p2 = []; s1 = []; s2 = [] line_buffer = line_buffer[len(res):] cols_buffer = cols_buffer[len(res):] if not stripline: if(len(line_buffer) != 0): raise ValueError( "{} lines left in the buffer, ".format(len(line_buffer)) + "should be none;" + "something went terribly wrong") break break if __name__ == '__main__': filterbycov() pairtools-0.3.0/pairtools/pairtools_flip.py000066400000000000000000000077501345765554600211770ustar00rootroot00000000000000import sys import click from . import _fileio, _pairsam_format, cli, _headerops, common_io_options UTIL_NAME = 'pairtools_flip' @cli.command() @click.argument( 'pairs_path', type=str, required=False) @click.option( "-c", "--chroms-path", type=str, required=True, help='Chromosome order used to flip interchromosomal mates: ' 'path to a chromosomes file (e.g. UCSC chrom.sizes or similar) whose ' 'first column lists scaffold names. Any scaffolds not listed will be ' 'ordered lexicographically following the names provided.') @click.option( '-o', "--output", type=str, default="", help='output file.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' By default, the output is printed into stdout.') @common_io_options def flip( pairs_path, chroms_path, output, **kwargs ): '''Flip pairs to get an upper-triangular matrix. Change the order of side1 and side2 in pairs, such that (order(chrom1) < order(chrom2) or (order(chrom1) == order(chrom2)) and (pos1 <=pos2)) Equivalent to reflecting the lower triangle of a Hi-C matrix onto its upper triangle, resulting in an upper triangular matrix. The order of chromosomes must be provided via a .chromsizes file. PAIRS_PATH : input .pairs/.pairsam file. If the path ends with .gz or .lz4, the input is decompressed by pbgzip/lz4c. By default, the input is read from stdin. ''' flip_py( pairs_path, chroms_path, output, **kwargs ) def flip_py( pairs_path, chroms_path, output, **kwargs ): instream = (_fileio.auto_open(pairs_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if pairs_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) chromosomes = _headerops.get_chrom_order(chroms_path) chrom_enum = dict(zip([_pairsam_format.UNMAPPED_CHROM] + list(chromosomes), range(len(chromosomes)+1))) header, body_stream = _headerops.get_header(instream) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) outstream.writelines((l+'\n' for l in header)) column_names = _headerops.extract_column_names(header) if len(column_names) == 0: column_names = _pairsam_format.COLUMNS chrom1_col = column_names.index('chrom1') chrom2_col = column_names.index('chrom2') pos1_col = column_names.index('pos1') pos2_col = column_names.index('pos2') pair_type_col = (column_names.index('pair_type') if 'pair_type' in column_names else -1) col_pairs_to_flip = [ (column_names.index(col), column_names.index(col[:-1]+'2')) for col in column_names if col.endswith('1') and (col[:-1]+'2') in column_names] for line in body_stream: cols = line.rstrip().split(_pairsam_format.PAIRSAM_SEP) has_correct_order = ( (chrom_enum[cols[chrom1_col]], int(cols[pos1_col])) <= (chrom_enum[cols[chrom2_col]], int(cols[pos2_col])) ) if not has_correct_order: for col1, col2 in col_pairs_to_flip: if (col1 < len(cols)) and (col2 < len(cols)): cols[col1], cols[col2] = cols[col2], cols[col1] if pair_type_col != -1 and pair_type_col < len(cols): cols[pair_type_col] = ( cols[pair_type_col][1] + cols[pair_type_col][0]) outstream.write(_pairsam_format.PAIRSAM_SEP.join(cols)) outstream.write('\n') if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() if __name__ == '__main__': pair() pairtools-0.3.0/pairtools/pairtools_markasdup.py000066400000000000000000000064241345765554600222310ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import sys import pipes import click from . import _fileio, _pairsam_format, cli, _headerops, common_io_options UTIL_NAME = 'pairtools_markasdup' @cli.command() @click.argument( 'pairsam_path', type=str, required=False) @click.option( "-o", "--output", type=str, default="", help='output .pairsam file.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' By default, the output is printed into stdout.') @common_io_options def markasdup(pairsam_path, output, **kwargs): '''Tag pairs as duplicates. Change the type of all pairs inside a .pairs/.pairsam file to DD. If sam entries are present, change the pair type in the Yt SAM tag to 'Yt:Z:DD'. PAIRSAM_PATH : input .pairs/.pairsam file. If the path ends with .gz, the input is gzip-decompressed. By default, the input is read from stdin. ''' markasdup_py(pairsam_path, output, **kwargs) def markasdup_py(pairsam_path, output, **kwargs): instream = (_fileio.auto_open(pairsam_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if pairsam_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) header, body_stream = _headerops.get_header(instream) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) outstream.writelines((l+'\n' for l in header)) for line in body_stream: cols = line.rstrip().split(_pairsam_format.PAIRSAM_SEP) mark_split_pair_as_dup(cols) outstream.write(_pairsam_format.PAIRSAM_SEP.join(cols)) outstream.write('\n') if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() def mark_split_pair_as_dup(cols): # if the original columns ended with a new line, the marked columns # should as well. original_has_newline = cols[-1].endswith('\n') cols[_pairsam_format.COL_PTYPE] = 'DD' if (len(cols) > _pairsam_format.COL_SAM1) and (len(cols) > _pairsam_format.COL_SAM2): for i in (_pairsam_format.COL_SAM1, _pairsam_format.COL_SAM2): # split each sam column into sam entries, tag and assemble back cols[i] = _pairsam_format.INTER_SAM_SEP.join( [mark_sam_as_dup(sam) for sam in cols[i].split(_pairsam_format.INTER_SAM_SEP) ]) if original_has_newline and not cols[-1].endswith('\n'): cols[-1] = cols[-1]+'\n' return cols def mark_sam_as_dup(sam): '''Tag the binary flag and the optional pair type field of a sam entry as a PCR duplicate.''' samcols = sam.split(_pairsam_format.SAM_SEP) if len(samcols) == 1: return sam samcols[1] = str(int(samcols[1]) | 1024) for j in range(11, len(samcols)): if samcols[j].startswith('Yt:Z:'): samcols[j] = 'Yt:Z:DD' return _pairsam_format.SAM_SEP.join(samcols) if __name__ == '__main__': markasdup() pairtools-0.3.0/pairtools/pairtools_merge.py000066400000000000000000000144721345765554600213430ustar00rootroot00000000000000#!/usr/bin/env python import sys import glob import math import subprocess import click from . import _fileio, _pairsam_format, _headerops, cli UTIL_NAME = 'pairtools_merge' @cli.command() @click.argument( 'pairs_path', nargs=-1, type=str, ) @click.option( "-o", "--output", type=str, default="", help='output file.' ' If the path ends with .gz/.lz4, the output is compressed by pbgzip/lz4c.' ' By default, the output is printed into stdout.') @click.option( "--max-nmerge", type=int, default=8, show_default=True, help='The maximal number of inputs merged at once. For more, store ' 'merged intermediates in temporary files.' ) @click.option( "--tmpdir", type=str, default='', help='Custom temporary folder for merged intermediates.' ) @click.option( "--memory", type=str, default='2G', show_default=True, help='The amount of memory used by default.', ) @click.option( "--compress-program", type=str, default='', show_default=True, help='A binary to compress temporary merged chunks. ' 'Must decompress input when the flag -d is provided. ' 'Suggested alternatives: lz4c, gzip, lzop, snzip. ' 'NOTE: fails silently if the command syntax is wrong. ' ) @click.option( "--nproc", type=int, default=8, help='Number of threads for merging.', show_default=True, ) @click.option( '--nproc-in', type=int, default=1, show_default=True, help='Number of processes used by the auto-guessed input decompressing command.' ) @click.option( '--nproc-out', type=int, default=8, show_default=True, help='Number of processes used by the auto-guessed output compressing command.' ) @click.option( '--cmd-in', type=str, default=None, help='A command to decompress the input. ' 'If provided, fully overrides the auto-guessed command. ' 'Does not work with stdin. ' 'Must read input from stdin and print output into stdout. ' 'EXAMPLE: pbgzip -dc -n 3' ) @click.option( '--cmd-out', type=str, default=None, help='A command to compress the output. ' 'If provided, fully overrides the auto-guessed command. ' 'Does not work with stdout. ' 'Must read input from stdin and print output into stdout. ' 'EXAMPLE: pbgzip -c -n 8' ) # Using custom IO options def merge(pairs_path, output, max_nmerge, tmpdir, memory, compress_program, nproc, **kwargs): """Merge sorted .pairs/.pairsam files. Merge triu-flipped sorted pairs/pairsam files. If present, the @SQ records of the SAM header must be identical; the sorting order of these lines is taken from the first file in the list. The ID fields of the @PG records of the SAM header are modified with a numeric suffix to produce unique records. The other unique SAM and non-SAM header lines are copied into the output header. PAIRS_PATH : upper-triangular flipped sorted .pairs/.pairsam files to merge or a group/groups of .pairs/.pairsam files specified by a wildcard. For paths ending in .gz/.lz4, the files are decompressed by pbgzip/lz4c. """ merge_py(pairs_path, output, max_nmerge, tmpdir, memory, compress_program, nproc, **kwargs) def merge_py(pairs_path, output, max_nmerge, tmpdir, memory, compress_program, nproc, **kwargs): paths = sum([glob.glob(mask) for mask in pairs_path], []) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) # if there is only one input, bypass merging and do not modify the header if len(paths) == 1: instream = _fileio.auto_open(paths[0], mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) for line in instream: outstream.write(line) if outstream != sys.stdout: outstream.close() return headers = [] for path in paths: f = _fileio.auto_open(path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) h, _ = _headerops.get_header(f) headers.append(h) f.close() merged_header = _headerops.merge_headers(headers) merged_header = _headerops.append_new_pg( merged_header, ID=UTIL_NAME, PN=UTIL_NAME) outstream.writelines((l+'\n' for l in merged_header)) outstream.flush() command = r''' /bin/bash -c 'export LC_COLLATE=C; export LANG=C; sort -k {0},{0} -k {1},{1} -k {2},{2}n -k {3},{3}n -k {4},{4} --merge --field-separator=$'\''{5}'\'' {6} {7} {8} -S {9} {10} '''.replace('\n',' ').format( _pairsam_format.COL_C1+1, _pairsam_format.COL_C2+1, _pairsam_format.COL_P1+1, _pairsam_format.COL_P2+1, _pairsam_format.COL_PTYPE+1, _pairsam_format.PAIRSAM_SEP_ESCAPE, ' --parallel={} '.format(nproc) if nproc > 1 else ' ', ' --batch-size={} '.format(max_nmerge) if max_nmerge else ' ', ' --temporary-directory={} '.format(tmpdir) if tmpdir else ' ', memory, (' --compress-program={} '.format(compress_program) if compress_program else ' '), ) for path in paths: if kwargs.get('cmd_in', None): command += r''' <(cat {} | {} | sed -n -e '\''/^[^#]/,$p'\'')'''.format(path, kwargs['cmd_in']) elif path.endswith('.gz'): command += r''' <(pbgzip -dc -n {} {} | sed -n -e '\''/^[^#]/,$p'\'')'''.format(kwargs['nproc_in'], path) elif path.endswith('.lz4'): command += r''' <(lz4c -dc {} | sed -n -e '\''/^[^#]/,$p'\'')'''.format(path) else: command += r''' <(sed -n -e '\''/^[^#]/,$p'\'' {})'''.format(path) command += "'" subprocess.check_call(command, shell=True, stdout=outstream) if outstream != sys.stdout: outstream.close() if __name__ == '__main__': merge() pairtools-0.3.0/pairtools/pairtools_parse.py000066400000000000000000000737061345765554600213630ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from collections import OrderedDict import subprocess import fileinput import itertools import click import pipes import sys import os import io from . import _fileio, _pairsam_format, _headerops, cli, common_io_options from .pairtools_stats import PairCounter UTIL_NAME = 'pairtools_parse' EXTRA_COLUMNS = [ 'mapq', 'pos5', 'pos3', 'cigar', 'read_len', 'matched_bp', 'algn_ref_span', 'algn_read_span', 'dist_to_5', 'dist_to_3', 'seq' ] @cli.command() @click.argument( 'sam_path', type=str, required=False) @click.option( "-c", "--chroms-path", type=str, required=True, help='Chromosome order used to flip interchromosomal mates: ' 'path to a chromosomes file (e.g. UCSC chrom.sizes or similar) whose ' 'first column lists scaffold names. Any scaffolds not listed will be ' 'ordered lexicographically following the names provided.') @click.option( "-o", "--output", type=str, default="", help='output file. ' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4-compressed.' 'By default, the output is printed into stdout. ') @click.option( "--assembly", type=str, help='Name of genome assembly (e.g. hg19, mm10) to store in the pairs header.') @click.option( "--min-mapq", type=int, default=1, show_default=True, help='The minimal MAPQ score to consider a read as uniquely mapped') @click.option( "--max-molecule-size", type=int, default=2000, show_default=True, help='The maximal size of a Hi-C molecule; used to rescue single ligations' 'from molecules with three alignments.') @click.option( "--drop-readid", is_flag=True, help='If specified, do not add read ids to the output') @click.option( "--drop-seq", is_flag=True, help='If specified, remove sequences and PHREDs from the sam fields') @click.option( "--drop-sam", is_flag=True, help='If specified, do not add sams to the output') @click.option( "--add-columns", type=click.STRING, default='', help='Report extra columns describing alignments ' 'Possible values (can take multiple values as a comma-separated ' 'list): a SAM tag (any pair of uppercase letters) or {}.'.format( ', '.join(EXTRA_COLUMNS))) @click.option( "--output-parsed-alignments", type=str, default="", help='output file for all parsed alignments, including walks.' ' Useful for debugging and rnalysis of walks.' ' If file exists, it will be open in the append mode.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4-compressed.' ' By default, not used.' ) @click.option( "--output-stats", type=str, default="", help='output file for various statistics of pairs file. ' ' By default, statistics is not generated.') @click.option( '--report-alignment-end', type=click.Choice(['5', '3']), default='5', help='specifies whether the 5\' or 3\' end of the alignment is reported as' ' the position of the Hi-C read.') @click.option( '--max-inter-align-gap', type=int, default=20, show_default=True, help='read segments that are not covered by any alignment and' ' longer than the specified value are treated as "null" alignments.' ' These null alignments convert otherwise linear alignments into walks,' ' and affect how they get reported as a Hi-C pair (see --walks-policy).' ) @click.option( "--walks-policy", type=click.Choice(['mask', 'all', '5any', '5unique', '3any', '3unique']), default='mask', help='the policy for reporting unrescuable walks (reads containing more' ' than one alignment on one or both sides, that can not be explained by a' ' single ligation between two mappable DNA fragments).' ' "mask" - mask walks (chrom="!", pos=0, strand="-"); ' ' "all" - report all pairs of consecutive alignments [NOT IMPLEMENTED]; ' ' "5any" - report the 5\'-most alignment on each side;' ' "5unique" - report the 5\'-most unique alignment on each side, if present;' ' "3any" - report the 3\'-most alignment on each side;' ' "3unique" - report the 3\'-most unique alignment on each side, if present.', show_default=True ) @click.option( "--no-flip", is_flag=True, help='If specified, do not flip pairs in genomic order and instead preserve ' 'the order in which they were sequenced.') @common_io_options def parse(sam_path, chroms_path, output, assembly, min_mapq, max_molecule_size, drop_readid, drop_seq, drop_sam, add_columns, output_parsed_alignments, output_stats, **kwargs): '''Find ligation junctions in .sam, make .pairs. SAM_PATH : an input .sam/.bam file with paired-end sequence alignments of Hi-C molecules. If the path ends with .bam, the input is decompressed from bam with samtools. By default, the input is read from stdin. ''' parse_py(sam_path, chroms_path, output, assembly, min_mapq, max_molecule_size, drop_readid, drop_seq, drop_sam, add_columns, output_parsed_alignments, output_stats, **kwargs) def parse_py(sam_path, chroms_path, output, assembly, min_mapq, max_molecule_size, drop_readid, drop_seq, drop_sam, add_columns, output_parsed_alignments, output_stats, **kwargs): instream = (_fileio.auto_open(sam_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if sam_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) out_alignments_stream = (_fileio.auto_open(output_parsed_alignments, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output_parsed_alignments else None) out_stats_stream = (_fileio.auto_open(output_stats, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output_stats else None) if out_alignments_stream: out_alignments_stream.write('readID\tside\tchrom\tpos\tstrand\tmapq\tcigar\tdist_5_lo\tdist_5_hi\tmatched_bp\n') # generate empty PairCounter if stats output is requested: out_stat = PairCounter() if output_stats else None samheader, body_stream = _headerops.get_header(instream, comment_char='@') sam_chromsizes = _headerops.get_chromsizes_from_sam_header(samheader) chromosomes = _headerops.get_chrom_order( chroms_path, list(sam_chromsizes.keys())) add_columns = [col for col in add_columns.split(',') if col] for col in add_columns: if not( (col in EXTRA_COLUMNS) or (len(col) == 2 and col.isupper())): raise Exception('{} is not a valid extra column'.format(col)) columns = (_pairsam_format.COLUMNS + ([c+side for c in add_columns for side in ['1', '2']]) ) if drop_sam: columns.pop(columns.index('sam1')) columns.pop(columns.index('sam2')) header = _headerops.make_standard_pairsheader( assembly = assembly, chromsizes = [(chrom, sam_chromsizes[chrom]) for chrom in chromosomes], columns = columns, shape = 'whole matrix' if kwargs['no_flip'] else 'upper triangle' ) header = _headerops.insert_samheader(header, samheader) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) outstream.writelines((l+'\n' for l in header)) streaming_classify(body_stream, outstream, chromosomes, min_mapq, max_molecule_size, drop_readid, drop_seq, drop_sam, add_columns, out_alignments_stream, out_stat, **kwargs) # save statistics to a file if it was requested: if out_stat: out_stat.save(out_stats_stream) if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() # close optional output streams if needed: if out_alignments_stream: out_alignments_stream.close() if out_stats_stream: out_stats_stream.close() def parse_cigar(cigar): matched_bp = 0 algn_ref_span = 0 algn_read_span = 0 read_len = 0 clip5_ref = 0 clip3_ref = 0 if cigar != '*': cur_num = 0 for char in cigar: charval = ord(char) if charval >= 48 and charval <= 57: cur_num = cur_num * 10 + (charval - 48) else: if char == 'M': matched_bp += cur_num algn_ref_span += cur_num algn_read_span += cur_num read_len += cur_num elif char == 'I': algn_read_span += cur_num read_len += cur_num elif char == 'D': algn_ref_span += cur_num elif char == 'S' or char == 'H': read_len += cur_num if matched_bp == 0: clip5_ref = cur_num else: clip3_ref = cur_num cur_num = 0 return { 'clip5_ref': clip5_ref, 'clip3_ref': clip3_ref, 'cigar': cigar, 'algn_ref_span': algn_ref_span, 'algn_read_span': algn_read_span, 'read_len': read_len, 'matched_bp': matched_bp, } def empty_alignment(): return { 'chrom': _pairsam_format.UNMAPPED_CHROM, 'pos5': _pairsam_format.UNMAPPED_POS, 'pos3': _pairsam_format.UNMAPPED_POS, 'pos': _pairsam_format.UNMAPPED_POS, 'strand': _pairsam_format.UNMAPPED_STRAND, 'dist_to_5': 0, 'dist_to_3': 0, 'mapq': 0, 'is_unique': False, 'is_mapped': False, 'is_linear': True, 'cigar' : '*', 'algn_ref_span': 0, 'algn_read_span': 0, 'matched_bp': 0, 'clip3_ref': 0, 'clip5_ref': 0, 'read_len': 0, 'type':'N' } def parse_algn( samcols, min_mapq, report_3_alignment_end=False, sam_tags=None, store_seq=False): is_mapped = (int(samcols[1]) & 0x04) == 0 mapq = int(samcols[4]) is_unique = (mapq >= min_mapq) is_linear = not any([col.startswith('SA:Z:') for col in samcols[11:]]) cigar = parse_cigar(samcols[5]) if is_mapped: if ((int(samcols[1]) & 0x10) == 0): strand = '+' dist_to_5 = cigar['clip5_ref'] dist_to_3 = cigar['clip3_ref'] else: strand = '-' dist_to_5 = cigar['clip3_ref'] dist_to_3 = cigar['clip5_ref'] if is_unique: chrom = samcols[2] if strand == '+': pos5 = int(samcols[3]) pos3 = int(samcols[3]) + cigar['algn_ref_span'] - 1 else: pos5 = int(samcols[3]) + cigar['algn_ref_span'] - 1 pos3 = int(samcols[3]) else: chrom = _pairsam_format.UNMAPPED_CHROM strand = _pairsam_format.UNMAPPED_STRAND pos5 = _pairsam_format.UNMAPPED_POS pos3 = _pairsam_format.UNMAPPED_POS else: chrom = _pairsam_format.UNMAPPED_CHROM strand = _pairsam_format.UNMAPPED_STRAND pos5 = _pairsam_format.UNMAPPED_POS pos3 = _pairsam_format.UNMAPPED_POS dist_to_5 = 0 dist_to_3 = 0 algn = { 'chrom': chrom, 'pos5': pos5, 'pos3': pos3, 'strand': strand, 'mapq': mapq, 'is_mapped': is_mapped, 'is_unique': is_unique, 'is_linear': is_linear, 'dist_to_5': dist_to_5, 'dist_to_3': dist_to_3, 'type': ('N' if not is_mapped else ('M' if not is_unique else 'U')) } algn.update(cigar) algn['pos'] = algn['pos3'] if report_3_alignment_end else algn['pos5'] if sam_tags: for tag in sam_tags: algn[tag] = '' for col in samcols[11:]: for tag in sam_tags: if col.startswith(tag+':'): algn[tag] = col[5:] continue if store_seq: algn['seq'] = samcols[9] return algn def rescue_walk(algns1, algns2, max_molecule_size): """ Rescue a single ligation that appears as a walk. Checks if a molecule with three alignments could be formed via a single ligation between two fragments, where one fragment was so long that it got sequenced on both sides. Uses three criteria: a) the 3'-end alignment on one side maps to the same chromosome as the alignment fully covering the other side (i.e. the linear alignment) b) the two alignments point towards each other on the chromosome c) the distance between the outer ends of the two alignments is below the specified threshold. Alternatively, a single ligation get rescued when the 3' sub-alignment maps to multiple locations or no locations at all. In the case of a successful rescue, tags the 3' sub-alignment with type='X' and the linear alignment on the other side with type='R'. Returns ------- linear_side : int If the case of a successful rescue, returns the index of the side with a linear alignment. """ # If both sides have one alignment or none, no need to rescue! n_algns1 = len(algns1) n_algns2 = len(algns2) if (n_algns1 <= 1) and (n_algns2 <= 1): return None # Can rescue only pairs with one chimeric alignment with two parts. if not ( ((n_algns1 == 1) and (n_algns2 == 2)) or ((n_algns1 == 2) and (n_algns2 == 1)) ): return None first_read_is_chimeric = n_algns1 > 1 chim5_algn = algns1[0] if first_read_is_chimeric else algns2[0] chim3_algn = algns1[1] if first_read_is_chimeric else algns2[1] linear_algn = algns2[0] if first_read_is_chimeric else algns1[0] # the linear alignment must be uniquely mapped if not(linear_algn['is_mapped'] and linear_algn['is_unique']): return None can_rescue = True # we automatically rescue chimeric alignments with null and non-unique # alignments at the 3' side if (chim3_algn['is_mapped'] and chim5_algn['is_unique']): # 1) in rescued walks, the 3' alignment of the chimeric alignment must be on # the same chromosome as the linear alignment on the opposite side of the # molecule can_rescue &= (chim3_algn['chrom'] == linear_algn['chrom']) # 2) in rescued walks, the 3' supplemental alignment of the chimeric # alignment and the linear alignment on the opposite side must point # towards each other can_rescue &= (chim3_algn['strand'] != linear_algn['strand']) if linear_algn['strand'] == '+': can_rescue &= (linear_algn['pos5'] < chim3_algn['pos5']) else: can_rescue &= (linear_algn['pos5'] > chim3_algn['pos5']) # 3) in single ligations appearing as walks, we can infer the size of # the molecule and this size must be smaller than the maximal size of # Hi-C molecules after the size selection step of the Hi-C protocol if linear_algn['strand'] == '+': molecule_size = ( chim3_algn['pos5'] - linear_algn['pos5'] + chim3_algn['dist_to_5'] + linear_algn['dist_to_5'] ) else: molecule_size = ( linear_algn['pos5'] - chim3_algn['pos5'] + chim3_algn['dist_to_5'] + linear_algn['dist_to_5'] ) can_rescue &= (molecule_size <= max_molecule_size) if can_rescue: if first_read_is_chimeric: # changing the type of the 3' alignment on side 1, does not show up # in the output algns1[1]['type'] = 'X' algns2[0]['type'] = 'R' return 2 else: algns1[0]['type'] = 'R' # changing the type of the 3' alignment on side 2, does not show up # in the output algns2[1]['type'] = 'X' return 1 else: return None def _convert_gaps_into_alignments(sorted_algns, max_inter_align_gap): if (len(sorted_algns) == 1) and (not sorted_algns[0]['is_mapped']): return last_5_pos = 0 for i in range(len(sorted_algns)): algn = sorted_algns[i] if (algn['dist_to_5'] - last_5_pos > max_inter_align_gap): new_algn = empty_alignment() new_algn['dist_to_5'] = last_5_pos new_algn['algn_read_span'] = algn['dist_to_5'] - last_5_pos new_algn['read_len'] = algn['read_len'] new_algn['dist_to_3'] = new_algn['read_len'] - algn['dist_to_5'] last_5_pos = algn['dist_to_5'] + algn['algn_read_span'] sorted_algns.insert(i, new_algn) i += 2 else: last_5_pos = max(last_5_pos, algn['dist_to_5'] + algn['algn_read_span']) i += 1 def _mask_alignment(algn): """ Reset the coordinates of an alignment. """ algn['chrom'] = _pairsam_format.UNMAPPED_CHROM algn['pos5'] = _pairsam_format.UNMAPPED_POS algn['pos3'] = _pairsam_format.UNMAPPED_POS algn['pos'] = _pairsam_format.UNMAPPED_POS algn['strand'] = _pairsam_format.UNMAPPED_STRAND return algn def parse_sams_into_pair(sams1, sams2, min_mapq, max_molecule_size, max_inter_align_gap, walks_policy, report_3_alignment_end, sam_tags, store_seq): """ Parse sam entries corresponding to a Hi-C molecule into alignments for a Hi-C pair. Returns ------- algn1, algn2: dict Two alignments selected for reporting as a Hi-C pair. algns1, algns2 All alignments, sorted according to their order in on a read. """ # Check if there is at least one SAM entry per side: if (len(sams1) == 0) or (len(sams2) == 0): algns1 = [empty_alignment()] algns2 = [empty_alignment()] algns1[0]['type'] = 'X' algns2[0]['type'] = 'X' return algns1[0], algns2[0], algns1, algns2 # Generate a sorted, gap-filled list of all alignments algns1 = [parse_algn(sam.rstrip().split('\t'), min_mapq, report_3_alignment_end, sam_tags, store_seq) for sam in sams1] algns2 = [parse_algn(sam.rstrip().split('\t'), min_mapq, report_3_alignment_end, sam_tags, store_seq) for sam in sams2] algns1 = sorted(algns1, key=lambda algn: algn['dist_to_5']) algns2 = sorted(algns2, key=lambda algn: algn['dist_to_5']) if max_inter_align_gap is not None: _convert_gaps_into_alignments(algns1, max_inter_align_gap) _convert_gaps_into_alignments(algns2, max_inter_align_gap) # Define the type of alignment on each side. # The most important split is between chimeric alignments and linear # alignments. is_chimeric_1 = len(algns1) > 1 is_chimeric_2 = len(algns2) > 1 hic_algn1 = algns1[0] hic_algn2 = algns2[0] # Parse chimeras rescued_linear_side = None if is_chimeric_1 or is_chimeric_2: # Pick two alignments to report as a Hi-C pair. rescued_linear_side = rescue_walk(algns1, algns2, max_molecule_size) # if the walk is unrescueable: if rescued_linear_side is None: if walks_policy == 'mask': if is_chimeric_1 or is_chimeric_2: hic_algn1 = _mask_alignment(dict(hic_algn1)) hic_algn2 = _mask_alignment(dict(hic_algn2)) hic_algn1['type'] = 'W' hic_algn2['type'] = 'W' elif walks_policy == '5any': hic_algn1 = algns1[0] hic_algn2 = algns2[0] elif walks_policy == '5unique': hic_algn1 = algns1[0] for algn in algns1: if algn['is_mapped'] and algn['is_unique']: hic_algn1 = algn break hic_algn2 = algns2[0] for algn in algns2: if algn['is_mapped'] and algn['is_unique']: hic_algn2 = algn break elif walks_policy == '3any': hic_algn1 = algns1[-1] hic_algn2 = algns2[-1] elif walks_policy == '3unique': hic_algn1 = algns1[-1] for algn in algns1[::-1]: if algn['is_mapped'] and algn['is_unique']: hic_algn1 = algn break hic_algn2 = algns2[-1] for algn in algns2[::-1]: if algn['is_mapped'] and algn['is_unique']: hic_algn2 = algn break elif walks_policy == 'all': # TO BE IMPLEMENTED pass # lower-case reported walks on the chimeric side if walks_policy != 'mask': if is_chimeric_1: hic_algn1 = dict(hic_algn1) hic_algn1['type'] = hic_algn1['type'].lower() if is_chimeric_2: hic_algn2 = dict(hic_algn2) hic_algn2['type'] = hic_algn2['type'].lower() return hic_algn1, hic_algn2, algns1, algns2 def check_pair_order(algn1, algn2, chrom_enum): ''' Check if a pair of alignments has the upper-triangular order or has to be flipped. ''' # First, the pair is flipped according to the type of mapping on its sides. # Later, we will check it is mapped on both sides and, if so, flip the sides # according to these coordinates. has_correct_order = ( (algn1['is_mapped'], algn1['is_unique']) <= (algn2['is_mapped'], algn2['is_unique']) ) # If a pair has coordinates on both sides, it must be flipped according to # its genomic coordinates. if ((algn1['chrom'] != _pairsam_format.UNMAPPED_CHROM) and (algn2['chrom'] != _pairsam_format.UNMAPPED_CHROM)): has_correct_order = ( (chrom_enum[algn1['chrom']], algn1['pos']) <= (chrom_enum[algn2['chrom']], algn2['pos'])) return has_correct_order def push_sam(line, drop_seq, sams1, sams2): """ """ sam = line.rstrip() if drop_seq: split_sam = sam.split('\t') split_sam[9] = '*' split_sam[10] = '*' sam = '\t'.join(split_sam) flag = split_sam[1] flag = int(flag) else: _, flag, _ = sam.split('\t', 2) flag = int(flag) if ((flag & 0x40) != 0): sams1.append(sam) else: sams2.append(sam) return def write_all_algnments(read_id, all_algns1, all_algns2, out_file): for side_idx, all_algns in enumerate((all_algns1, all_algns2)): out_file.write(read_id) out_file.write('\t') out_file.write(str(side_idx+1)) out_file.write('\t') for algn in sorted(all_algns, key=lambda x: x['dist_to_5']): out_file.write(algn['chrom']) out_file.write('\t') out_file.write(str(algn['pos5'])) out_file.write('\t') out_file.write(algn['strand']) out_file.write('\t') out_file.write(str(algn['mapq'])) out_file.write('\t') out_file.write(str(algn['cigar'])) out_file.write('\t') out_file.write(str(algn['dist_to_5'])) out_file.write('\t') out_file.write(str(algn['dist_to_5']+algn['algn_read_span'])) out_file.write('\t') out_file.write(str(algn['matched_bp'])) out_file.write('\t') out_file.write('\n') def write_pairsam( algn1, algn2, read_id, sams1, sams2, out_file, drop_readid, drop_sam, add_columns): """ SAM is already tab-separated and any printable character between ! and ~ may appear in the PHRED field! (http://www.ascii-code.com/) Thus, use the vertical tab character to separate fields! """ cols = [ '.' if drop_readid else read_id, algn1['chrom'], str(algn1['pos']), algn2['chrom'], str(algn2['pos']), algn1['strand'], algn2['strand'], algn1['type'] + algn2['type'] ] if not drop_sam: for sams in [sams1, sams2]: cols.append( _pairsam_format.INTER_SAM_SEP.join([ (sam.replace('\t', _pairsam_format.SAM_SEP) + _pairsam_format.SAM_SEP + 'Yt:Z:' + algn1['type'] + algn2['type']) for sam in sams ]) ) for col in add_columns: # use get b/c empty alignments would not have sam tags (NM, AS, etc) cols.append(str(algn1.get(col, ''))) cols.append(str(algn2.get(col, ''))) out_file.write(_pairsam_format.PAIRSAM_SEP.join(cols) + '\n') def streaming_classify(instream, outstream, chromosomes, min_mapq, max_molecule_size, drop_readid, drop_seq, drop_sam, add_columns, out_alignments_stream, out_stat, **kwargs): """ """ chrom_enum = dict(zip([_pairsam_format.UNMAPPED_CHROM] + list(chromosomes), range(len(chromosomes)+1))) sam_tags = [col for col in add_columns if len(col)==2 and col.isupper()] prev_read_id = '' sams1 = [] sams2 = [] line = '' store_seq = ('seq' in add_columns) instream = iter(instream) while line is not None: line = next(instream, None) read_id = line.split('\t', 1)[0] if line else None if not(line) or ((read_id != prev_read_id) and prev_read_id): algn1, algn2, all_algns1, all_algns2 = parse_sams_into_pair( sams1, sams2, min_mapq, max_molecule_size, kwargs['max_inter_align_gap'], kwargs['walks_policy'], kwargs['report_alignment_end']=='3', sam_tags, store_seq ) flip_pair = (not kwargs['no_flip']) and ( not check_pair_order(algn1, algn2, chrom_enum)) if flip_pair: algn1, algn2 = algn2, algn1 sams1, sams2 = sams2, sams1 write_pairsam( algn1, algn2, prev_read_id, sams1, sams2, outstream, drop_readid, drop_sam, add_columns) # add a pair to PairCounter if stats output is requested: if out_stat: out_stat.add_pair(algn1['chrom'], int(algn1['pos']), algn1['strand'], algn2['chrom'], int(algn2['pos']), algn2['strand'], algn1['type'] + algn2['type']) if out_alignments_stream: write_all_algnments(prev_read_id, all_algns1, all_algns2, out_alignments_stream) sams1.clear() sams2.clear() if line is not None: push_sam(line, drop_seq, sams1, sams2) prev_read_id = read_id if __name__ == '__main__': parse() def parse_alternative_algns(samcols): alt_algns = [] for col in samcols[11:]: if not col.startswith('XA:Z:'): continue for SA in col[5:].split(';'): if not SA: continue SAcols = SA.split(',') chrom = SAcols[0] strand = '-' if SAcols[1]<0 else '+' cigar = parse_cigar(SAcols[2]) NM = SAcols[3] pos = _pairsam_format.UNMAPPED_POS if strand == '+': pos = int(SAcols[1]) else: pos = int(SAcols[1]) + cigar['algn_ref_span'] alt_algns.append({ 'chrom': chrom, 'pos': pos, 'strand': strand, 'mapq': mapq, 'is_mapped': True, 'is_unique': False, 'is_linear': None, 'cigar': cigar, 'NM': NM, 'dist_to_5': cigar['clip5_ref'] if strand == '+' else cigar['clip3_ref'], }) return supp_algns #def parse_supp(samcols, min_mapq): # supp_algns = [] # for col in samcols[11:]: # if not col.startswith('SA:Z:'): # continue # # for SA in col[5:].split(';'): # if not SA: # continue # SAcols = SA.split(',') # mapq = int(SAcols[4]) # is_unique = (mapq >= min_mapq) # # chrom = SAcols[0] if is_unique else _pairsam_format.UNMAPPED_CHROM # strand = SAcols[2] if is_unique else _pairsam_format.UNMAPPED_STRAND # # cigar = parse_cigar(SAcols[3]) # # pos = _pairsam_format.UNMAPPED_POS # if is_unique: # if strand == '+': # pos = int(SAcols[1]) # else: # pos = int(SAcols[1]) + cigar['algn_ref_span'] # # supp_algns.append({ # 'chrom': chrom, # 'pos': pos, # 'strand': strand, # 'mapq': mapq, # 'is_mapped': True, # 'is_unique': is_unique, # 'is_linear': None, # 'cigar': cigar, # 'dist_to_5': cigar['clip5_ref'] if strand == '+' else cigar['clip3_ref'], # }) # # return supp_algns pairtools-0.3.0/pairtools/pairtools_phase.py000066400000000000000000000147131345765554600213420ustar00rootroot00000000000000import sys import click import re, fnmatch from . import _fileio, _pairsam_format, cli, _headerops, common_io_options UTIL_NAME = 'pairtools_phase' @cli.command() @click.argument( 'pairs_path', type=str, required=False) @click.option( '-o', "--output", type=str, default="", help='output file.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' By default, the output is printed into stdout.') @click.option( "--phase-suffixes", nargs=2, #type=click.Tuple([str, str]), help='phase suffixes.' ) @click.option( "--clean-output", is_flag=True, help='drop all columns besides the standard ones and phase1/2' ) @common_io_options def phase( pairsam_path, output, phase_suffixes, clean_output, **kwargs ): '''Phase pairs mapped to a diploid genome. PAIRS_PATH : input .pairs/.pairsam file. If the path ends with .gz or .lz4, the input is decompressed by pbgzip/lz4c. By default, the input is read from stdin. ''' phase_py( pairs_path, output, phase_suffixes, clean_output, **kwargs ) def phase_py( pairs_path, output, phase_suffixes, clean_output, **kwargs ): instream = (_fileio.auto_open(pairsam_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if pairsam_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) header, body_stream = _headerops.get_header(instream) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) old_column_names = _headerops.extract_column_names(header) if clean_output: new_column_names = [col for col in old_column_names if col in _pairsam_format.COLUMNS] new_column_idxs = [i for i,col in enumerate(old_column_names) if col in _pairsam_format.COLUMNS] + [ len(old_column_names), len(old_column_names)+1] else: new_column_names = list(old_column_names) new_column_names.append('phase1') new_column_names.append('phase2') header = _headerops._update_header_entry( header, 'columns', ' '.join(new_column_names)) if ( ('XB1' not in old_column_names) or ('XB2' not in old_column_names) or ('AS1' not in old_column_names) or ('AS2' not in old_column_names) or ('XS1' not in old_column_names) or ('XS2' not in old_column_names) ): raise ValueError( 'The input pairs file must be parsed with the flag --add-columns XB,AS,XS --min-mapq 0') COL_XB1 = old_column_names.index('XB1') COL_XB2 = old_column_names.index('XB2') COL_AS1 = old_column_names.index('AS1') COL_AS2 = old_column_names.index('AS2') COL_XS1 = old_column_names.index('XS1') COL_XS2 = old_column_names.index('XS2') outstream.writelines((l+'\n' for l in header)) def get_chrom_phase(chrom, phase_suffixes): if chrom.endswith(phase_suffixes[0]): return '0', chrom[:-len(phase_suffixes[0])] elif chrom.endswith(phase_suffixes[1]): return '1', chrom[:-len(phase_suffixes[1])] else: return '!', chrom def phase_side(chrom, XB, AS, XS, phase_suffixes): phase, chrom_base = get_chrom_phase(chrom, phase_suffixes) XBs = [i for i in XB.split(';') if len(i)>0] if AS > XS: return phase, chrom_base elif len(XBs) >= 1: if len(XBs) >= 2: alt2_chrom, alt2_pos, alt2_CIGAR, alt2_NM, alt2_AS = XBs[1].split(',') if alt2_AS == XS == AS: return '!', '!' alt_chrom, alt_pos, alt_CIGAR, alt_NM, alt_AS = XBs[0].split(',') alt_phase, alt_chrom_base = get_chrom_phase(alt_chrom, phase_suffixes) alt_is_homologue = ( (chrom_base == alt_chrom_base) and ( ((phase=='0') and (alt_phase=='1')) or ((phase=='1') and (alt_phase=='0')) ) ) if alt_is_homologue: return '.', chrom_base return '!', '!' for line in body_stream: cols = line.rstrip().split(_pairsam_format.PAIRSAM_SEP) cols.append('!') cols.append('!') pair_type = cols[_pairsam_format.COL_PTYPE] if cols[_pairsam_format.COL_C1] != _pairsam_format.UNMAPPED_CHROM: phase1, chrom_base1 = phase_side( cols[_pairsam_format.COL_C1], cols[COL_XB1], int(cols[COL_AS1]), int(cols[COL_XS1]), phase_suffixes ) cols[-2] = phase1 cols[_pairsam_format.COL_C1] = chrom_base1 if chrom_base1 == '!': cols[_pairsam_format.COL_C1] = _pairsam_format.UNMAPPED_CHROM cols[_pairsam_format.COL_P1] = str(_pairsam_format.UNMAPPED_POS) cols[_pairsam_format.COL_S1] = _pairsam_format.UNMAPPED_STRAND pair_type = 'M' + pair_type[1] if cols[_pairsam_format.COL_C2] != _pairsam_format.UNMAPPED_CHROM: phase2, chrom_base2 = phase_side( cols[_pairsam_format.COL_C2], cols[COL_XB2], int(cols[COL_AS2]), int(cols[COL_XS2]), phase_suffixes ) cols[-1] = phase2 cols[_pairsam_format.COL_C2] = chrom_base2 if chrom_base2 == '!': cols[_pairsam_format.COL_C2] = _pairsam_format.UNMAPPED_CHROM cols[_pairsam_format.COL_P2] = str(_pairsam_format.UNMAPPED_POS) cols[_pairsam_format.COL_S2] = _pairsam_format.UNMAPPED_STRAND pair_type = pair_type[0] + 'M' cols[_pairsam_format.COL_PTYPE] = pair_type if clean_output: cols = [cols[i] for i in new_column_idxs] outstream.write(_pairsam_format.PAIRSAM_SEP.join(cols)) outstream.write('\n') if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() if __name__ == '__main__': phase() pairtools-0.3.0/pairtools/pairtools_restrict.py000066400000000000000000000066371345765554600221070ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import io import sys import click import subprocess import numpy as np from . import _fileio, _pairsam_format, cli, _headerops, common_io_options UTIL_NAME = 'pairtools_restrict' @cli.command() @click.argument( 'pairs_path', type=str, required=False) @click.option( '-f', '--frags', type=str, required=True, help='a tab-separated BED file with the positions of restriction fragments ' '(chrom, start, end). Can be generated using cooler digest.') @click.option( '-o', "--output", type=str, default="", help='output .pairs/.pairsam file.' ' If the path ends with .gz/.lz4, the output is compressed by pbgzip/lz4c.' ' By default, the output is printed into stdout.') @common_io_options def restrict(pairs_path, frags, output, **kwargs): '''Assign restriction fragments to pairs. Identify the restriction fragments that got ligated into a Hi-C molecule. PAIRS_PATH : input .pairs/.pairsam file. If the path ends with .gz/.lz4, the input is decompressed by pbgzip/lz4c. By default, the input is read from stdin. ''' restrict_py(pairs_path, frags, output, **kwargs) def restrict_py(pairs_path, frags, output, **kwargs): instream = (_fileio.auto_open(pairs_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if pairs_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) header, body_stream = _headerops.get_header(instream) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) outstream.writelines((l+'\n' for l in header)) rfrags = np.genfromtxt( frags, delimiter='\t', comments='#', dtype=None, names=['chrom', 'start', 'end']) rfrags.sort(order=['chrom', 'start','end']) rfrags.sort(order=['chrom', 'start', 'end']) chrom_borders = np.r_[0, 1+np.where(rfrags['chrom'][:-1] != rfrags['chrom'][1:])[0], rfrags.shape[0]] rfrags = {rfrags['chrom'][i]:rfrags['end'][i:j] +1 for i, j in zip(chrom_borders[:-1], chrom_borders[1:])} for line in body_stream: cols = line.rstrip().split(_pairsam_format.PAIRSAM_SEP) chrom1, pos1 = cols[_pairsam_format.COL_C1], int(cols[_pairsam_format.COL_P1]) rfrag_idx1, rfrag_start1, rfrag_end1 = find_rfrag(rfrags, chrom1, pos1) chrom2, pos2 = cols[_pairsam_format.COL_C2], int(cols[_pairsam_format.COL_P2]) rfrag_idx2, rfrag_start2, rfrag_end2 = find_rfrag(rfrags, chrom2, pos2) cols += [str(rfrag_idx1), str(rfrag_start1), str(rfrag_end1)] cols += [str(rfrag_idx2), str(rfrag_start2), str(rfrag_end2)] outstream.write(_pairsam_format.PAIRSAM_SEP.join(cols)) outstream.write('\n') if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() def find_rfrag(rfrags, chrom, pos): rsites_chrom = rfrags[chrom.encode('ascii')] idx = min(max(0,rsites_chrom.searchsorted(pos, 'right')-1), len(rsites_chrom)-2) return idx, rsites_chrom[idx], rsites_chrom[idx+1] if __name__ == '__main__': restrict() pairtools-0.3.0/pairtools/pairtools_select.py000066400000000000000000000164221345765554600215200ustar00rootroot00000000000000import sys import click import re, fnmatch from . import _fileio, _pairsam_format, cli, _headerops, common_io_options UTIL_NAME = 'pairtools_select' @cli.command() @click.argument( 'condition', type=str ) @click.argument( 'pairs_path', type=str, required=False) @click.option( '-o', "--output", type=str, default="", help='output file.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' By default, the output is printed into stdout.') @click.option( "--output-rest", type=str, default="", help='output file for pairs of other types. ' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' By default, such pairs are dropped.') @click.option( "--send-comments-to", type=click.Choice(['selected', 'rest', 'both', 'none']), default="both", help="Which of the outputs should receive header and comment lines", show_default=True) @click.option( "--chrom-subset", type=str, default=None, help="A path to a chromosomes file (tab-separated, 1st column contains " "chromosome names) containing a chromosome subset of interest. " "If provided, additionally filter pairs with both sides originating from " "the provided subset of chromosomes. This operation modifies the #chromosomes: " "and #chromsize: header fields accordingly." ) @click.option( "--startup-code", type=str, default=None, help="An auxiliary code to execute before filtering. " "Use to define functions that can be evaluated in the CONDITION statement" ) @click.option( "-t", "--type-cast", type=(str,str), default=(), multiple=True, help="Cast a given column to a given type. By default, only pos and mapq " "are cast to int, other columns are kept as str. Provide as " "-t , e.g. -t read_len1 int. Multiple entries are allowed." ) @common_io_options def select( condition, pairs_path, output, output_rest, send_comments_to, chrom_subset, startup_code, type_cast, **kwargs ): '''Select pairs according to some condition. CONDITION : A Python expression; if it returns True, select the read pair. Any column declared in the #columns line of the pairs header can be accessed by its name. If the header lacks the #columns line, the columns are assumed to follow the .pairs/.pairsam standard (readID, chrom1, chrom2, pos1, pos2, strand1, strand2, pair_type). Finally, CONDITION has access to COLS list which contains the string values of columns. In Bash, quote CONDITION with single quotes, and use double quotes for string variables inside CONDITION. PAIRS_PATH : input .pairs/.pairsam file. If the path ends with .gz or .lz4, the input is decompressed by pbgzip/lz4c. By default, the input is read from stdin. The following functions can be used in CONDITION besides the standard Python functions: - csv_match(x, csv) - True if variable x is contained in a list of comma-separated values, e.g. csv_match(chrom1, 'chr1,chr2') - wildcard_match(x, wildcard) - True if variable x matches a wildcard, e.g. wildcard_match(pair_type, 'C*') - regex_match(x, regex) - True if variable x matches a Python-flavor regex, e.g. regex_match(chrom1, 'chr\d') \b Examples: pairtools select '(pair_type=="UU") or (pair_type=="UR") or (pair_type=="RU")' pairtools select 'chrom1==chrom2' pairtools select 'COLS[1]==COLS[3]' pairtools select '(chrom1==chrom2) and (abs(pos1 - pos2) < 1e6)' pairtools select '(chrom1=="!") and (chrom2!="!")' pairtools select 'regex_match(chrom1, "chr\d+") and regex_match(chrom2, "chr\d+")' pairtools select 'True' --chr-subset mm9.reduced.chromsizes ''' select_py( condition, pairs_path, output, output_rest, send_comments_to, chrom_subset, startup_code, type_cast, **kwargs ) def select_py( condition, pairs_path, output, output_rest, send_comments_to, chrom_subset, startup_code, type_cast, **kwargs ): instream = (_fileio.auto_open(pairs_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if pairs_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) outstream_rest = (_fileio.auto_open(output_rest, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output_rest else None) wildcard_library = {} def wildcard_match(x, wildcard): if wildcard not in wildcard_library: regex = fnmatch.translate(wildcard) reobj = re.compile(regex) wildcard_library[wildcard] = reobj return wildcard_library[wildcard].fullmatch(x) csv_library = {} def csv_match(x, csv): if csv not in csv_library: csv_library[csv] = set(csv.split(',')) return x in csv_library[csv] regex_library = {} def regex_match(x, regex): if regex not in regex_library: reobj = re.compile(regex) regex_library[regex] = reobj return regex_library[regex].fullmatch(x) new_chroms = None if chrom_subset is not None: new_chroms = [l.strip().split('\t')[0] for l in open(chrom_subset, 'r')] TYPES = {'pos1':'int', 'pos2':'int', 'mapq1':'int', 'mapq2':'int'} TYPES.update(dict(type_cast)) header, body_stream = _headerops.get_header(instream) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) if new_chroms is not None: header = _headerops.subset_chroms_in_pairsheader(header, new_chroms) outstream.writelines((l+'\n' for l in header)) if outstream_rest: outstream_rest.writelines((l+'\n' for l in header)) column_names = _headerops.extract_column_names(header) if len(column_names) == 0: column_names = _pairsam_format.COLUMNS if startup_code is not None: exec(startup_code, globals()) condition = condition.strip() if new_chroms is not None: condition = ('({}) and (chrom1 in new_chroms) ' 'and (chrom2 in new_chroms)').format(condition) for i,col in enumerate(column_names): if col in TYPES: col_type = TYPES[col] condition = condition.replace(col, '{}(COLS[{}])'.format(col_type,i)) else: condition = condition.replace(col, 'COLS[{}]'.format(i)) match_func = compile(condition, '', 'eval') for line in body_stream: COLS = line.rstrip().split(_pairsam_format.PAIRSAM_SEP) if eval(match_func): outstream.write(line) elif outstream_rest: outstream_rest.write(line) if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() if outstream_rest: outstream_rest.close() if __name__ == '__main__': select() pairtools-0.3.0/pairtools/pairtools_sort.py000066400000000000000000000107621345765554600212310ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import io import sys import click import subprocess import shutil import warnings from . import _fileio, _pairsam_format, cli, _headerops, common_io_options UTIL_NAME = 'pairtools_sort' @cli.command() @click.argument( 'pairs_path', type=str, required=False) @click.option( '-o', "--output", type=str, default="", help='output pairs file.' ' If the path ends with .gz or .lz4, the output is compressed by pbgzip ' 'or lz4, correspondingly. By default, the output is printed into stdout.') @click.option( "--nproc", type=int, default=8, show_default=True, help='Number of processes to split the sorting work between.' ) @click.option( "--tmpdir", type=str, default='', help='Custom temporary folder for sorting intermediates.' ) @click.option( "--memory", type=str, default='2G', show_default=True, help='The amount of memory used by default.', ) @click.option( "--compress-program", type=str, default='auto', show_default=True, help='A binary to compress temporary sorted chunks. ' 'Must decompress input when the flag -d is provided. ' 'Suggested alternatives: gzip, lzop, lz4c, snzip. ' 'If "auto", then use lz4c if available, and gzip ' 'otherwise.' ) @common_io_options def sort(pairs_path, output, nproc, tmpdir, memory, compress_program, **kwargs): '''Sort a .pairs/.pairsam file. Sort pairs in the lexicographic order along chrom1 and chrom2, in the numeric order along pos1 and pos2 and in the lexicographic order along pair_type. PAIRS_PATH : input .pairs/.pairsam file. If the path ends with .gz or .lz4, the input is decompressed by pbgzip or lz4c, correspondingly. By default, the input is read as text from stdin. ''' sort_py(pairs_path, output, nproc, tmpdir, memory, compress_program, **kwargs) def sort_py(pairs_path, output, nproc, tmpdir, memory, compress_program, **kwargs): instream = (_fileio.auto_open(pairs_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if pairs_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) header, body_stream = _headerops.get_header(instream) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) header = _headerops.mark_header_as_sorted(header) outstream.writelines((l+'\n' for l in header)) outstream.flush() if compress_program == 'auto': if shutil.which('lz4c') is not None: compress_program = 'lz4c' else: warnings.warn( 'lz4c is not found. Using gzip for compression of sorted chunks, ' 'which results in a minor decrease in performance. Please install ' 'lz4c for faster sorting.') compress_program = 'gzip' command = r''' /bin/bash -c 'export LC_COLLATE=C; export LANG=C; sort -k {0},{0} -k {1},{1} -k {2},{2}n -k {3},{3}n -k {4},{4} --stable --field-separator=$'\''{5}'\'' {6} {7} -S {8} {9} '''.replace('\n',' ').format( _pairsam_format.COL_C1+1, _pairsam_format.COL_C2+1, _pairsam_format.COL_P1+1, _pairsam_format.COL_P2+1, _pairsam_format.COL_PTYPE+1, _pairsam_format.PAIRSAM_SEP_ESCAPE, ' --parallel={} '.format(nproc) if nproc > 0 else ' ', ' --temporary-directory={} '.format(tmpdir) if tmpdir else ' ', memory, (' --compress-program={} '.format(compress_program) if compress_program else ' '), ) command += "'" with subprocess.Popen( command, stdin=subprocess.PIPE, bufsize=-1, shell=True, stdout=outstream) as process: stdin_wrapper = io.TextIOWrapper(process.stdin, 'utf-8') for line in body_stream: stdin_wrapper.write(line) stdin_wrapper.flush() process.communicate() if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() if __name__ == '__main__': sort() pairtools-0.3.0/pairtools/pairtools_split.py000066400000000000000000000116151345765554600213730ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import sys import pipes import click from . import _fileio, _pairsam_format, _headerops, cli, common_io_options UTIL_NAME = 'pairtools_split' @cli.command() @click.argument( 'pairsam_path', type=str, required=False) @click.option( "--output-pairs", type=str, default="", help='output pairs file.' ' If the path ends with .gz or .lz4, the output is pbgzip-/lz4c-compressed.' ' If -, pairs are printed to stdout.' ' If not specified, pairs are dropped.') @click.option( "--output-sam", type=str, default="", help='output sam file.' ' If the path ends with .bam, the output is compressed into a bam file.' ' If -, sam entries are printed to stdout.' ' If not specified, sam entries are dropped.') @common_io_options def split(pairsam_path, output_pairs, output_sam, **kwargs): '''Split a .pairsam file into .pairs and .sam. Restore a .sam file from sam1 and sam2 fields of a .pairsam file. Create a .pairs file without sam1/sam2 fields. PAIRSAM_PATH : input .pairsam file. If the path ends with .gz or .lz4, the input is decompressed by pbgzip or lz4c. By default, the input is read from stdin. ''' split_py(pairsam_path, output_pairs, output_sam, **kwargs) def split_py(pairsam_path, output_pairs, output_sam, **kwargs): instream = (_fileio.auto_open(pairsam_path, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if pairsam_path else sys.stdin) # Output streams if (not output_pairs) and (not output_sam): raise ValueError('At least one output (pairs and/or sam) must be specified!') if (output_pairs == '-') and (output_sam == '-'): raise ValueError('Only one output (pairs or sam) can be printed in stdout!') outstream_pairs = (sys.stdout if (output_pairs=='-') else (_fileio.auto_open(output_pairs, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output_pairs else None)) outstream_sam = (sys.stdout if (output_sam=='-') else (_fileio.auto_open(output_sam, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output_sam else None)) header, body_stream = _headerops.get_header(instream) header = _headerops.append_new_pg(header, ID=UTIL_NAME, PN=UTIL_NAME) columns = _headerops.extract_column_names(header) has_sams = False if columns: # trust the column order specified in the header if ('sam1' in columns) and ('sam2' in columns): sam1col = columns.index('sam1') sam2col = columns.index('sam2') columns.pop(max(sam1col, sam2col)) columns.pop(min(sam1col, sam2col)) header = _headerops._update_header_entry( header, 'columns', ' '.join(columns)) has_sams = True elif ('sam1' in columns) != ('sam1' in columns): raise ValueError( 'According to the #columns header field only one sam entry is present') else: # assume that the file has sam columns and follows the pairsam format sam1col = _pairsam_format.COL_SAM1 sam2col = _pairsam_format.COL_SAM2 has_sams = True if outstream_pairs: outstream_pairs.writelines((l+'\n' for l in header)) if outstream_sam: outstream_sam.writelines( (l[11:].strip()+'\n' for l in header if l.startswith('#samheader:'))) # Split sam1 = None sam2 = None for line in body_stream: cols = line.rstrip().split(_pairsam_format.PAIRSAM_SEP) if has_sams: if sam1col < sam2col: sam2 = cols.pop(sam2col) sam1 = cols.pop(sam1col) else: sam1 = cols.pop(sam1col) sam2 = cols.pop(sam2col) if outstream_pairs: # hard-coded tab separator to follow the DCIC pairs standard outstream_pairs.write('\t'.join(cols)) outstream_pairs.write('\n') if (outstream_sam and has_sams): for col in (sam1, sam2): if col != '.': for sam_entry in col.split(_pairsam_format.INTER_SAM_SEP): outstream_sam.write(sam_entry.replace(_pairsam_format.SAM_SEP,'\t')) outstream_sam.write('\n') if outstream_pairs and outstream_pairs != sys.stdout: outstream_pairs.close() if outstream_sam and outstream_sam != sys.stdout: outstream_sam.close() if __name__ == '__main__': split() pairtools-0.3.0/pairtools/pairtools_stats.py000066400000000000000000000503001345765554600213700ustar00rootroot00000000000000 #!/usr/bin/env python # -*- coding: utf-8 -*- import io import sys import click import numpy as np from collections import OrderedDict, Mapping from . import _fileio, _pairsam_format, cli, _headerops, common_io_options UTIL_NAME = 'pairtools_stats' @cli.command() @click.argument( 'input_path', type=str, nargs=-1, required=False) @click.option( '-o', "--output", type=str, default="", help='output stats tsv file.') @click.option( "--merge", is_flag=True, help='If specified, merge multiple input stats files instead of calculating' ' statistics of a .pairs/.pairsam file. Merging is performed via summation of' ' all overlapping statistics. Non-overlapping statistics are appended to' ' the end of the file.', ) @common_io_options def stats(input_path, output, merge, **kwargs): '''Calculate pairs statistics. INPUT_PATH : by default, a .pairs/.pairsam file to calculate statistics. If not provided, the input is read from stdin. If --merge is specified, then INPUT_PATH is interpreted as an arbitrary number of stats files to merge. The files with paths ending with .gz/.lz4 are decompressed by pbgzip/lz4c. ''' stats_py(input_path, output, merge, **kwargs) def stats_py(input_path, output, merge, **kwargs): if merge: do_merge(output, input_path, **kwargs) return instream = (_fileio.auto_open(input_path[0], mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) if input_path else sys.stdin) outstream = (_fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) if output else sys.stdout) header, body_stream = _headerops.get_header(instream) # new stats class stuff would come here ... stats = PairCounter() # Collecting statistics for line in body_stream: cols = line.rstrip().split(_pairsam_format.PAIRSAM_SEP) # algn1: chrom1 = cols[_pairsam_format.COL_C1] pos1 = int(cols[_pairsam_format.COL_P1]) strand1 = cols[_pairsam_format.COL_S1] # algn2: chrom2 = cols[_pairsam_format.COL_C2] pos2 = int(cols[_pairsam_format.COL_P2]) strand2 = cols[_pairsam_format.COL_S2] # pair type: pair_type = cols[_pairsam_format.COL_PTYPE] stats.add_pair(chrom1, pos1, strand1, chrom2, pos2, strand2, pair_type) # save statistics to file ... stats.save(outstream) if instream != sys.stdin: instream.close() if outstream != sys.stdout: outstream.close() def do_merge(output, files_to_merge, **kwargs): # Parse all stats files. stats = [] for stat_file in files_to_merge: f = _fileio.auto_open(stat_file, mode='r', nproc=kwargs.get('nproc_in'), command=kwargs.get('cmd_in', None)) # use a factory method to instanciate PairCounter stat = PairCounter.from_file(f) stats.append(stat) f.close() # combine stats from several files (files_to_merge): out_stat = sum(stats) # Save merged stats. outstream = _fileio.auto_open(output, mode='w', nproc=kwargs.get('nproc_out'), command=kwargs.get('cmd_out', None)) # save statistics to file ... out_stat.save(outstream) if outstream != sys.stdout: outstream.close() class PairCounter(Mapping): """ A Counter for Hi-C pairs that accumulates various statistics. PairCounter implements two interfaces to access multi-level statistics: 1. as a nested dict, e.g. pairCounter['pair_types']['LL'] 2. as a flat dict, with the level keys separated by '/', e.g. pairCounter['pair_types/LL'] Other features: -- PairCounters can be saved into/loaded from a file -- multiple PairCounters can be merged via addition. """ _SEP = '\t' _KEY_SEP = '/' def __init__(self, min_log10_dist=0, max_log10_dist=9, log10_dist_bin_step=0.25): self._stat = OrderedDict() # some variables used for initialization: # genomic distance bining for the ++/--/-+/+- distribution self._dist_bins = (np.r_[0, np.round(10**np.arange(min_log10_dist, max_log10_dist+0.001, log10_dist_bin_step)) .astype(np.int)] ) # establish structure of an empty _stat: self._stat['total'] = 0 self._stat['total_unmapped'] = 0 self._stat['total_single_sided_mapped'] = 0 # total_mapped = total_dups + total_nodups self._stat['total_mapped'] = 0 self._stat['total_dups'] = 0 self._stat['total_nodups'] = 0 ######################################## # the rest of stats are based on nodups: ######################################## self._stat['cis'] = 0 self._stat['trans'] = 0 self._stat['pair_types'] = {} # to be removed: self._stat['dedup'] = {} self._stat['cis_1kb+'] = 0 self._stat['cis_2kb+'] = 0 self._stat['cis_4kb+'] = 0 self._stat['cis_10kb+'] = 0 self._stat['cis_20kb+'] = 0 self._stat['cis_40kb+'] = 0 self._stat['chrom_freq'] = OrderedDict() self._stat['dist_freq'] = OrderedDict([ ('+-', np.zeros(len(self._dist_bins), dtype=np.int)), ('-+', np.zeros(len(self._dist_bins), dtype=np.int)), ('--', np.zeros(len(self._dist_bins), dtype=np.int)), ('++', np.zeros(len(self._dist_bins), dtype=np.int)), ]) def __getitem__(self, key): if isinstance(key, str): # let's strip any unintentional '/' # from either side of the key key = key.strip('/') if self._KEY_SEP in key: # multi-key to access nested elements k_fields = key.split(self._KEY_SEP) else: # single-key access flat part of PairCounter # or to access highest level of hierarchy return self._stat[key] else: # clearly an error: raise ValueError( '{} is not a valid key: must be str'.format(key)) # K_FIELDS: # process multi-key case: # in this case key must be in ['pair_types','chrom_freq','dist_freq','dedup'] # get the first 'k' and keep the remainders in 'k_fields' k = k_fields.pop(0) if k in ['pair_types', 'dedup']: # assert there is only one element in key_fields left: # 'pair_types' and 'dedup' treated the same if len(k_fields) == 1: return self._stat[k][k_fields[0]] else: raise ValueError( '{} is not a valid key: {} section implies 1 identifier'.format(key, k)) elif k == 'chrom_freq': # assert remaining key_fields == [chr1, chr2]: if len(k_fields) == 2: return self._stat[k][tuple(k_fields)] else: raise ValueError( '{} is not a valid key: {} section implies 2 identifiers'.format(key, k)) elif k == 'dist_freq': # assert that last element of key_fields is the 'directions' # THIS IS DONE FOR CONSISTENCY WITH .stats FILE # SHOULD THAT BE CHANGED IN .stats AND HERE AS WELL? if len(k_fields) == 2: # assert 'dirs' in ['++','--','+-','-+'] dirs = k_fields.pop() # there is only genomic distance range of the bin that's left: bin_range, = k_fields # extract left border of the bin "1000000+" or "1500-6000": dist_bin_left = bin_range.strip('+') if bin_range.endswith('+') \ else bin_range.split('-')[0] # get the index of that bin: bin_idx = np.searchsorted(self._dist_bins, int(dist_bin_left), 'right') - 1 # store corresponding value: return self._stat['dist_freq'][dirs][bin_idx] else: raise ValueError( '{} is not a valid key: {} section implies 2 identifiers'.format(key,k)) else: raise ValueError( '{} is not a valid key'.format(k)) def __iter__(self): return iter(self._stat) def __len__(self): return len(self._stat) @classmethod def from_file(cls, file_handle): """create instance of PairCounter from file Parameters ---------- file_handle: file handle Returns ------- PairCounter new PairCounter filled with the contents of the input file """ # fill in from file - file_handle: stat_from_file = cls() for l in file_handle: fields = l.strip().split(cls._SEP) if len(fields) == 0: # skip empty lines: continue if len(fields) != 2: # expect two _SEP separated values per line: raise _fileio.ParseError( '{} is not a valid stats file'.format(file_handle.name)) # extract key and value, then split the key: putative_key, putative_val = fields[0], fields[1] key_fields = putative_key.split(cls._KEY_SEP) # we should impose a rigid structure of .stats or redo it: if len(key_fields)==1: key = key_fields[0] if key in stat_from_file._stat: stat_from_file._stat[key] = int(fields[1]) else: raise _fileio.ParseError( '{} is not a valid stats file: unknown field {} detected'.format(file_handle.name,key)) else: # in this case key must be in ['pair_types','chrom_freq','dist_freq','dedup'] # get the first 'key' and keep the remainders in 'key_fields' key = key_fields.pop(0) if key in ['pair_types', 'dedup']: # assert there is only one element in key_fields left: # 'pair_types' and 'dedup' treated the same if len(key_fields) == 1: stat_from_file._stat[key][key_fields[0]] = int(fields[1]) else: raise _fileio.ParseError( '{} is not a valid stats file: {} section implies 1 identifier'.format(file_handle.name,key)) elif key == 'chrom_freq': # assert remaining key_fields == [chr1, chr2]: if len(key_fields) == 2: stat_from_file._stat[key][tuple(key_fields)] = int(fields[1]) else: raise _fileio.ParseError( '{} is not a valid stats file: {} section implies 2 identifiers'.format(file_handle.name,key)) elif key == 'dist_freq': # assert that last element of key_fields is the 'directions' if len(key_fields) == 2: # assert 'dirs' in ['++','--','+-','-+'] dirs = key_fields.pop() # there is only genomic distance range of the bin that's left: bin_range, = key_fields # extract left border of the bin "1000000+" or "1500-6000": dist_bin_left = (bin_range.strip('+') if bin_range.endswith('+') else bin_range.split('-')[0]) # get the index of that bin: bin_idx = np.searchsorted( stat_from_file._dist_bins, int(dist_bin_left), 'right') - 1 # store corresponding value: stat_from_file._stat[key][dirs][bin_idx] = int(fields[1]) else: raise _fileio.ParseError( '{} is not a valid stats file: {} section implies 2 identifiers'.format(file_handle.name,key)) else: raise _fileio.ParseError( '{} is not a valid stats file: unknown field {} detected'.format(file_handle.name,key)) # return PairCounter from a non-empty dict: return stat_from_file def add_pair(self, chrom1, pos1, strand1, chrom2, pos2, strand2, pair_type): """Gather statistics for a Hi-C pair and add to the PairCounter. Parameters ---------- chrom1: str chromosome of the first read pos1: int position of the first read strand1: str strand of the first read chrom2: str chromosome of the first read pos2: int position of the first read strand2: str strand of the first read pair_type: str type of the mapped pair of reads """ self._stat['total'] += 1 # collect pair type stats including DD: self._stat['pair_types'][pair_type] = self._stat['pair_types'].get(pair_type,0) + 1 if chrom1 == '!' and chrom2 == '!': self._stat['total_unmapped'] += 1 elif chrom1 != '!' and chrom2 != '!': self._stat['total_mapped'] += 1 # only mapped ones can be duplicates: if pair_type == 'DD': self._stat['total_dups'] += 1 else: self._stat['total_nodups'] += 1 self._stat['chrom_freq'][(chrom1, chrom2)] = ( self._stat['chrom_freq'].get((chrom1, chrom2), 0) + 1) if chrom1 == chrom2: self._stat['cis'] += 1 dist = np.abs(pos2-pos1) bin_idx = np.searchsorted(self._dist_bins, dist, 'right') - 1 self._stat['dist_freq'][strand1+strand2][bin_idx] += 1 if dist >= 1000: self._stat['cis_1kb+'] += 1 if dist >= 2000: self._stat['cis_2kb+'] += 1 if dist >= 4000: self._stat['cis_4kb+'] += 1 if dist >= 10000: self._stat['cis_10kb+'] += 1 if dist >= 20000: self._stat['cis_20kb+'] += 1 if dist >= 40000: self._stat['cis_40kb+'] += 1 else: self._stat['trans'] += 1 else: self._stat['total_single_sided_mapped'] += 1 def __add__(self, other): # both PairCounter are implied to have a list of common fields: # # 'total', 'total_unmapped', 'total_single_sided_mapped', 'total_mapped', # 'cis', 'trans', 'pair_types', 'cis_1kb+', 'cis_2kb+', # 'cis_10kb+', 'cis_20kb+', 'chrom_freq', 'dist_freq', 'dedup' # # initialize empty PairCounter for the result of summation: sum_stat = PairCounter() # use the empty PairCounter to iterate over: for k,v in sum_stat._stat.items(): # not nested fields are summed trivially: if isinstance(v, int): sum_stat._stat[k] = self._stat[k] + other._stat[k] # sum nested dicts/arrays in a context dependet manner: else: if k in ['pair_types','dedup']: # handy function for summation of a pair of dicts: # https://stackoverflow.com/questions/10461531/merge-and-sum-of-two-dictionaries sum_dicts = lambda dict_x,dict_y: { key: dict_x.get(key, 0) + dict_y.get(key, 0) for key in set(dict_x) | set(dict_y) } # sum a pair of corresponding dicts: sum_stat._stat[k] = sum_dicts(self._stat[k], other._stat[k]) if k == 'chrom_freq': # union list of keys (chr1,chr2) with potential duplicates: union_keys_with_dups = list(self._stat[k].keys()) + list(other._stat[k].keys()) # OrderedDict.fromkeys will take care of keys' order and duplicates in a consistent manner: # https://stackoverflow.com/questions/1720421/how-to-concatenate-two-lists-in-python # last comment to the 3rd Answer sum_stat._stat[k] = OrderedDict.fromkeys( union_keys_with_dups ) # perform a summation: for union_key in sum_stat._stat[k]: sum_stat._stat[k][union_key] = ( self._stat[k].get(union_key, 0) + other._stat[k].get(union_key, 0) ) if k == 'dist_freq': for dirs in sum_stat[k]: sum_stat[k][dirs] = self._stat[k][dirs] + other._stat[k][dirs] return sum_stat # we need this to be able to sum(list_of_PairCounters) def __radd__(self, other): if other == 0: return self else: return self.__add__(other) def flatten(self): """return a flattened OrderedDic (formatted same way as .stats file) """ # OrderedDict for flat store: flat_stat = OrderedDict() # Storing statistics for k,v in self._stat.items(): if isinstance(v, int): flat_stat[k] = v # store nested dicts/arrays in a context dependet manner: # nested categories are stored only if they are non-trivial else: if (k == 'dist_freq') and v: for i in range(len(self._dist_bins)): for dirs, freqs in v.items(): # last bin is treated differently: "100000+" vs "1200-3000": if i != len(self._dist_bins) - 1: formatted_key = self._KEY_SEP.join(['{}','{}-{}','{}']).format( k, self._dist_bins[i], self._dist_bins[i+1], dirs) else: formatted_key = self._KEY_SEP.join(['{}','{}+','{}']).format( k, self._dist_bins[i], dirs) #store key,value pair: flat_stat[formatted_key] = freqs[i] elif (k in ['pair_types','dedup']) and v: # 'pair_types' and 'dedup' are simple dicts inside, # treat them the exact same way: for k_item, freq in v.items(): formatted_key = self._KEY_SEP.join(['{}','{}']).format(k, k_item) #store key,value pair: flat_stat[formatted_key] = freq elif (k == 'chrom_freq') and v: for (chrom1, chrom2), freq in v.items(): formatted_key = self._KEY_SEP.join(['{}','{}','{}']).format(k, chrom1, chrom2) #store key,value pair: flat_stat[formatted_key] = freq # return flattened OrderedDict return flat_stat def save(self, outstream): """save PairCounter to tab-delimited text file. Flattened version of PairCounter is stored in the file. Parameters ---------- outstream: file handle Note ---- The order of the keys is not guaranteed Merging several .stats is not associative with respect to key order: merge(A,merge(B,C)) != merge(merge(A,B),C). Theys shou5ld match exactly, however, when soprted: sort(merge(A,merge(B,C))) == sort(merge(merge(A,B),C)) """ # write flattened version of the PairCounter to outstream for k,v in self.flatten().items(): outstream.write('{}{}{}\n'.format(k, self._SEP, v)) if __name__ == '__main__': stats() pairtools-0.3.0/readthedocs.yml000066400000000000000000000002021345765554600165540ustar00rootroot00000000000000 # .readthedocs.yml build: image: latest python: version: 3.6 pip_install: true requirements_file: requirements_doc.txt pairtools-0.3.0/requirements.txt000066400000000000000000000000501345765554600170310ustar00rootroot00000000000000cython numpy>=1.10 nose>=1.3 click>=6.6 pairtools-0.3.0/requirements_doc.txt000066400000000000000000000001451345765554600176630ustar00rootroot00000000000000Cython numpy>=1.10 nose>=1.3 click>=6.6 git+https://github.com/golobor/sphinx-click sphinx_rtd_theme pairtools-0.3.0/setup.py000066400000000000000000000062661345765554600152760ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import io import os import re import glob from setuptools import setup, find_packages from setuptools.extension import Extension try: from Cython.Distutils import build_ext as _build_ext from Cython.Build import cythonize HAVE_CYTHON = True except ImportError: from setuptools.command.build_ext import build_ext as _build_ext HAVE_CYTHON = False classifiers = """\ Development Status :: 4 - Beta Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 """ def _read(*parts, **kwargs): filepath = os.path.join(os.path.dirname(__file__), *parts) encoding = kwargs.pop('encoding', 'utf-8') with io.open(filepath, encoding=encoding) as fh: text = fh.read() return text def get_version(): version = re.search( r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', _read('pairtools', '__init__.py'), re.MULTILINE).group(1) return version long_description = _read('README.md') install_requires = [l for l in _read('requirements.txt').split('\n') if l] def get_ext_modules(): ext = '.pyx' if HAVE_CYTHON else '.c' src_files = glob.glob(os.path.join( os.path.dirname(__file__), "pairtools", "*" + ext)) ext_modules = [] for src_file in src_files: name = "pairtools." + os.path.splitext(os.path.basename(src_file))[0] ext_modules.append(Extension(name, [src_file])) if HAVE_CYTHON: # .pyx to .c ext_modules = cythonize(ext_modules) #, annotate=True return ext_modules class build_ext(_build_ext): # Extension module build configuration def finalize_options(self): _build_ext.finalize_options(self) # Fix to work with bootstrapped numpy installation # http://stackoverflow.com/a/21621689/579416 # Prevent numpy from thinking it is still in its setup process: __builtins__.__NUMPY_SETUP__ = False import numpy self.include_dirs.append(numpy.get_include()) def run(self): # Import numpy here, only when headers are needed import numpy # Add numpy headers to include_dirs self.include_dirs.append(numpy.get_include()) # Call original build_ext command _build_ext.run(self) setup( name='pairtools', author='Mirny Lab', author_email='espresso@mit.edu', version=get_version(), license='MIT', description='CLI tools to process mapped Hi-C data', long_description=long_description, long_description_content_type="text/markdown", keywords=['genomics', 'bioinformatics', 'Hi-C', 'contact'], url='https://github.com/mirnylab/pairtools', ext_modules=get_ext_modules(), cmdclass = {'build_ext': build_ext}, zip_safe=False, classifiers=[s.strip() for s in classifiers.split('\n') if s], install_requires=install_requires, entry_points={ 'console_scripts': [ 'pairtools = pairtools:cli', #'pairsamtools = pairtools:cli', ] }, packages = find_packages() ) pairtools-0.3.0/tests/000077500000000000000000000000001345765554600147145ustar00rootroot00000000000000pairtools-0.3.0/tests/data/000077500000000000000000000000001345765554600156255ustar00rootroot00000000000000pairtools-0.3.0/tests/data/mock.2.pairsam000066400000000000000000000021361345765554600202760ustar00rootroot00000000000000## pairs format v1.0.0 #shape: upper triangle #genome_assembly: unknown #samheader: @SQ SN:chr1 LN:100 #samheader: @SQ SN:chr2 LN:100 #samheader: @SQ SN:chr3 LN:100 #samheader: @PG ID:bwa PN:bwa VN:0.7.15-r1140 CL:bwa mem -SP /path/ucsc.hg19.fasta.gz /path/1.fastq.gz /path/2.fastq.gz #chromosomes: chr2 chr3 chr1 #columns: readID chrom1 pos1 chrom2 pos2 strand1 strand2 pair_type sam1 sam2 readid01 chr1 1 chr2 25 + + UU readid01129chr1160101Mchr2250CGFFXS:i:0Yt:Z:UU readid0165chr22560101Mchr110ATIIXS:i:0Yt:Z:UU readid02 chr1 1 chr1 40 + + UU readid02129chr1160101Mchr1400CGFFXS:i:0Yt:Z:UU readid0265chr14060101Mchr110ATIIXS:i:0Yt:Z:UU readid03 chr1 1 chr1 3 + + UR readid03129chr1160101Mchr130CGFFXS:i:0Yt:Z:UR readid0365chr1360101Mchr110ATIIXS:i:0Yt:Z:UR readid04 ! 0 chr1 3 - + NU readid04129chr1160101Mchr130CGFFXS:i:0Yt:Z:NU readid0465chr1360101Mchr110ATIIXS:i:0Yt:Z:NU readid05 ! 0 ! 0 - - NN readid05129chr1160101Mchr130CGFFXS:i:0Yt:Z:NN readid0565chr1360101Mchr110ATIIXS:i:0Yt:Z:NN pairtools-0.3.0/tests/data/mock.4dedup.pairsam000066400000000000000000000007341345765554600213240ustar00rootroot00000000000000## pairs format v1.0.0 #sorted: chr1-chr2-pos1-pos2 #shape: upper triangle #genome_assembly: unknown #chromosomes: chr1 chr2 #columns: readID chrom1 pos1 chrom2 pos2 strand1 strand2 pair_type sam1 sam2 . ! 0 chr1 25 - + NU . . . ! 0 chr1 25 - + NU . . . chr1 1 chr1 20 + + UU . . . chr1 1 chr1 20 + + UU . . . chr1 1 chr1 25 + + UU . . . chr1 1 chr1 27 + + UU . . . chr1 1 chr1 28 + - UU . . . chr1 1 chr1 28 + + UU . . . chr1 1 chr1 50 + + UU . . . chr1 1 chr2 25 + + UU . . pairtools-0.3.0/tests/data/mock.4filterbycov.pairs000066400000000000000000000013151345765554600222310ustar00rootroot00000000000000## pairs format v1.0.0 #shape: upper triangle #genome_assembly: unknown #samheader: @SQ SN:chr1 LN:100 #samheader: @SQ SN:chr2 LN:100 #samheader: @SQ SN:chr3 LN:100 #samheader: @PG ID:bwa PN:bwa VN:0.7.15-r1140 CL:bwa mem -SP /path/ucsc.hg19.fasta.gz /path/1.fastq.gz /path/2.fastq.gz #chromosomes: chr2 chr3 chr1 #chromsize: chr2 100 #chromsize: chr3 100 #chromsize: chr1 100 #columns: readID chrom1 pos1 chrom2 pos2 strand1 strand2 pair_type sam1 sam2 readid01 chr2 40 chr3 2 + + UU readid02 chr1 6 chr1 9 + + UR readid03 chr1 1 chr2 20 + + UU readid04 chr1 50 chr1 1 + + UU readid05 chr1 1 chr1 5 + + UU readid06 chr1 20 chr1 30 + + UR readid07 ! 0 chr1 3 - + NU readid08 ! 0 chr1 3 - + MU readid09 ! 0 ! 0 - - WW pairtools-0.3.0/tests/data/mock.4flip.pairs000066400000000000000000000014051345765554600206330ustar00rootroot00000000000000## pairs format v1.0.0 #shape: upper triangle #genome_assembly: unknown #samheader: @SQ SN:chr1 LN:100 #samheader: @SQ SN:chr2 LN:100 #samheader: @SQ SN:chr3 LN:100 #samheader: @PG ID:bwa PN:bwa VN:0.7.15-r1140 CL:bwa mem -SP /path/ucsc.hg19.fasta.gz /path/1.fastq.gz /path/2.fastq.gz #chromosomes: chr1 chr2 chr3 #chromsize: chr1 100 #chromsize: chr2 100 #chromsize: chr3 100 #columns: readID chrom1 pos1 chrom2 pos2 strand1 strand2 pair_type sam1 sam2 readid01 chr1 1 chr1 2 + + UU readid02 chr1 1 chr2 2 + + UU readid03 chr1 2 chr1 1 + + UU readid04 chr1 21 chr1 2 + + UU readid05 chr2 2 chr1 1 + + UU readid06 chr2 1 chr1 2 + + UU readid07 chr1 2 chr1 1 - + UU readid08 chr1 2 chr1 1 + + RU readid09 ! 0 chr1 3 - + NU readid10 ! 0 chr1 3 - + MU readid11 ! 0 ! 0 + - WW pairtools-0.3.0/tests/data/mock.chrom.sizes000066400000000000000000000000221345765554600207360ustar00rootroot00000000000000chr1 100 chr2 100 pairtools-0.3.0/tests/data/mock.pairsam000066400000000000000000000031201345765554600201300ustar00rootroot00000000000000## pairs format v1.0.0 #shape: upper triangle #genome_assembly: unknown #samheader: @SQ SN:chr1 LN:100 #samheader: @SQ SN:chr2 LN:100 #samheader: @SQ SN:chr3 LN:100 #samheader: @PG ID:bwa PN:bwa VN:0.7.15-r1140 CL:bwa mem -SP /path/ucsc.hg19.fasta.gz /path/1.fastq.gz /path/2.fastq.gz #chromosomes: chr2 chr3 chr1 #chromsize: chr2 100 #chromsize: chr3 100 #chromsize: chr1 100 #columns: readID chrom1 pos1 chrom2 pos2 strand1 strand2 pair_type sam1 sam2 readid01 chr1 1 chr2 20 + + UU readid01129chr1160101Mchr2200CGFFXS:i:0Yt:Z:UU readid0165chr22060101Mchr110ATIIXS:i:0Yt:Z:UU readid02 chr1 1 chr1 50 + + UU readid02129chr1160101Mchr1500CGFFXS:i:0Yt:Z:UU readid0265chr15060101Mchr110ATIIXS:i:0Yt:Z:UU readid03 chr1 1 chr1 2 + + UU readid03129chr1160101Mchr120CGFFXS:i:0Yt:Z:UU readid0365chr1260101Mchr110ATIIXS:i:0Yt:Z:UU readid04 chr1 1 chr1 3 + + UR readid04129chr1160101Mchr130CGFFXS:i:0Yt:Z:UR readid0465chr1360101Mchr110ATIIXS:i:0Yt:Z:UR readid05 chr2 1 chr3 2 + + UU readid05129chr2160101Mchr320CGFFXS:i:0Yt:Z:UU readid0565chr3260101Mchr210ATIIXS:i:0Yt:Z:UU readid06 ! 0 chr1 3 - + NU readid06129chr1160101Mchr130CGFFXS:i:0Yt:Z:NU readid0665chr1360101Mchr110ATIIXS:i:0Yt:Z:NU readid07 ! 0 chr1 3 - + MU readid07129chr1160101Mchr130CGFFXS:i:0Yt:Z:NU readid0765chr1360101Mchr110ATIIXS:i:0Yt:Z:NU readid08 ! 0 ! 0 - - WW readid08129chr1160101Mchr130CGFFXS:i:0Yt:Z:WW readid0865chr1360101Mchr110ATIIXS:i:0Yt:Z:WW pairtools-0.3.0/tests/data/mock.sam000066400000000000000000000120711345765554600172610ustar00rootroot00000000000000@SQ SN:chr1 LN:100 @SQ SN:chr2 LN:100 @PG ID:mock PN:mock VN:0.0.0 CL:mock readid01 65 chr1 10 60 50M chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,200,+,+,UU readid01 129 chr1 200 60 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,200,+,+,UU readid02 97 chr1 10 60 50M chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,249,+,-,UU readid02 145 chr1 200 60 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,249,+,-,UU readid03 65 chr1 10 60 1S49M chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,200,+,+,UU readid03 129 chr1 200 60 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,200,+,+,UU readid04 81 chr1 10 60 49M1S chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,58,chr1,200,-,+,UU readid04 161 chr1 200 60 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,58,chr1,200,-,+,UU readid05 97 chr1 10 60 50M chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,248,+,-,UU readid05 145 chr1 200 60 1S49M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,248,+,-,UU readid06 97 chr1 10 60 50M chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,248,+,-,UU readid06 145 chr1 200 60 49M1S chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,248,+,-,UU readid07 97 chr1 10 60 50M chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,247,+,-,UU readid07 145 chr1 200 60 1S48M1S chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,247,+,-,UU readid08 105 chr1 10 60 50M = 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,chr1,10,-,+,NU readid08 149 * 0 0 * chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,chr1,10,-,+,NU readid09 85 * 0 0 * chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,chr1,10,-,+,NU readid09 169 chr1 10 60 50M = 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,chr1,10,-,+,NU readid10 77 * 0 0 * * 0 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,NN readid10 141 * 0 0 * * 0 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,NN readid11 105 chr1 10 0 50M = 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,NM readid11 149 * 0 0 * chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,NM readid12 85 * 0 0 * chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,NM readid12 169 chr1 10 0 50M = 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,NM readid13 65 chr1 10 0 50M chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,chr1,200,-,+,MU readid13 129 chr1 200 60 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,chr1,200,-,+,MU readid14 65 chr1 10 60 50M chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,chr1,10,-,+,MU readid14 129 chr1 200 0 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,chr1,10,-,+,MU readid15 65 chr1 10 0 50M chr1 200 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,MM readid15 129 chr1 200 0 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,MM readid16 65 chr1 10 60 25M25S chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,300,-,25M25H,60,0; SIMULATED:chr1,10,chr1,200,+,+,UR readid16 2129 chr1 300 60 25M25H chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,10,+,25M25S,60,0; SIMULATED:chr1,10,chr1,200,+,+,UR readid16 129 chr1 200 60 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,200,+,+,UR readid17 65 chr1 10 60 25M25S chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,5300,-,25M25H,60,0; SIMULATED:!,0,!,0,-,-,WW readid17 2129 chr1 5300 60 25M25H chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,10,+,25M25S,60,0; SIMULATED:!,0,!,0,-,-,WW readid17 129 chr1 200 60 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,WW readid18 65 chr1 10 60 25M25S chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,300,+,25M25H,60,0; SIMULATED:!,0,!,0,-,-,WW readid18 2113 chr1 300 60 25M25H chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,10,+,25M25S,60,0; SIMULATED:!,0,!,0,-,-,WW readid18 129 chr1 200 60 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,WW readid19 81 chr1 300 60 25M25S chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,10,+,25M25H,60,0; SIMULATED:chr1,10,chr1,200,+,+,UR readid19 2113 chr1 10 60 25M25H chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr10,300,-,25M25S,60,0; SIMULATED:chr1,10,chr1,200,+,+,UR readid19 129 chr1 200 60 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:chr1,10,chr1,200,+,+,UR readid20 65 chr1 10 60 25M25S chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,300,+,25M25H,60,0; SIMULATED:!,0,!,0,-,-,WW readid20 2113 chr1 300 60 25M25H chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,10,+,25M25S,60,0; SIMULATED:!,0,!,0,-,-,WW readid20 129 chr1 200 60 25M25S chr1 10 0 SEQ PHRED FLAG1 SA:Z:chr1,2000,+,25S25M,60,0; SIMULATED:!,10,!,0,-,-,WW readid20 2177 chr1 2000 60 25S25M chr1 10 0 SEQ PHRED FLAG1 SA:Z:chr1,2000,+,25S25M,60,0; SIMULATED:!,0,!,0,-,-,WW readid21 105 chr1 10 60 25M25S * 0 0 SEQ PHRED FLAG1 SA:Z:chr1,5300,-,25M25H,60,0; SIMULATED:!,0,!,0,-,-,WW readid21 2169 chr1 5300 60 25M25H * 0 0 SEQ PHRED FLAG1 SA:Z:chr1,10,+,25M25S,60,0; SIMULATED:!,0,!,0,-,-,WW readid21 141 * 0 0 * chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,WW readid22 65 chr1 10 60 25M25S chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,5300,-,25M25H,60,0; SIMULATED:!,0,!,0,-,-,WW readid22 2129 chr1 5300 60 25M25H chr1 200 0 SEQ PHRED FLAG1 SA:Z:chr1,10,+,25M25S,60,0; SIMULATED:!,0,!,0,-,-,WW readid22 129 chr1 200 0 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,WW readid23 129 chr1 200 0 50M chr1 10 0 SEQ PHRED FLAG1 FLAG2 SIMULATED:!,0,!,0,-,-,XX pairtools-0.3.0/tests/test_dedup.py000066400000000000000000000076171345765554600174410ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys import subprocess from nose.tools import assert_raises, with_setup import tempfile testdir = os.path.dirname(os.path.realpath(__file__)) mock_pairsam_path = os.path.join(testdir, 'data', 'mock.4dedup.pairsam') tmpdir = tempfile.TemporaryDirectory() tmpdir_name = tmpdir.name dedup_path = os.path.join(tmpdir_name, 'dedup.pairsam') unmapped_path = os.path.join(tmpdir_name, 'unmapped.pairsam') dups_path = os.path.join(tmpdir_name, 'dups.pairsam') dedup_markdups_path = os.path.join(tmpdir_name, 'dedup.markdups.pairsam') unmapped_markdups_path = os.path.join(tmpdir_name, 'unmapped.markdups.pairsam') dups_markdups_path = os.path.join(tmpdir_name, 'dups.markdups.pairsam') max_mismatch = 3 def setup_func(): try: subprocess.check_output( ['python', '-m', 'pairtools', 'dedup', mock_pairsam_path, '--output', dedup_path, '--output-dups', dups_path, '--output-unmapped', unmapped_path, '--max-mismatch', str(max_mismatch) ], ) subprocess.check_output( ['python', '-m', 'pairtools', 'dedup', mock_pairsam_path, '--mark-dups', '--output', dedup_markdups_path, '--output-dups', dups_markdups_path, '--output-unmapped', unmapped_markdups_path, '--max-mismatch', str(max_mismatch) ], ) except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e def teardown_func(): tmpdir.cleanup() @with_setup(setup_func, teardown_func) def test_mock_pairsam(): pairsam_pairs = [l.strip().split('\t') for l in open(mock_pairsam_path, 'r') if not l.startswith('#') and l.strip()] for (ddp, up, dp) in [(dedup_path, unmapped_path, dups_path), (dedup_markdups_path, unmapped_markdups_path, dups_markdups_path)]: dedup_pairs = [l.strip().split('\t') for l in open(ddp, 'r') if not l.startswith('#') and l.strip()] unmapped_pairs = [l.strip().split('\t') for l in open(up, 'r') if not l.startswith('#') and l.strip()] dup_pairs = [l.strip().split('\t') for l in open(dp, 'r') if not l.startswith('#') and l.strip()] # check that at least a few pairs remained in deduped and dup files assert len(dedup_pairs) > 0 assert len(dup_pairs) > 0 assert len(unmapped_pairs) > 0 # check that all pairsam entries survived deduping: assert (len(dedup_pairs) + len(unmapped_pairs) + len(dup_pairs) == len(pairsam_pairs)) def pairs_overlap(pair1, pair2, max_mismatch): overlap = ( (pair1[1] == pair2[1]) and (pair1[3] == pair2[3]) and (pair1[5] == pair2[5]) and (pair1[6] == pair2[6]) and (abs(int(pair1[2]) - int(pair2[2])) <= max_mismatch) and (abs(int(pair1[4]) - int(pair2[4])) <= max_mismatch) ) return overlap # check that deduped pairs do not overlap assert all([not pairs_overlap(pair1, pair2, 3) for i, pair1 in enumerate(dedup_pairs) for j, pair2 in enumerate(dedup_pairs) if i != j]) # check that the removed duplicates overlap with at least one of the # deduplicated entries assert all([ any([pairs_overlap(pair1, pair2, 3) for pair2 in dedup_pairs]) for pair1 in dup_pairs ]) pairtools-0.3.0/tests/test_filterbycov.py000066400000000000000000000102051345765554600206530ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys import subprocess from nose.tools import assert_raises, with_setup import tempfile testdir = os.path.dirname(os.path.realpath(__file__)) mock_pairs_path = os.path.join(testdir, 'data', 'mock.4filterbycov.pairs') tmpdir = tempfile.TemporaryDirectory() tmpdir_name = tmpdir.name params = [ {'max_dist': 0, 'max_cov' : 3}, {'max_dist': 0, 'max_cov' : 2}, {'max_dist': 1, 'max_cov' : 1}, ] for p in params: p['lowcov_path'] = os.path.join( tmpdir_name, 'lowcov.{}.{}.pairs'.format(p['max_dist'], p['max_cov']) ) p['highcov_path'] = os.path.join( tmpdir_name, 'highcov.{}.{}.pairs'.format(p['max_dist'], p['max_cov']) ) p['unmapped_path'] = os.path.join( tmpdir_name, 'unmapped.{}.{}.pairs'.format(p['max_dist'], p['max_cov']) ) def setup_func(): try: for p in params: subprocess.check_output( ['python', '-m', 'pairtools', 'filterbycov', mock_pairs_path, '--output', p['lowcov_path'], '--output-highcov', p['highcov_path'], '--output-unmapped', p['unmapped_path'], '--max-dist', str(p['max_dist']), '--max-cov', str(p['max_cov']), ] ) except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e def teardown_func(): tmpdir.cleanup() @with_setup(setup_func, teardown_func) def test_mock_pairs(): all_pairs = [l.strip().split('\t') for l in open(mock_pairs_path, 'r') if not l.startswith('#') and l.strip()] for p in params: lowcov_pairs = [l.strip().split('\t') for l in open(p['lowcov_path'], 'r') if not l.startswith('#') and l.strip()] highcov_pairs = [l.strip().split('\t') for l in open(p['highcov_path'], 'r') if not l.startswith('#') and l.strip()] unmapped_pairs = [l.strip().split('\t') for l in open(p['unmapped_path'], 'r') if not l.startswith('#') and l.strip()] # check that at least a few pairs remained in deduped and dup files #assert len(lowcov_pairs) > 0 #assert len(highcov_pairs) > 0 #assert len(unmapped_pairs) > 0 # check that all pairs entries survived deduping: assert (len(lowcov_pairs) + len(unmapped_pairs) + len(highcov_pairs) == len(all_pairs)) assert all([(pair[1] != '!' and pair[3] != '!') for pair in lowcov_pairs]) assert all([(pair[1] != '!' and pair[3] != '!') for pair in highcov_pairs]) assert all([(pair[1] == '!' or pair[3] == '!') for pair in unmapped_pairs]) def update_coverage(coverage, chrom, pos, max_dist): if chrom == '!': return coverage[chrom] = coverage.get(chrom, {}) for i in range(max(0, pos-max_dist), pos+max_dist+1): coverage[chrom][i] = coverage[chrom].get(i,0) + 1 coverage = {} for pair in all_pairs: update_coverage(coverage, pair[1], int(pair[2]), p['max_dist']) update_coverage(coverage, pair[3], int(pair[4]), p['max_dist']) for pair in lowcov_pairs: #print (p['max_cov'],p['max_dist']) #print (pair, coverage[pair[1]][int(pair[2])]) #print (pair, coverage[pair[3]][int(pair[4])]) assert (coverage[pair[1]][int(pair[2])] <= p['max_cov']) assert (coverage[pair[3]][int(pair[4])] <= p['max_cov']) for pair in highcov_pairs: #print (p['max_cov'],p['max_dist']) #print (pair, coverage[pair[1]][int(pair[2])]) #print (pair, coverage[pair[3]][int(pair[4])]) assert ( (coverage[pair[1]][int(pair[2])] > p['max_cov']) or (coverage[pair[3]][int(pair[4])] > p['max_cov']) ) pairtools-0.3.0/tests/test_flip.py000066400000000000000000000034351345765554600172640ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys import subprocess from nose.tools import assert_raises testdir = os.path.dirname(os.path.realpath(__file__)) mock_pairs_path = os.path.join(testdir, 'data', 'mock.4flip.pairs') mock_chromsizes_path = os.path.join(testdir, 'data', 'mock.chrom.sizes') def test_flip(): try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'flip', mock_pairs_path, '-c', mock_chromsizes_path ], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e orig_pairs = [l.strip().split('\t') for l in open(mock_pairs_path, 'r') if not l.startswith('#') and l.strip()] flipped_pairs = [l.strip().split('\t') for l in result.split('\n') if not l.startswith('#') and l.strip()] chrom_enum = {'!':0, 'chr1':1, 'chr2': 2} for orig_pair, flipped_pair in zip(orig_pairs, flipped_pairs): has_correct_order = ( (chrom_enum[orig_pair[1]], int(orig_pair[2])) <= (chrom_enum[orig_pair[3]], int(orig_pair[4])) ) if has_correct_order: assert(all([c1==c2 for c1,c2 in zip(orig_pair, flipped_pair)])) if not has_correct_order: assert(orig_pair[1] == flipped_pair[3]) assert(orig_pair[2] == flipped_pair[4]) assert(orig_pair[3] == flipped_pair[1]) assert(orig_pair[4] == flipped_pair[2]) assert(orig_pair[5] == flipped_pair[6]) assert(orig_pair[6] == flipped_pair[5]) assert(orig_pair[7] == flipped_pair[7][::-1]) pairtools-0.3.0/tests/test_headerops.py000066400000000000000000000107001345765554600202750ustar00rootroot00000000000000# -*- coding: utf-8 -*- from pairtools import _headerops from nose.tools import assert_raises, with_setup, raises def test_make_standard_header(): header = _headerops.make_standard_pairsheader() assert any([l.startswith('## pairs format') for l in header]) assert any([l.startswith('#shape') for l in header]) assert any([l.startswith('#columns') for l in header]) header = _headerops.make_standard_pairsheader( chromsizes=[('b', 100), ('c', 100), ('a', 100)]) assert sum([l.startswith('#chromsize') for l in header]) == 3 def test_samheaderops(): header = _headerops.make_standard_pairsheader() samheader = [ '@SQ\tSN:chr1\tLN:100', '@SQ\tSN:chr2\tLN:100', '@SQ\tSN:chr3\tLN:100', '@PG\tID:bwa\tPN:bwa\tCL:bwa', '@PG\tID:bwa-2\tPN:bwa\tCL:bwa\tPP:bwa' ] header_with_sam = _headerops.insert_samheader(header, samheader) assert len(header_with_sam) == len(header) + len(samheader) for l in samheader: assert any([l2.startswith('#samheader') and l in l2 for l2 in header_with_sam]) # test adding new programs to the PG chain header_extra_pg = _headerops.append_new_pg( header_with_sam, ID='test', PN='test') # test if all lines got transferred assert all([(old_l in header_extra_pg) for old_l in header_with_sam]) # test if one PG got added assert len(header_extra_pg) == len(header_with_sam) + 1 # test if the new PG has PP matching the ID of one of already existing PGs new_l = [l for l in header_extra_pg if l not in header_with_sam][0] pp = [f[3:] for f in new_l.split('\t') if f.startswith('PP:')][0] assert len([l for l in header_extra_pg if l.startswith('#samheader') and ('\tID:{}\t'.format(pp) in l) ]) == 1 def test_merge_pairheaders(): headers = [ ['## pairs format v1.0'], ['## pairs format v1.0'] ] merged_header = _headerops._merge_pairheaders(headers) assert merged_header == headers[0] headers = [ ['## pairs format v1.0', '#a'], ['## pairs format v1.0', '#b'] ] merged_header = _headerops._merge_pairheaders(headers) assert merged_header == ['## pairs format v1.0', '#a', '#b'] headers = [ ['## pairs format v1.0', '#chromsize: chr1 100', '#chromsize: chr2 200'], ['## pairs format v1.0', '#chromsize: chr1 100', '#chromsize: chr2 200'], ] merged_header = _headerops._merge_pairheaders(headers) assert merged_header == headers[0] @raises(Exception) def test_merge_different_pairheaders(): headers = [ ['## pairs format v1.0'], ['## pairs format v1.1'] ] merged_header = _headerops._merge_pairheaders(headers) def test_force_merge_pairheaders(): headers = [ ['## pairs format v1.0', '#chromsize: chr1 100'], ['## pairs format v1.0', '#chromsize: chr2 200'], ] merged_header = _headerops._merge_pairheaders(headers, force=True) assert merged_header == ['## pairs format v1.0', '#chromsize: chr1 100', '#chromsize: chr2 200'] def test_merge_samheaders(): headers = [ ['@HD\tVN:1'], ['@HD\tVN:1'], ] merged_header = _headerops._merge_samheaders(headers) assert merged_header == headers[0] headers = [ ['@HD\tVN:1', '@SQ\tSN:chr1\tLN:100', '@SQ\tSN:chr2\tLN:100', ], ['@HD\tVN:1', '@SQ\tSN:chr1\tLN:100', '@SQ\tSN:chr2\tLN:100', ], ] merged_header = _headerops._merge_samheaders(headers) assert merged_header == headers[0] headers = [ ['@HD\tVN:1', '@PG\tID:bwa\tPN:bwa\tPP:cat', ], ['@HD\tVN:1', '@PG\tID:bwa\tPN:bwa\tPP:cat', ], ] merged_header = _headerops._merge_samheaders(headers) print(merged_header) assert merged_header == [ '@HD\tVN:1', '@PG\tID:bwa-1\tPN:bwa\tPP:cat-1', '@PG\tID:bwa-2\tPN:bwa\tPP:cat-2', ] def test_merge_headers(): headers = [ ['## pairs format v1.0', '#samheader: @HD\tVN:1', '#samheader: @SQ\tSN:chr1\tLN:100', '#samheader: @SQ\tSN:chr2\tLN:100'] ] * 2 merged_header = _headerops.merge_headers(headers) assert merged_header == headers[0] pairtools-0.3.0/tests/test_markasdup.py000066400000000000000000000020331345765554600203120ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys import subprocess from nose.tools import assert_raises testdir = os.path.dirname(os.path.realpath(__file__)) def test_mock_pairsam(): mock_pairsam_path = os.path.join(testdir, 'data', 'mock.pairsam') try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'markasdup', mock_pairsam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e pairsam_body = [l.strip() for l in open(mock_pairsam_path, 'r') if not l.startswith('#') and l.strip()] output_body = [l.strip() for l in result.split('\n') if not l.startswith('#') and l.strip()] # check that all pairsam entries survived sorting: assert len(pairsam_body) == len(output_body) # check that all pairtypes got changed to DD for l in output_body: assert l.split('\t')[7] == 'DD' pairtools-0.3.0/tests/test_merge.py000066400000000000000000000052751345765554600174350ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys import subprocess from nose.tools import assert_raises, with_setup import tempfile testdir = os.path.dirname(os.path.realpath(__file__)) tmpdir = tempfile.TemporaryDirectory() tmpdir_name = tmpdir.name mock_pairsam_path_1 = os.path.join(testdir, 'data', 'mock.pairsam') mock_pairsam_path_2 = os.path.join(testdir, 'data', 'mock.2.pairsam') mock_sorted_pairsam_path_1 = os.path.join(tmpdir_name, '1.pairsam') mock_sorted_pairsam_path_2 = os.path.join(tmpdir_name, '2.pairsam') def setup_func(): try: subprocess.check_output( ['python', '-m', 'pairtools', 'sort', mock_pairsam_path_1, '--output', mock_sorted_pairsam_path_1 ], ) subprocess.check_output( ['python', '-m', 'pairtools', 'sort', mock_pairsam_path_2, '--output', mock_sorted_pairsam_path_2 ], ) except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e def teardown_func(): tmpdir.cleanup() @with_setup(setup_func, teardown_func) def test_mock_pairsam(): try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'merge', mock_sorted_pairsam_path_1, mock_sorted_pairsam_path_2 ], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e # check that all pairsam entries survived sorting: pairsam_body_1 = [l.strip() for l in open(mock_pairsam_path_1, 'r') if not l.startswith('#') and l.strip()] pairsam_body_2 = [l.strip() for l in open(mock_pairsam_path_2, 'r') if not l.startswith('#') and l.strip()] output_body = [l.strip() for l in result.split('\n') if not l.startswith('#') and l.strip()] assert len(pairsam_body_1) + len(pairsam_body_2) == len(output_body) # check the sorting order of the output: prev_pair = None for l in output_body: cur_pair = l.split('\t')[1:8] if prev_pair is not None: assert (cur_pair[0] >= prev_pair[0]) if (cur_pair[0] == prev_pair[0]): assert (cur_pair[1] >= prev_pair[1]) if (cur_pair[1] == prev_pair[1]): assert (cur_pair[2] >= prev_pair[2]) if (cur_pair[2] == prev_pair[2]): assert (cur_pair[3] >= prev_pair[3]) prev_pair = cur_pair pairtools-0.3.0/tests/test_parse.py000066400000000000000000000127571345765554600174530ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys from nose.tools import assert_raises import subprocess testdir = os.path.dirname(os.path.realpath(__file__)) from pairtools import parse, parse_algn, parse_cigar def test_python_version(): assert (sys.version_info[0] == 3), 'Use Python 3!' def test_parse_cigar(): assert (parse_cigar('*') == { 'cigar' : '*', 'read_len': 0, 'matched_bp': 0, 'algn_ref_span': 0, 'algn_read_span': 0, 'clip5_ref': 0, 'clip3_ref': 0}) assert (parse_cigar('50M') == { 'cigar' : '50M', 'read_len': 50, 'matched_bp': 50, 'algn_ref_span': 50, 'algn_read_span': 50, 'clip5_ref': 0, 'clip3_ref': 0}) assert (parse_cigar('40M10S') == { 'cigar' : '40M10S', 'read_len': 50, 'matched_bp': 40, 'algn_ref_span': 40, 'algn_read_span': 40, 'clip5_ref': 0, 'clip3_ref': 10}) assert (parse_cigar('10S40M') == { 'cigar' : '10S40M', 'read_len': 50, 'matched_bp': 40, 'algn_ref_span': 40, 'algn_read_span': 40, 'clip5_ref': 10, 'clip3_ref': 0}) assert (parse_cigar('10S30M10S') == { 'cigar' : '10S30M10S', 'read_len': 50, 'matched_bp': 30, 'algn_ref_span': 30, 'algn_read_span': 30, 'clip5_ref': 10, 'clip3_ref': 10}) assert (parse_cigar('30M10I10M') == { 'cigar' : '30M10I10M', 'read_len': 50, 'matched_bp': 40, 'algn_ref_span': 40, 'algn_read_span': 50, 'clip5_ref': 0, 'clip3_ref': 0}) assert (parse_cigar('30M10D10M10S') == { 'cigar' : '30M10D10M10S', 'read_len': 50, 'matched_bp': 40, 'algn_ref_span': 50, 'algn_read_span': 40, 'clip5_ref': 0, 'clip3_ref': 10}) def test_parse_algn(): min_mapq = 50 sam='SRR1658570.5\t65\tchr12\t24316205\t60\t90M11S\t' '=\t46893391\t22577187\t.\t.\t' 'NM:i:1\tMD:Z:36A53\tAS:i:85\tXS:i:19' samcols = sam.split('\t') parsed_algn = parse_algn(samcols, min_mapq) assert parsed_algn == { 'chrom': 'chr12', 'pos': 24316205, 'pos5': 24316205, 'pos3': 24316294, 'pos': 24316205, 'strand': '+', 'dist_to_5': 0, 'dist_to_3': 11, 'mapq': 60, 'is_unique': True, 'is_mapped': True, 'is_linear': True, 'cigar' : '90M11S', 'algn_ref_span': 90, 'algn_read_span': 90, 'matched_bp': 90, 'clip3_ref': 11, 'clip5_ref': 0, 'read_len': 101, 'type':'U'} sam = ('readid01\t65\tchr1\t10\t60\t50M\tchr1\t200\t0\tSEQ\tPHRED' '\tFLAG1\tFLAG2\tSIMULATED:readid01,chr1,chr1,10,200,+,+,UU') samcols = sam.split('\t') parsed_algn = parse_algn(samcols, min_mapq, True) assert parsed_algn == { 'chrom': 'chr1', 'pos': 59, 'pos5': 10, 'pos3': 59, 'strand': '+', 'dist_to_5': 0, 'dist_to_3': 0, 'mapq': 60, 'is_unique': True, 'is_mapped': True, 'is_linear': True, 'cigar' : '50M', 'algn_ref_span': 50, 'algn_read_span': 50, 'matched_bp': 50, 'clip3_ref': 0, 'clip5_ref': 0, 'read_len': 50, 'type':'U'} sam = ('readid10\t77\t*\t0\t0\t*\t*\t0\t0\tSEQ\tPHRED' '\tFLAG1\tFLAG2\tSIMULATED:readid10,!,!,0,0,-,-,NN') samcols = sam.split('\t') parsed_algn = parse_algn(samcols, min_mapq) assert parsed_algn == { 'chrom': '!', 'pos': 0, 'pos5': 0, 'pos3': 0, 'strand': '-', 'dist_to_5': 0, 'dist_to_3': 0, 'mapq': 0, 'is_unique': False, 'is_mapped': False, 'is_linear': True, 'cigar' : '*', 'algn_ref_span': 0, 'algn_read_span': 0, 'matched_bp': 0, 'clip3_ref': 0, 'clip5_ref': 0, 'read_len': 0, 'type':'N'} def test_mock_sam(): mock_sam_path = os.path.join(testdir, 'data', 'mock.sam') mock_chroms_path = os.path.join(testdir, 'data', 'mock.chrom.sizes') try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'parse', '--walks-policy', 'mask', '-c', mock_chroms_path, mock_sam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e # check if the header got transferred correctly sam_header = [l.strip() for l in open(mock_sam_path, 'r') if l.startswith('@')] pairsam_header = [l.strip() for l in result.split('\n') if l.startswith('#')] for l in sam_header: assert any([l in l2 for l2 in pairsam_header]) # check that the pairs got assigned properly for l in result.split('\n'): if l.startswith('#') or not l: continue assigned_pair = l.split('\t')[1:8] simulated_pair = l.split('SIMULATED:',1)[1].split('\031',1)[0].split(',') print(assigned_pair) print(simulated_pair) print() assert assigned_pair == simulated_pair pairtools-0.3.0/tests/test_select.py000066400000000000000000000131661345765554600176130ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys import subprocess from nose.tools import assert_raises testdir = os.path.dirname(os.path.realpath(__file__)) mock_pairsam_path = os.path.join(testdir, 'data', 'mock.pairsam') mock_chromsizes_path = os.path.join(testdir, 'data', 'mock.chrom.sizes') def test_preserve(): try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'select', 'True', mock_pairsam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e pairsam_body = [l.strip() for l in open(mock_pairsam_path, 'r') if not l.startswith('#') and l.strip()] output_body = [l.strip() for l in result.split('\n') if not l.startswith('#') and l.strip()] assert all(l in pairsam_body for l in output_body) def test_equal(): try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'select', '(pair_type == "RU") or (pair_type == "UR") or (pair_type == "UU")', mock_pairsam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e print(result) pairsam_body = [l.strip() for l in open(mock_pairsam_path, 'r') if not l.startswith('#') and l.strip()] output_body = [l.strip() for l in result.split('\n') if not l.startswith('#') and l.strip()] assert all(l.split('\t')[7] in ['RU', 'UR', 'UU'] for l in output_body) assert all(l in output_body for l in pairsam_body if l.split('\t')[7] in ['RU', 'UR', 'UU']) def test_csv(): try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'select', 'csv_match(pair_type, "RU,UR,UU")', mock_pairsam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e print(result) pairsam_body = [l.strip() for l in open(mock_pairsam_path, 'r') if not l.startswith('#') and l.strip()] output_body = [l.strip() for l in result.split('\n') if not l.startswith('#') and l.strip()] assert all(l.split('\t')[7] in ['RU','UR', 'UU'] for l in output_body) assert all(l in output_body for l in pairsam_body if l.split('\t')[7] in ['RU', 'UR', 'UU']) def test_wildcard(): try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'select', 'wildcard_match(pair_type, "*U")', mock_pairsam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e print(result) pairsam_body = [l.strip() for l in open(mock_pairsam_path, 'r') if not l.startswith('#') and l.strip()] output_body = [l.strip() for l in result.split('\n') if not l.startswith('#') and l.strip()] assert all(l.split('\t')[7] in ['NU', 'MU', 'RU', 'UU'] for l in output_body) assert all(l in output_body for l in pairsam_body if l.split('\t')[7] in ['NU', 'MU', 'RU', 'UU']) def test_regex(): try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'select', 'regex_match(pair_type, "[NM]U")', mock_pairsam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e print(result) pairsam_body = [l.strip() for l in open(mock_pairsam_path, 'r') if not l.startswith('#') and l.strip()] output_body = [l.strip() for l in result.split('\n') if not l.startswith('#') and l.strip()] assert all(l.split('\t')[7] in ['NU', 'MU'] for l in output_body) assert all(l in output_body for l in pairsam_body if l.split('\t')[7] in ['NU', 'MU']) def test_chrom_subset(): try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'select', 'True', '--chrom-subset', mock_chromsizes_path, mock_pairsam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e pairsam_body = [l.strip() for l in open(mock_pairsam_path, 'r') if not l.startswith('#') and l.strip()] output_body = [l.strip() for l in result.split('\n') if not l.startswith('#') and l.strip()] output_header = [l.strip() for l in result.split('\n') if l.startswith('#') and l.strip()] chroms_from_chrom_field = [l.strip().split()[1:] for l in result.split('\n') if l.startswith('#chromosomes:')][0] assert set(chroms_from_chrom_field) == set(['chr1', 'chr2']) chroms_from_chrom_sizes = [l.strip().split()[1] for l in result.split('\n') if l.startswith('#chromsize:')] assert set(chroms_from_chrom_sizes) == set(['chr1', 'chr2']) pairtools-0.3.0/tests/test_sort.py000066400000000000000000000040751345765554600173220ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys import subprocess from nose.tools import assert_raises testdir = os.path.dirname(os.path.realpath(__file__)) def test_mock_pairsam(): mock_pairsam_path = os.path.join(testdir, 'data', 'mock.pairsam') try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'sort', mock_pairsam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e # Check that the only changes strings are a @PG record of a SAM header, # the "#sorted" entry and chromosomes pairsam_header = [l.strip() for l in open(mock_pairsam_path, 'r') if l.startswith('#')] output_header = [l.strip() for l in result.split('\n') if l.startswith('#')] print(output_header) print(pairsam_header) for l in output_header: if not any([l in l2 for l2 in pairsam_header]): assert ( l.startswith('#samheader: @PG') or l.startswith('#sorted') or l.startswith('#chromosomes') ) pairsam_body = [l.strip() for l in open(mock_pairsam_path, 'r') if not l.startswith('#') and l.strip()] output_body = [l.strip() for l in result.split('\n') if not l.startswith('#') and l.strip()] # check that all pairsam entries survived sorting: assert len(pairsam_body) == len(output_body) # check the sorting order of the output: prev_pair = None for l in output_body: cur_pair = l.split('\t')[1:8] if prev_pair is not None: assert (cur_pair[0] >= prev_pair[0]) if (cur_pair[0] == prev_pair[0]): assert (cur_pair[2] >= prev_pair[2]) if (cur_pair[2] == prev_pair[2]): assert (cur_pair[1] >= prev_pair[1]) if (cur_pair[1] == prev_pair[1]): assert (cur_pair[3] >= prev_pair[3]) prev_pair = cur_pair pairtools-0.3.0/tests/test_split.py000066400000000000000000000052421345765554600174630ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys import subprocess from nose.tools import assert_raises, with_setup import tempfile testdir = os.path.dirname(os.path.realpath(__file__)) mock_pairsam_path = os.path.join(testdir, 'data', 'mock.pairsam') tmpdir = tempfile.TemporaryDirectory() tmpdir_name = tmpdir.name pairs_path = os.path.join(tmpdir_name, 'out.pairs') sam_path = os.path.join(tmpdir_name, 'out.sam') def setup_func(): try: subprocess.check_output( ['python', '-m', 'pairtools', 'split', mock_pairsam_path, '--output-pairs', pairs_path, '--output-sam', sam_path, ], ) except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e def teardown_func(): tmpdir.cleanup() @with_setup(setup_func, teardown_func) def test_split(): pairsam_lines = [l.strip() for l in open(mock_pairsam_path, 'r') if l.strip()] pairs_lines = [l.strip() for l in open(pairs_path, 'r') if l.strip()] sam_lines = [l.strip() for l in open(sam_path, 'r') if l.strip()] # check that all entries survived splitting: n_pairsam = len([l for l in pairsam_lines if not l.startswith('#')]) n_pairs = len([l for l in pairs_lines if not l.startswith('#')]) n_sam = len([l for l in sam_lines if not l.startswith('@')]) // 2 assert n_pairsam == n_pairs assert n_pairsam == n_sam # check that the header survived splitting: pairsam_header = [l.strip() for l in open(mock_pairsam_path, 'r') if l.strip() and l.startswith('#')] pairs_header = [l.strip() for l in open(pairs_path, 'r') if l.strip() and l.startswith('#')] sam_header = [l.strip() for l in open(sam_path, 'r') if l.strip() and l.startswith('@')] assert all( any(l in l2 for l2 in pairsam_header) for l in sam_header if not l.startswith('@PG')) assert all( l in pairsam_header for l in pairs_header if (not (l.startswith('#columns') or l.startswith('#samheader')))) columns_pairsam = [l for l in pairsam_header if l.startswith('#columns')][0].split()[1:] columns_pairs = [l for l in pairs_header if l.startswith('#columns')][0].split()[1:] assert ( ('sam1' in columns_pairsam) and ('sam2' in columns_pairsam) and ('sam1' not in columns_pairs) and ('sam2' not in columns_pairs)) assert [c for c in columns_pairsam if c != 'sam1' and c != 'sam2'] == columns_pairs pairtools-0.3.0/tests/test_stats.py000066400000000000000000000032351345765554600174660ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import sys import subprocess from nose.tools import assert_raises testdir = os.path.dirname(os.path.realpath(__file__)) def test_mock_pairsam(): mock_pairsam_path = os.path.join(testdir, 'data', 'mock.pairsam') try: result = subprocess.check_output( ['python', '-m', 'pairtools', 'stats', mock_pairsam_path], ).decode('ascii') except subprocess.CalledProcessError as e: print(e.output) print(sys.exc_info()) raise e stats = dict(l.strip().split('\t') for l in result.split('\n') if not l.startswith('#') and l.strip()) for k in stats: stats[k] = int(stats[k]) print(stats) assert stats['total'] == 8 assert stats['total_single_sided_mapped'] == 2 assert stats['total_mapped'] == 5 assert stats['cis'] == 3 assert stats['trans'] == 2 assert stats['pair_types/UU'] == 4 assert stats['pair_types/NU'] == 1 assert stats['pair_types/WW'] == 1 assert stats['pair_types/UR'] == 1 assert stats['pair_types/MU'] == 1 assert stats['chrom_freq/chr1/chr2'] == 1 assert stats['chrom_freq/chr1/chr1'] == 3 assert stats['chrom_freq/chr2/chr3'] == 1 assert all(stats[k]==0 for k in stats if k.startswith('dist_freq') and k not in ['dist_freq/1-2/++', 'dist_freq/2-3/++', 'dist_freq/32-56/++']) assert stats['dist_freq/1-2/++'] == 1 assert stats['dist_freq/2-3/++'] == 1 assert stats['dist_freq/32-56/++'] == 1