├── AUTHORS ├── tests ├── data │ ├── nucmer_test_out.coords │ ├── coords_file_test_convert_to_msp_crunch.qry.fa.fai │ ├── coords_file_test_convert_to_msp_crunch.ref.fa.fai │ ├── nucmer_test_out.coords.snps │ ├── nucmer_test_write_script_no_snps.sh │ ├── snp_file_test_no_header.snps │ ├── coords_file_test_convert_to_msp_crunch.no_offset.crunch │ ├── coords_file_test_convert_to_msp_crunch.with_offset.crunch │ ├── nucmer_test_write_script_with_snps.sh │ ├── coords_file_test_no_header.coords │ ├── snp_file_test_with_header.snps │ ├── snp_file_test_get_all_variants.snps │ ├── coords_file_test_with_header.coords │ ├── coords_file_test_convert_to_msp_crunch.coords │ ├── nucmer_test_qry.fa │ ├── nucmer_test_ref.fa │ ├── coords_file_test_convert_to_msp_crunch.qry.fa │ └── coords_file_test_convert_to_msp_crunch.ref.fa ├── syscall_test.py ├── snp_test.py ├── coords_file_test.py ├── nucmer_test.py ├── snp_file_test.py ├── variant_test.py └── alignment_test.py ├── environment.yml ├── env_osx64.yml ├── .travis.yml ├── src └── pymummer │ ├── snp_file.py │ ├── __init__.py │ ├── syscall.py │ ├── snp.py │ ├── coords_file.py │ ├── variant.py │ ├── nucmer.py │ └── alignment.py ├── .gitignore ├── scripts └── install_dependencies.sh ├── pyproject.toml ├── README.md ├── CHANGELOG.md └── LICENSE /AUTHORS: -------------------------------------------------------------------------------- 1 | Martin Hunt (path-help@sanger.ac.uk) 2 | Nishadi De Silva (path-help@sanger.ac.uk) 3 | -------------------------------------------------------------------------------- /tests/data/nucmer_test_out.coords: -------------------------------------------------------------------------------- 1 | 61 900 1 840 840 840 99.76 1000 840 1 1 test_ref test_qry [CONTAINS] 2 | -------------------------------------------------------------------------------- /tests/data/coords_file_test_convert_to_msp_crunch.qry.fa.fai: -------------------------------------------------------------------------------- 1 | qry1 420 6 60 61 2 | qry2 479 439 60 61 3 | qry3 360 932 60 61 4 | -------------------------------------------------------------------------------- /tests/data/coords_file_test_convert_to_msp_crunch.ref.fa.fai: -------------------------------------------------------------------------------- 1 | ref1 420 6 60 61 2 | ref2 480 439 60 61 3 | ref3 500 933 60 61 4 | -------------------------------------------------------------------------------- /tests/data/nucmer_test_out.coords.snps: -------------------------------------------------------------------------------- 1 | 436 C . 375 2 375 1000 840 1 1 test_ref test_qry 2 | 438 . G 378 2 378 1000 840 1 1 test_ref test_qry 3 | -------------------------------------------------------------------------------- /tests/data/nucmer_test_write_script_no_snps.sh: -------------------------------------------------------------------------------- 1 | nucmer -p p ref qry 2 | delta-filter p.delta > p.delta.filter 3 | show-coords -dTlro p.delta.filter > outfile 4 | -------------------------------------------------------------------------------- /tests/data/snp_file_test_no_header.snps: -------------------------------------------------------------------------------- 1 | 133 G . 122 1 122 500 489 1 1 ref qry 2 | 143 . C 131 1 132 500 489 1 1 ref qry 3 | 253 T A 242 120 242 500 489 1 1 ref qry 4 | -------------------------------------------------------------------------------- /tests/data/coords_file_test_convert_to_msp_crunch.no_offset.crunch: -------------------------------------------------------------------------------- 1 | 418 99.76 1 420 qry1 1 420 ref1 2 | 477 99.58 1 479 qry2 1 480 ref2 3 | 357 99.44 1 360 qry3 61 420 ref3 4 | -------------------------------------------------------------------------------- /tests/data/coords_file_test_convert_to_msp_crunch.with_offset.crunch: -------------------------------------------------------------------------------- 1 | 418 99.76 1 420 qry1 1 420 ref1 2 | 477 99.58 421 899 qry2 421 900 ref2 3 | 357 99.44 900 1259 qry3 961 1320 ref3 4 | -------------------------------------------------------------------------------- /tests/data/nucmer_test_write_script_with_snps.sh: -------------------------------------------------------------------------------- 1 | nucmer -p p ref qry 2 | delta-filter p.delta > p.delta.filter 3 | show-coords -dTlro p.delta.filter > outfile 4 | show-snps -TClr p.delta.filter > outfile.snps 5 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: pymummer 2 | channels: 3 | - conda-forge 4 | - bioconda 5 | dependencies: 6 | - python 3.12 7 | - bioconda::mummer4 8 | - "pyfastaq >= 3.10.0" 9 | - pip 10 | - pip: 11 | - hatch 12 | -------------------------------------------------------------------------------- /env_osx64.yml: -------------------------------------------------------------------------------- 1 | name: pymummer-osx64 2 | channels: 3 | - conda-forge/osx-64 4 | - bioconda/osx-64 5 | dependencies: 6 | - python 3.12 7 | - bioconda::mummer4 8 | - "pyfastaq >= 3.10.0" 9 | - pip 10 | - pip: 11 | - hatch 12 | -------------------------------------------------------------------------------- /tests/data/coords_file_test_no_header.coords: -------------------------------------------------------------------------------- 1 | 61 900 1 840 840 840 99.76 1000 840 1 1 test_ref1 test_qry1 [CONTAINS] 2 | 62 901 2 841 841 850 99.66 999 839 1 1 test_ref2 test_qry2 [CONTAINS] 3 | 63 902 3 842 842 860 99.56 998 838 1 1 test_ref3 test_qry3 [CONTAINS] 4 | -------------------------------------------------------------------------------- /tests/data/snp_file_test_with_header.snps: -------------------------------------------------------------------------------- 1 | /path/to/ref.fa /path/to/qry.fa 2 | NUCMER 3 | 4 | [P1] [SUB] [SUB] [P2] [BUFF] [DIST] [LEN R] [LEN Q] [FRM] [TAGS] 5 | 133 G . 122 1 122 500 489 1 1 ref qry 6 | 143 . C 131 1 132 500 489 1 1 ref qry 7 | 253 T A 242 120 242 500 489 1 1 ref qry 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | addons: 3 | apt: 4 | packages: 5 | - g++ 6 | - python-dev 7 | cache: 8 | directories: 9 | - "build" 10 | - "$HOME/.cache/pip" 11 | python: 12 | - "3.7" 13 | - "3.12" 14 | sudo: false 15 | install: 16 | - "source ./scripts/install_dependencies.sh" 17 | script: "pip install . && pytest tests" 18 | -------------------------------------------------------------------------------- /tests/data/snp_file_test_get_all_variants.snps: -------------------------------------------------------------------------------- 1 | 125 T . 124 1 124 500 497 1 1 ref1 qry1 2 | 126 A . 124 1 124 500 497 1 1 ref1 qry1 3 | 127 C . 124 1 124 500 497 1 1 ref1 qry1 4 | 386 C T 383 115 115 500 497 1 1 ref1 qry1 5 | 479 . G 480 0 22 500 504 1 1 ref2 qry2 6 | 479 . A 481 0 22 500 504 1 1 ref2 qry2 7 | 479 . T 482 0 22 500 504 1 1 ref2 qry2 8 | 479 . A 483 0 22 500 504 1 1 ref2 qry2 9 | -------------------------------------------------------------------------------- /tests/data/coords_file_test_with_header.coords: -------------------------------------------------------------------------------- 1 | /path/to/ref.fa /path/to/query.fa 2 | NUCMER 3 | 4 | [S1] [E1] [S2] [E2] [LEN 1] [LEN 2] [% IDY] [LEN R] [LEN Q] [FRM] [TAGS] 5 | 61 900 1 840 840 840 99.76 1000 840 1 1 test_ref1 test_qry1 [CONTAINS] 6 | 62 901 2 841 841 850 99.66 999 839 1 1 test_ref2 test_qry2 [CONTAINS] 7 | 63 902 3 842 842 860 99.56 998 838 1 1 test_ref3 test_qry3 [CONTAINS] 8 | -------------------------------------------------------------------------------- /tests/syscall_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from pymummer import syscall 4 | 5 | 6 | class TestSyscall(unittest.TestCase): 7 | def test_run_fail(self): 8 | """Test that run raises error when command fails""" 9 | with self.assertRaises(syscall.Error): 10 | syscall.run("notacommandandthrowerror") 11 | 12 | def test_run_ok(self): 13 | """Test run is ok on command that works""" 14 | syscall.run("ls") 15 | -------------------------------------------------------------------------------- /tests/data/coords_file_test_convert_to_msp_crunch.coords: -------------------------------------------------------------------------------- 1 | /Users/mh12/sanger-pathogens/pymummer/pymummer/tests/data/coords_file_test_convert_to_msp_crunch.ref.fa /Users/mh12/sanger-pathogens/pymummer/pymummer/tests/data/coords_file_test_convert_to_msp_crunch.qry.fa 2 | NUCMER 3 | 4 | [S1] [E1] [S2] [E2] [LEN 1] [LEN 2] [% IDY] [LEN R] [LEN Q] [FRM] [TAGS] 5 | 1 420 1 420 420 420 99.76 420 420 1 1 ref1 qry1 [IDENTITY] 6 | 1 480 1 479 480 479 99.58 480 479 1 1 ref2 qry2 [IDENTITY] 7 | 61 420 1 360 360 360 99.44 500 360 1 1 ref3 qry3 [CONTAINS] 8 | -------------------------------------------------------------------------------- /src/pymummer/snp_file.py: -------------------------------------------------------------------------------- 1 | import pyfastaq 2 | from pymummer import snp, variant 3 | 4 | 5 | def reader(fname): 6 | f = pyfastaq.utils.open_file_read(fname) 7 | 8 | for line in f: 9 | if line.startswith("[") or (not "\t" in line): 10 | continue 11 | 12 | yield snp.Snp(line) 13 | 14 | pyfastaq.utils.close(f) 15 | 16 | 17 | def get_all_variants(fname): 18 | variants = [] 19 | fr = reader(fname) 20 | for nucmer_snp in fr: 21 | if len(variants) == 0 or not variants[-1].update_indel(nucmer_snp): 22 | variants.append(variant.Variant(nucmer_snp)) 23 | 24 | return variants 25 | -------------------------------------------------------------------------------- /src/pymummer/__init__.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import sys 3 | 4 | from pymummer._version import __version__ 5 | 6 | required_progs = ["nucmer", "show-coords", "show-snps", "delta-filter"] 7 | missing_progs = [prog for prog in required_progs if shutil.which(prog) is None] 8 | 9 | if missing_progs: 10 | print( 11 | "The following required programs from the MUMmer package are not found in your PATH:", 12 | file=sys.stderr, 13 | ) 14 | for prog in missing_progs: 15 | print(f" NOT FOUND: {prog}", file=sys.stderr) 16 | raise ImportError( 17 | "Some required MUMmer programs are missing. Please install them and ensure they are in your PATH." 18 | ) 19 | 20 | __all__ = [ 21 | "alignment", 22 | "coords_file", 23 | "nucmer", 24 | "snp", 25 | "snp_file", 26 | "syscall", 27 | "variant", 28 | ] 29 | 30 | from pymummer import * 31 | -------------------------------------------------------------------------------- /tests/data/nucmer_test_qry.fa: -------------------------------------------------------------------------------- 1 | >test_qry 2 | GTAATCAAATAATCCACCGGATGAGGTATTTTCTCATCGGGGTGACTCTAACCACATCGT 3 | GTTTCTTCCTGAAGGCTCTAAGATGTCATGAGAACTCTTACTGTCTAGCTGAGGGGCTTG 4 | TGCACAACACTAGGATTGTGTCTTATGCTCTATTGGACAGCGAAAACTGCTGAAATTAAC 5 | GGGCCGTAACATACTATATTCTTCAAACCGAATTAACGTTCAGCCCCCGCTTGATTGCGA 6 | AATTAACTGGAATGCAACACCTTGCACTGGCCGTCCTGCGGTGGTGACCCTTTGAGGTAA 7 | ACACGTCGTCGACGCATTACAGTTGGGAGAAGCACACTCATGTTTCTAATAAAGCGCTCA 8 | CAGACGCGACCACTATAGCTCTAAAATACATCCCTCTAAGGTTCCATCTAGAAAGTGGCC 9 | CCCGCGACCGTCTACCGTGGTGGATGCAGGGAGTCACCTACGCGTCTTTTCGTGCTACCT 10 | AGGCATTTTTGCACTACCTAACTCCGTATTAAGGCCTTCGGAGAGGGCCGTCCCACTTCA 11 | ATGTGTGTGGTGGACTGTCCTCATGGGAAAAGCAAGTGTTTGACCGGTTGACACTAGTCC 12 | CGTTTATCTTCATGGGCGGGAGCGCGCATTCGTGACGGGGACACTTCTCGCCGTTTAGCC 13 | GGTGAGCTTATTAGGCCGATGGCGGGCCACCCTGATACGGGGGCCTATATGTCCACGAAT 14 | ATCAATTTCTGTATAACATTGGGCGCAGAAAACAGACTGGTCCAAATAAGATGAATCTAT 15 | AGCCCAGTGTGTTGCCTAAACGCTGAGCGAATAATTGGTCGCGCTCGGCGAGACCAGGGA 16 | -------------------------------------------------------------------------------- /src/pymummer/syscall.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | 5 | 6 | class Error(Exception): 7 | pass 8 | 9 | 10 | def decode(x): 11 | try: 12 | s = x.decode() 13 | except: 14 | return x 15 | return s 16 | 17 | 18 | def run(cmd, verbose=False): 19 | if verbose: 20 | print("Running command:", cmd, flush=True) 21 | try: 22 | output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) 23 | except subprocess.CalledProcessError as error: 24 | print( 25 | "The following command failed with exit code", 26 | error.returncode, 27 | file=sys.stderr, 28 | ) 29 | print(cmd, file=sys.stderr) 30 | print("\nThe output was:\n", file=sys.stderr) 31 | print(error.output.decode(), file=sys.stderr) 32 | raise Error("Error running command:", cmd) 33 | 34 | if verbose: 35 | print(decode(output)) 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac stuff 2 | .DS_Store 3 | 4 | # Versioning 5 | src/pymummer/_version.py 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | -------------------------------------------------------------------------------- /tests/data/nucmer_test_ref.fa: -------------------------------------------------------------------------------- 1 | >test_ref 2 | TCAAGATCGTGCCCCGTTGATATCGCTGTTGCACAGGACTTTCTCCACCCTGATACCGCA 3 | GTAATCAAATAATCCACCGGATGAGGTATTTTCTCATCGGGGTGACTCTAACCACATCGT 4 | GTTTCTTCCTGAAGGCTCTAAGATGTCATGAGAACTCTTACTGTCTAGCTGAGGGGCTTG 5 | TGCACAACACTAGGATTGTGTCTTATGCTCTATTGGACAGCGAAAACTGCTGAAATTAAC 6 | GGGCCGTAACATACTATATTCTTCAAACCGAATTAACGTTCAGCCCCCGCTTGATTGCGA 7 | AATTAACTGGAATGCAACACCTTGCACTGGCCGTCCTGCGGTGGTGACCCTTTGAGGTAA 8 | ACACGTCGTCGACGCATTACAGTTGGGAGAAGCACACTCATGTTTCTAATAAAGCGCTCA 9 | CAGACGCGACCACTACTACTCTAAAATACATCCCTCTAAGGTTCCATCTAGAAAGTGGCC 10 | CCCGCGACCGTCTACCGTGGTGGATGCAGGGAGTCACCTACGCGTCTTTTCGTGCTACCT 11 | AGGCATTTTTGCACTACCTAACTCCGTATTAAGGCCTTCGGAGAGGGCCGTCCCACTTCA 12 | ATGTGTGTGGTGGACTGTCCTCATGGGAAAAGCAAGTGTTTGACCGGTTGACACTAGTCC 13 | CGTTTATCTTCATGGGCGGGAGCGCGCATTCGTGACGGGGACACTTCTCGCCGTTTAGCC 14 | GGTGAGCTTATTAGGCCGATGGCGGGCCACCCTGATACGGGGGCCTATATGTCCACGAAT 15 | ATCAATTTCTGTATAACATTGGGCGCAGAAAACAGACTGGTCCAAATAAGATGAATCTAT 16 | AGCCCAGTGTGTTGCCTAAACGCTGAGCGAATAATTGGTCGCGCTCGGCGAGACCAGGGA 17 | TTCTGGCAAGATCCTAACTCGGCCGTCAATGATGTTAAATCAACTCGGTGTGGCCCCAGC 18 | TTCGACACCAAAAGTTGTGATCACTACATAGCGTATCTCG 19 | -------------------------------------------------------------------------------- /tests/data/coords_file_test_convert_to_msp_crunch.qry.fa: -------------------------------------------------------------------------------- 1 | >qry1 2 | AACTTTCATACTGTAGTTATTGGCATCGTTGTAAAATTGCTCACCACGTCGCTCGTTCAT 3 | GATAATCTAAAGATCGCTAACGATTACTACCGGCAGCGGTTTATACGGACCAATCGTTGG 4 | TTCATTTTAATGTAGACCCAGAAATCGACTGCATTTCTGCAGCCCGGCCCGACGTGTTGA 5 | ACGGATGATCTTAGACAGAACGTCCTTGGGTCTATATCCGGCCACATATTTATTGGGCAG 6 | GGGAGTCAATTGGGGGCGTACCGAAATATCGTCTTTACGAGCGTCGGTGACGCACATGAC 7 | ATGGTCGACCCATAGCCTCAGCTTCTAGACGGTTGCACCAGCGCAAGACAAAACTCTCAA 8 | TTTTGTCTGGGTACCGAGATTGCGGAACCGGGGATATTGTAGAGCGGTGCACACGGCCTT 9 | >qry2 10 | CTAGCGCAAGACCGACTCTGATTCATGGAGACAGGGCCAGACAGGGAAACGAGATTGAGC 11 | GATGCTGTCATTTTCGTAACGAGGATTGGTCGGGGACCGAGATCGTACACGTCTCCGAGC 12 | CCCCACAGTCGAGTACAAATGGCTTAATTTACTGACTTCTTCCTGTTACCGGCATGGTAT 13 | GCTGAGCCTGGCCCGCTCACTATTGGATATAGCCTGTGCGCTGGCGTACCGCTGTTCTAC 14 | CGGTTCCTCTTGAGGGTCAAAGGCCGGCTACCATCGTTAACTTATTAGCTTAGAGTAATG 15 | TAGGTTACGTGACGCTGGCCGGTTAGCGTTTCGAAGGATCGCAGGACTATAGTCAAAACT 16 | CGTGGACTTCTACCAGAACTATCGATGTTCACGATGACTACGTTCCTTCCGAATATTACA 17 | GTAAGGGATAGTCATGCCGGTTTAACATCATCTGTGTGTACGCAATGCAGTTTGGCACA 18 | >qry3 19 | AGGTGCGACAGGATCTAACACCTGTACAGTAAGAAAGGGGCATATGATCGACCCCGGTTG 20 | CTCGTATGATAATCCCATTATTGTTATCTGAGGATCGTTATGCGGCAGTTCTAGTCCGAT 21 | AAAAGTTAGGTGAGTTGTGTTGGTAATCCTTCTCTAGGAGGCCTGGCGACTCCACTGAGC 22 | CCAGCGATGGGAGAGCTGGTCCCCCCAATATCGTGACTGAATTGGTAAGGTAGATATCTC 23 | CCAGATAGCCGCATACCGTCTGGCACCGTCGACCGAAAGAAATGTTCGCCTTGGCATGCT 24 | CAGATTGCATCTATACTTACTTGTATAGAACTGCCCGCGCCACCCAGAAGACAAACTAAT 25 | -------------------------------------------------------------------------------- /tests/snp_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from pymummer import snp 4 | 5 | 6 | data_dir = "tests/data" 7 | 8 | 9 | class TestSnp(unittest.TestCase): 10 | def test_str_no_c_option(self): 11 | """Test __str__ with format with no -C option""" 12 | l_in = [ 13 | "187", 14 | "A", 15 | "C", 16 | "269", 17 | "187", 18 | "187", 19 | "654", 20 | "853", 21 | "1", 22 | "1", 23 | "ref_name", 24 | "qry_name", 25 | ] 26 | s = snp.Snp("\t".join(l_in)) 27 | expected = "\t".join( 28 | ["187", "A", "C", "269", "654", "853", "1", "ref_name", "qry_name"] 29 | ) 30 | self.assertEqual(str(s), expected) 31 | 32 | def test_str_with_c_option(self): 33 | """Test __str__ with format with -C option""" 34 | l_in = [ 35 | "187", 36 | "A", 37 | "C", 38 | "269", 39 | "187", 40 | "187", 41 | "0", 42 | "0", 43 | "654", 44 | "853", 45 | "1", 46 | "-1", 47 | "ref_name", 48 | "qry_name", 49 | ] 50 | s = snp.Snp("\t".join(l_in)) 51 | expected = "\t".join( 52 | ["187", "A", "C", "269", "654", "853", "-1", "ref_name", "qry_name"] 53 | ) 54 | self.assertEqual(str(s), expected) 55 | -------------------------------------------------------------------------------- /scripts/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | set -e 5 | 6 | start_dir=$(pwd) 7 | 8 | MUMMER_VERSION="3.23" 9 | 10 | MUMMER_DOWNLOAD_URL="http://sourceforge.net/projects/mummer/files/mummer/${MUMMER_VERSION}/MUMmer${MUMMER_VERSION}.tar.gz/download" 11 | 12 | # Make an install location 13 | if [ ! -d 'build' ]; then 14 | mkdir build 15 | fi 16 | cd build 17 | build_dir=$(pwd) 18 | 19 | # DOWNLOAD ALL THE THINGS 20 | download () { 21 | url=$1 22 | download_location=$2 23 | 24 | if [ -e $download_location ]; then 25 | echo "Skipping download of $url, $download_location already exists" 26 | else 27 | echo "Downloading $url to $download_location" 28 | wget $url -O $download_location 29 | fi 30 | } 31 | 32 | download $MUMMER_DOWNLOAD_URL "mummer-${MUMMER_VERSION}.tgz" 33 | 34 | # Build all the things 35 | cd $build_dir 36 | 37 | ## Mummer 38 | mummer_dir=$(pwd)/MUMmer${MUMMER_VERSION} 39 | if [ ! -d $mummer_dir ]; then 40 | tar xzf mummer-${MUMMER_VERSION}.tgz 41 | fi 42 | cd $mummer_dir 43 | if [ -e "${mummer_dir}/mummer" ]; then 44 | echo "Already built Mummer; skipping build" 45 | else 46 | make check 47 | make 48 | cd ./src/tigr 49 | make 50 | fi 51 | 52 | cd $build_dir 53 | 54 | # Setup environment variables 55 | update_path () { 56 | new_dir=$1 57 | export PATH=${PATH:-$new_dir} 58 | if [[ ! "$PATH" =~ (^|:)"${new_dir}"(:|$) ]]; then 59 | export PATH=${new_dir}:${PATH} 60 | fi 61 | } 62 | 63 | update_path ${mummer_dir} 64 | update_path ${mummer_dir}/src/tigr 65 | 66 | cd $start_dir 67 | 68 | set +x 69 | set +e 70 | -------------------------------------------------------------------------------- /tests/data/coords_file_test_convert_to_msp_crunch.ref.fa: -------------------------------------------------------------------------------- 1 | >ref1 2 | AACTTTCATACTGTAGTTATTGGCATCGTTGTAAAATTGCTCACCACGTCGCTCGTTCAT 3 | GATAATCTAAAGATCGCTAACGATTACTACCGGCAGCGGTTTATACGGACCAATCGTTGG 4 | TTCATTTTAATGTAGACCCAGAAATCGACTGCATTTCTGCAGCCCGGCCCGACGTGTTGA 5 | ACGGATGATCTTAGACAGAACGTCCTTGGGTCTATATCCGGCCACATATTTATTGGGCAG 6 | GGGTGTCAATTGGGGGCGTACCGAAATATCGTCTTTACGAGCGTCGGTGACGCACATGAC 7 | ATGGTCGACCCATAGCCTCAGCTTCTAGACGGTTGCACCAGCGCAAGACAAAACTCTCAA 8 | TTTTGTCTGGGTACCGAGATTGCGGAACCGGGGATATTGTAGAGCGGTGCACACGGCCTT 9 | >ref2 10 | CTAGCGCAAGACCGACTCTGATTCATGGAGACAGGGCCAGACAGGGAAACGAGATTGAGC 11 | GATGCTGTCATTTTCGTAACGAGGATTGGTCGGGGACCGAGATCGTACACGTCTCCGAGC 12 | CCCCACAGTCGAGTACAAATGGCTTAATTTACTGACTTCTTCCTGTTACCGGCATGGTAT 13 | GCTGAGCCTGGCCCGCTCACTATTGGATATAGCCTGTGCGCTGGCGTACCGCTGTTCTAC 14 | CGGTACCTCTTGAGGGTCAAAGGCCGGCTACCATCGTTAACTTATTAGCTTAGAGTAATG 15 | TAGGTTACGTGACGCTGGCCGGTTAGCGTTTCGAAGGATCGCAGGACTATAGTCAAAACT 16 | CGTGGACTTCTACCAGAACTATCGATGTTCACGATGACTACGTTCCTTCCGAATATTACA 17 | GTATAGGGATAGTCATGCCGGTTTAACATCATCTGTGTGTACGCAATGCAGTTTGGCACA 18 | >ref3 19 | ATTCAACGGGTAGGGTCATCAGATTTTTAGTACGAACGAACAATTCCCCATTCAATTCCG 20 | AGGTGCGACAGGATCTAACACCTGTACAGTAAGAAAGGGGCATATGATCGACCCCGGTTG 21 | CTCGTATGATAATCCCATTATTGTTATCTGAGGATCGTTATGCGGCAGTTCTAGTCCGAT 22 | CCAAGTTAGGTGAGTTGTGTTGGTAATCCTTCTCTAGGAGGCCTGGCGACTCCACTGAGC 23 | CCAGCGATGGGAGAGCTGGTCCCCCCAATATCGTGACTGAATTGGTAAGGTAGATATCTC 24 | CCAGATAGCCGCATACCGTCTGGCACCGTCGACCGAAAGAAATGTTCGCCTTGGCATGCT 25 | CAGATTGCATCTATACTTACTTGTATAGAACTGCCCGCGCCACCCAGAAGACAAACTAAT 26 | TTATTGTCGCTCAAACCTGTTTAGTTAATTCACCTTTGTAACCAGCTTACCCTCAATTGC 27 | GTATGTAACTCCTTGGCTGC 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Build system configuration 2 | [build-system] 3 | requires = ["hatchling", "hatch-vcs"] 4 | build-backend = "hatchling.build" 5 | 6 | # Project metadata 7 | [project] 8 | name = "pymummer" 9 | description = "Wrapper for MUMmer" 10 | readme="README.md" 11 | requires-python = ">=3.8" 12 | license = { text = "GPLv3" } 13 | authors = [ 14 | { name = "Martin Hunt", email = "path-help@sanger.ac.uk" }, 15 | { name = "Nishadi De Silva", email = "path-help@sanger.ac.uk" } 16 | ] 17 | 18 | # The classifiers for the project. 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "Topic :: Scientific/Engineering :: Bio-Informatics", 22 | "Programming Language :: Python :: 3 :: Only", 23 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)" 24 | ] 25 | 26 | # The dependencies required to install the package. 27 | dependencies = ["pyfastaq >= 3.10.0"] 28 | 29 | # Dynamic versioning 30 | dynamic = ["version"] 31 | 32 | # Project URLs 33 | [project.urls] 34 | homepage = "https://github.com/sanger-pathogens/pymummer" 35 | documentation = "https://github.com/sanger-pathogens/pymummer" 36 | repository = "https://github.com/sanger-pathogens/pymummer" 37 | 38 | # Hatch build configuration 39 | [tool.hatch.build] 40 | source = "src" 41 | 42 | # Exclude files and directories from the build 43 | exclude = [ 44 | "environment.yml", 45 | "env_osx64.yml", 46 | ] 47 | 48 | # Hatch versioning configuration 49 | [tool.hatch.version] 50 | source = "vcs" 51 | 52 | # Version control system (VCS) versioning 53 | [tool.hatch.version.vcs] 54 | tag-pattern = "v*" # Git tags starting with 'v' will be used for versioning 55 | fallback-version = "0.0.0" 56 | 57 | # Version file location for VCS 58 | [tool.hatch.build.hooks.vcs] 59 | version-file = "src/pymummer/_version.py" 60 | 61 | # Optional dependencies for testing 62 | [project.optional-dependencies] 63 | tests = ["pytest"] -------------------------------------------------------------------------------- /src/pymummer/snp.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | pass 3 | 4 | 5 | class Snp: 6 | def __init__(self, line): 7 | # Without the -C option to show-snps, looks like this: 8 | # [P1] [SUB] [SUB] [P2] [BUFF] [DIST] [LEN R] [LEN Q] [FRM] [TAGS] 9 | # 187 A C 269 187 187 654 853 1 1 ref_name qry_name 10 | 11 | # With the -C option to show-snps, looks like this: 12 | # [P1] [SUB] [SUB] [P2] [BUFF] [DIST] [R] [Q] [LEN R] [LEN Q] [FRM] [TAGS] 13 | # 187 A C 269 187 187 0 0 654 853 1 1 ref_name qry_name 14 | try: 15 | l = line.rstrip().split("\t") 16 | self.ref_pos = int(l[0]) - 1 17 | self.ref_base = l[1] 18 | self.qry_base = l[2] 19 | self.qry_pos = int(l[3]) - 1 20 | self.ref_length = int(l[-6]) 21 | self.qry_length = int(l[-5]) 22 | self.reverse = {"1": False, "-1": True}[l[-3]] 23 | self.ref_name = l[-2] 24 | self.qry_name = l[-1] 25 | except: 26 | raise Error( 27 | "Error constructing pymummer.snp.Snp from mummer show-snps output at this line:\n" 28 | + line 29 | ) 30 | 31 | def __eq__(self, other): 32 | return type(other) is type(self) and self.__dict__ == other.__dict__ 33 | 34 | def __str__(self): 35 | return "\t".join( 36 | [ 37 | str(x) 38 | for x in [ 39 | self.ref_pos + 1, 40 | self.ref_base, 41 | self.qry_base, 42 | self.qry_pos + 1, 43 | self.ref_length, 44 | self.qry_length, 45 | "-1" if self.reverse else "1", 46 | self.ref_name, 47 | self.qry_name, 48 | ] 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /src/pymummer/coords_file.py: -------------------------------------------------------------------------------- 1 | import pyfastaq 2 | from pymummer import alignment 3 | 4 | 5 | class Error(Exception): 6 | pass 7 | 8 | 9 | def reader(fname): 10 | """Helper function to open the results file (coords file) and create alignment objects with the values in it""" 11 | f = pyfastaq.utils.open_file_read(fname) 12 | 13 | for line in f: 14 | if line.startswith("[") or (not "\t" in line): 15 | continue 16 | 17 | yield alignment.Alignment(line) 18 | 19 | pyfastaq.utils.close(f) 20 | 21 | 22 | def convert_to_msp_crunch(infile, outfile, ref_fai=None, qry_fai=None): 23 | """Converts a coords file to a file in MSPcrunch format (for use with ACT, most likely). 24 | ACT ignores sequence names in the crunch file, and just looks at the numbers. 25 | To make a compatible file, the coords all must be shifted appropriately, which 26 | can be done by providing both the ref_fai and qry_fai options. 27 | Both or neither of these must be used, otherwise an error will be thrown.""" 28 | fai_files = {ref_fai, qry_fai} 29 | if None in fai_files and len(fai_files) != 1: 30 | print(fai_files) 31 | raise Error( 32 | "Error in convert_to_msp_crunch. Must use both of ref_fai and qry_fai, or neither of them" 33 | ) 34 | 35 | if ref_fai is not None: 36 | assert qry_fai is not None 37 | ref_offsets = pyfastaq.tasks.length_offsets_from_fai(ref_fai) 38 | qry_offsets = pyfastaq.tasks.length_offsets_from_fai(qry_fai) 39 | 40 | file_reader = reader(infile) 41 | f_out = pyfastaq.utils.open_file_write(outfile) 42 | 43 | for aln in file_reader: 44 | if ref_fai is not None: 45 | aln.ref_start += ref_offsets[aln.ref_name] 46 | aln.ref_end += ref_offsets[aln.ref_name] 47 | aln.qry_start += qry_offsets[aln.qry_name] 48 | aln.qry_end += qry_offsets[aln.qry_name] 49 | 50 | print(aln.to_msp_crunch(), file=f_out) 51 | 52 | pyfastaq.utils.close(f_out) 53 | -------------------------------------------------------------------------------- /src/pymummer/variant.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | pass 3 | 4 | 5 | SNP = 1 6 | DEL = 2 7 | INS = 3 8 | 9 | var_types = { 10 | 1: "SNP", 11 | 2: "DEL", 12 | 3: "INS", 13 | } 14 | 15 | 16 | class Variant: 17 | def __init__(self, snp): 18 | """Create a Variant object from a pymummer.snp.Snp object""" 19 | if snp.ref_base == ".": 20 | self.var_type = INS 21 | self.qry_base = snp.qry_base 22 | self.ref_base = "." 23 | elif snp.qry_base == ".": 24 | self.var_type = DEL 25 | self.qry_base = "." 26 | self.ref_base = snp.ref_base 27 | elif "." not in [snp.ref_base, snp.qry_base]: 28 | self.var_type = SNP 29 | self.ref_base = snp.ref_base 30 | self.qry_base = snp.qry_base 31 | else: 32 | raise Error("Error constructing Variant from pymummer.snp.Snp:" + str(snp)) 33 | 34 | self.ref_start = snp.ref_pos 35 | self.ref_end = snp.ref_pos 36 | self.ref_length = snp.ref_length 37 | self.ref_name = snp.ref_name 38 | self.qry_start = snp.qry_pos 39 | self.qry_end = snp.qry_pos 40 | self.qry_length = snp.qry_length 41 | self.qry_name = snp.qry_name 42 | self.reverse = snp.reverse 43 | 44 | def __eq__(self, other): 45 | return type(other) is type(self) and self.__dict__ == other.__dict__ 46 | 47 | def __str__(self): 48 | return "\t".join( 49 | [ 50 | str(self.ref_start + 1), 51 | str(self.ref_end + 1), 52 | str(self.ref_length), 53 | str(self.ref_name), 54 | self.ref_base, 55 | str(self.qry_start + 1), 56 | str(self.qry_end + 1), 57 | str(self.qry_length), 58 | str(self.qry_name), 59 | self.qry_base, 60 | "-1" if self.reverse else "1", 61 | ] 62 | ) 63 | 64 | def update_indel(self, nucmer_snp): 65 | """Indels are reported over multiple lines, 1 base insertion or deletion per line. This method extends the current variant by 1 base if it's an indel and adjacent to the new SNP and returns True. If the current variant is a SNP, does nothing and returns False""" 66 | new_variant = Variant(nucmer_snp) 67 | if ( 68 | self.var_type not in [INS, DEL] 69 | or self.var_type != new_variant.var_type 70 | or self.qry_name != new_variant.qry_name 71 | or self.ref_name != new_variant.ref_name 72 | or self.reverse != new_variant.reverse 73 | ): 74 | return False 75 | if ( 76 | self.var_type == INS 77 | and self.ref_start == new_variant.ref_start 78 | and self.qry_end + 1 == new_variant.qry_start 79 | ): 80 | self.qry_base += new_variant.qry_base 81 | self.qry_end += 1 82 | return True 83 | if ( 84 | self.var_type == DEL 85 | and self.qry_start == new_variant.qry_start 86 | and self.ref_end + 1 == new_variant.ref_start 87 | ): 88 | self.ref_base += new_variant.ref_base 89 | self.ref_end += 1 90 | return True 91 | 92 | return False 93 | -------------------------------------------------------------------------------- /tests/coords_file_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import filecmp 4 | from pymummer import coords_file, alignment 5 | 6 | data_dir = "tests/data" 7 | 8 | 9 | class TestCoordsFile(unittest.TestCase): 10 | def test_coords_file(self): 11 | """test coords_file""" 12 | expected = [ 13 | "\t".join( 14 | [ 15 | "61", 16 | "900", 17 | "1", 18 | "840", 19 | "840", 20 | "840", 21 | "99.76", 22 | "1000", 23 | "840", 24 | "1", 25 | "1", 26 | "test_ref1", 27 | "test_qry1", 28 | "[CONTAINS]", 29 | ] 30 | ), 31 | "\t".join( 32 | [ 33 | "62", 34 | "901", 35 | "2", 36 | "841", 37 | "841", 38 | "850", 39 | "99.66", 40 | "999", 41 | "839", 42 | "1", 43 | "1", 44 | "test_ref2", 45 | "test_qry2", 46 | "[CONTAINS]", 47 | ] 48 | ), 49 | "\t".join( 50 | [ 51 | "63", 52 | "902", 53 | "3", 54 | "842", 55 | "842", 56 | "860", 57 | "99.56", 58 | "998", 59 | "838", 60 | "1", 61 | "1", 62 | "test_ref3", 63 | "test_qry3", 64 | "[CONTAINS]", 65 | ] 66 | ), 67 | ] 68 | expected = [alignment.Alignment(x) for x in expected] 69 | 70 | infiles = [ 71 | os.path.join(data_dir, "coords_file_test_with_header.coords"), 72 | os.path.join(data_dir, "coords_file_test_no_header.coords"), 73 | ] 74 | 75 | for fname in infiles: 76 | fr = coords_file.reader(fname) 77 | alignments = [x for x in fr] 78 | self.assertEqual(alignments, expected) 79 | 80 | def test_convert_to_msp_crunch_no_offset(self): 81 | """Test convert_to_msp_crunch with no offsets""" 82 | infile = os.path.join(data_dir, "coords_file_test_convert_to_msp_crunch.coords") 83 | expected = os.path.join( 84 | data_dir, "coords_file_test_convert_to_msp_crunch.no_offset.crunch" 85 | ) 86 | tmpfile = "tmp.test_convert_to_msp_crunch_no_offset.crunch" 87 | coords_file.convert_to_msp_crunch(infile, tmpfile) 88 | self.assertTrue(filecmp.cmp(expected, tmpfile, shallow=False)) 89 | os.unlink(tmpfile) 90 | 91 | def test_convert_to_msp_crunch_with_offset(self): 92 | """Test convert_to_msp_crunch with offsets""" 93 | infile = os.path.join(data_dir, "coords_file_test_convert_to_msp_crunch.coords") 94 | ref_fai = os.path.join( 95 | data_dir, "coords_file_test_convert_to_msp_crunch.ref.fa.fai" 96 | ) 97 | qry_fai = os.path.join( 98 | data_dir, "coords_file_test_convert_to_msp_crunch.qry.fa.fai" 99 | ) 100 | expected = os.path.join( 101 | data_dir, "coords_file_test_convert_to_msp_crunch.with_offset.crunch" 102 | ) 103 | tmpfile = "tmp.test_convert_to_msp_crunch_with_offset.crunch" 104 | 105 | with self.assertRaises(coords_file.Error): 106 | coords_file.convert_to_msp_crunch(infile, tmpfile, ref_fai=ref_fai) 107 | coords_file.convert_to_msp_crunch(infile, tmpfile, qry_fai=qry_fai) 108 | 109 | coords_file.convert_to_msp_crunch( 110 | infile, tmpfile, ref_fai=ref_fai, qry_fai=qry_fai 111 | ) 112 | self.assertTrue(filecmp.cmp(expected, tmpfile, shallow=False)) 113 | os.unlink(tmpfile) 114 | -------------------------------------------------------------------------------- /src/pymummer/nucmer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import shutil 4 | import pyfastaq 5 | from pymummer import syscall 6 | 7 | 8 | class Error(Exception): 9 | pass 10 | 11 | 12 | class Runner: 13 | """Handy reference for all the arguments needed for nucmer, delta-filter, show-coords, show-snps""" 14 | 15 | def __init__( 16 | self, 17 | ref, 18 | query, 19 | outfile, 20 | min_id=None, 21 | min_length=None, 22 | breaklen=None, 23 | coords_header=True, 24 | diagdiff=None, 25 | diagfactor=None, 26 | maxgap=None, 27 | maxmatch=False, 28 | mincluster=None, 29 | simplify=True, 30 | show_snps=False, 31 | snps_header=True, 32 | verbose=False, 33 | promer=False, 34 | show_snps_C=True, 35 | ): 36 | self.qry = query 37 | self.ref = ref 38 | self.outfile = outfile 39 | self.min_id = min_id 40 | self.min_length = min_length 41 | self.breaklen = breaklen 42 | self.diagdiff = diagdiff 43 | self.diagfactor = diagfactor 44 | self.coords_header = coords_header 45 | self.maxgap = maxgap 46 | self.maxmatch = maxmatch 47 | self.mincluster = mincluster 48 | self.simplify = simplify 49 | self.show_snps = show_snps 50 | self.snps_header = snps_header 51 | self.verbose = verbose 52 | self.use_promer = promer 53 | self.show_snps_C = show_snps_C 54 | 55 | def _nucmer_command(self, ref, qry, outprefix): 56 | """Construct the nucmer command""" 57 | if self.use_promer: 58 | command = "promer" 59 | else: 60 | command = "nucmer" 61 | 62 | command += " -p " + outprefix 63 | 64 | if self.breaklen is not None: 65 | command += " -b " + str(self.breaklen) 66 | 67 | if self.diagdiff is not None and not self.use_promer: 68 | command += " -D " + str(self.diagdiff) 69 | 70 | if self.diagfactor: 71 | command += " -d " + str(self.diagfactor) 72 | 73 | if self.maxgap: 74 | command += " -g " + str(self.maxgap) 75 | 76 | if self.maxmatch: 77 | command += " --maxmatch" 78 | 79 | if self.mincluster is not None: 80 | command += " -c " + str(self.mincluster) 81 | 82 | if not self.simplify and not self.use_promer: 83 | command += " --nosimplify" 84 | 85 | return command + " " + ref + " " + qry 86 | 87 | def _delta_filter_command(self, infile, outfile): 88 | """Construct delta-filter command""" 89 | command = "delta-filter" 90 | 91 | if self.min_id is not None: 92 | command += " -i " + str(self.min_id) 93 | 94 | if self.min_length is not None: 95 | command += " -l " + str(self.min_length) 96 | 97 | return command + " " + infile + " > " + outfile 98 | 99 | def _show_coords_command(self, infile, outfile): 100 | """Construct show-coords command""" 101 | command = "show-coords -dTlro" 102 | 103 | if not self.coords_header: 104 | command += " -H" 105 | 106 | return command + " " + infile + " > " + outfile 107 | 108 | def _show_snps_command(self, infile, outfile): 109 | command = "show-snps -T" + ("C" if self.show_snps_C else "") + "lr" 110 | 111 | if not self.snps_header: 112 | command += " -H" 113 | 114 | return command + " " + infile + " > " + outfile 115 | 116 | def _write_script(self, script_name, ref, qry, outfile): 117 | """Write commands into a bash script""" 118 | f = pyfastaq.utils.open_file_write(script_name) 119 | print(self._nucmer_command(ref, qry, "p"), file=f) 120 | print(self._delta_filter_command("p.delta", "p.delta.filter"), file=f) 121 | print(self._show_coords_command("p.delta.filter", outfile), file=f) 122 | if self.show_snps: 123 | print(self._show_snps_command("p.delta.filter", outfile + ".snps"), file=f) 124 | pyfastaq.utils.close(f) 125 | 126 | def run(self): 127 | """ 128 | Change to a temp directory 129 | Run bash script containing commands 130 | Place results in specified output file 131 | Clean up temp directory 132 | """ 133 | qry = os.path.abspath(self.qry) 134 | ref = os.path.abspath(self.ref) 135 | outfile = os.path.abspath(self.outfile) 136 | tmpdir = tempfile.mkdtemp(prefix="tmp.run_nucmer.") 137 | original_dir = os.getcwd() 138 | os.chdir(tmpdir) 139 | script = "run_nucmer.sh" 140 | self._write_script(script, ref, qry, outfile) 141 | syscall.run("bash " + script, verbose=self.verbose) 142 | os.chdir(original_dir) 143 | shutil.rmtree(tmpdir) 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pymummer 2 | 3 | Python3 wrapper for running MUMmer and parsing the output. 4 | 5 | [![Build Status](https://travis-ci.org/sanger-pathogens/pymummer.svg?branch=master)](https://travis-ci.org/sanger-pathogens/pymummer) 6 | [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-brightgreen.svg)](https://github.com/sanger-pathogens/pymummer/blob/master/LICENSE) 7 | [![install with bioconda](https://img.shields.io/badge/install%20with-bioconda-brightgreen.svg?style=flat-square)](http://bioconda.github.io/recipes/pymummer/README.html) 8 | [![Container ready](https://img.shields.io/badge/container-ready-brightgreen.svg)](https://quay.io/repository/biocontainers/pymummer) 9 | 10 | ## Contents 11 | * [Introduction](#introduction) 12 | * [Installation](#installation) 13 | * [Required dependencies](#required-dependencies) 14 | * [Homebrew/LinuxBrew](#homebrewlinuxbrew) 15 | * [Using pip](#using-pip) 16 | * [Running the tests](#running-the-tests) 17 | * [Usage (for developers)](#usage-for-developers) 18 | * [pymummer nucmer class](#pymummer-nucmer-class) 19 | * [pymummer coords\_file class](#pymummer-coords_file-class) 20 | * [pymummer alignment class](#pymummer-alignment-class) 21 | * [License](#license) 22 | * [Feedback/Issues](#feedbackissues) 23 | 24 | ## Introduction 25 | Runs MUMmer and parses the output. 26 | 27 | ## Installation 28 | pymummer has the following dependencies: 29 | 30 | ### Required dependencies 31 | * [MUMmer](http://mummer.sourceforge.net/manual/#installation) 32 | 33 | There are a number of ways to install pymummer and details are provided below. If you encounter an issue when installing pymummer please contact your local system administrator. If you encounter a bug please log it [here](https://github.com/sanger-pathogens/pymummer/issues) or email us at path-help@sanger.ac.uk. 34 | 35 | ### Homebrew/LinuxBrew 36 | ``` 37 | brew tap homebrew/python 38 | brew install pymummer 39 | ``` 40 | 41 | ### Conda 42 | 43 | We have provided conda environment recipes in this repo that can be used to create a fresh environment with the required dependencies. After creating a new env you can `pip install` pymummer from pypi or this repo using the commands in the next section. 44 | 45 | ```bash 46 | # Create pymummer env 47 | conda env create -f environment.yml 48 | # Activate env 49 | conda activate pymummer 50 | # Install pymummer 51 | pip install pymummer 52 | ``` 53 | 54 | If you are using an M-Series Mac (ARM64 processor) you will need to create a mock Intel environment to install Mummer4 from Bioconda. 55 | 56 | ```bash 57 | # Apple ARM64 Macs only 58 | # Create mock Intel env 59 | conda env create -f env_osx64.yml 60 | # Activate env 61 | conda activate pymummer-osx64 62 | # Install pymummer 63 | pip install pymummer 64 | ``` 65 | 66 | ### Pip install 67 | 68 | Install from PyPi 69 | 70 | ```bash 71 | pip3 install pymummer 72 | ``` 73 | 74 | Or pip install the latest development version directly from this repo. 75 | 76 | ```bash 77 | pip3 install git+https://github.com/sanger-pathogens/pymummer.git 78 | ``` 79 | 80 | ### Running the tests 81 | The test can be run from the top level directory: 82 | 83 | ``` 84 | pytest tests 85 | ``` 86 | 87 | ## Usage (for developers) 88 | 89 | Example showing how pymummer can be used to run nucmer on a fasta file and parse the output file to produce a set of alignment objects: 90 | ``` 91 | from pymummer import coords_file, alignment, nucmer 92 | ... 93 | runner = nucmer.Runner(reference_file, query_file, results_file) 94 | runner.run() 95 | file_reader = coords_file.reader(results_file) 96 | alignments = [coord for coord in file_reader if not coord.is_self_hit()] #Remove self hits 97 | ... 98 | ``` 99 | ### pymummer nucmer class 100 | 101 | Wraps the `nucmer`, `delta-filter`, `show-coords` and `show-snps` commands. 102 | 103 | Arguments: 104 | ``` 105 | __ref__ reference file 106 | __query__ query file 107 | __outfile__ output file 108 | __min\_id__ min\_id for delta-filter command (default None) 109 | __min\_length__ min\_length for delta-filter command (default None) 110 | __breaklen__ breaklen for nucmer command (nucmer's default is 200) 111 | __coords\_header__ print header in show-coords output (default True) 112 | __maxmatch__ maxmatch for nucmer (default False) 113 | __show\_snps__ run show-snps (default False) 114 | __snps\_header__ print header in show-snps output (default True) 115 | ``` 116 | 117 | ### pymummer coords_file class 118 | 119 | Parses the nucmer output and populate an alignment object for each hit in the output 120 | 121 | ### pymummer alignment class 122 | 123 | Check attributes of a hit, swap the reference and query, check if it's a self hit and so on 124 | 125 | ## License 126 | pymummer is free software, licensed under [GPLv3](https://github.com/sanger-pathogens/pymummer/blob/master/LICENSE). 127 | 128 | ## Feedback/Issues 129 | Please report any issues to the [issues page](https://github.com/sanger-pathogens/pymummer/issues) or email path-help@sanger.ac.uk. -------------------------------------------------------------------------------- /tests/nucmer_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import filecmp 4 | from pymummer import nucmer 5 | 6 | data_dir = "tests/data" 7 | 8 | 9 | class TestRunner(unittest.TestCase): 10 | def test_nucmer_command(self): 11 | """test _nucmer_command""" 12 | tests = [ 13 | [nucmer.Runner("ref", "qry", "outfile"), "nucmer -p pre ref qry"], 14 | [ 15 | nucmer.Runner("ref", "qry", "outfile", breaklen=42), 16 | "nucmer -p pre -b 42 ref qry", 17 | ], 18 | [ 19 | nucmer.Runner("ref", "qry", "outfile", diagdiff=11), 20 | "nucmer -p pre -D 11 ref qry", 21 | ], 22 | [ 23 | nucmer.Runner("ref", "qry", "outfile", diagdiff=11, promer=True), 24 | "promer -p pre ref qry", 25 | ], 26 | [ 27 | nucmer.Runner("ref", "qry", "outfile", maxmatch=True), 28 | "nucmer -p pre --maxmatch ref qry", 29 | ], 30 | [ 31 | nucmer.Runner("ref", "qry", "outfile", mincluster=42), 32 | "nucmer -p pre -c 42 ref qry", 33 | ], 34 | [ 35 | nucmer.Runner("ref", "qry", "outfile", simplify=False), 36 | "nucmer -p pre --nosimplify ref qry", 37 | ], 38 | [ 39 | nucmer.Runner("ref", "qry", "outfile", promer=True), 40 | "promer -p pre ref qry", 41 | ], 42 | [ 43 | nucmer.Runner("ref", "qry", "outfile", promer=True, breaklen=42), 44 | "promer -p pre -b 42 ref qry", 45 | ], 46 | [ 47 | nucmer.Runner("ref", "qry", "outfile", promer=True, maxmatch=True), 48 | "promer -p pre --maxmatch ref qry", 49 | ], 50 | [ 51 | nucmer.Runner("ref", "qry", "outfile", promer=True, simplify=False), 52 | "promer -p pre ref qry", 53 | ], 54 | ] 55 | 56 | for l in tests: 57 | self.assertEqual(l[0]._nucmer_command("ref", "qry", "pre"), l[1]) 58 | 59 | def test_delta_filter_command(self): 60 | """test _delta_filter_command""" 61 | tests = [ 62 | [nucmer.Runner("ref", "qry", "outfile"), "delta-filter infile > outfile"], 63 | [ 64 | nucmer.Runner("ref", "qry", "outfile", min_id=42), 65 | "delta-filter -i 42 infile > outfile", 66 | ], 67 | [ 68 | nucmer.Runner("ref", "qry", "outfile", min_length=43), 69 | "delta-filter -l 43 infile > outfile", 70 | ], 71 | ] 72 | 73 | for l in tests: 74 | self.assertEqual(l[0]._delta_filter_command("infile", "outfile"), l[1]) 75 | 76 | def test_show_coords_command(self): 77 | """test _show_coords_command""" 78 | tests = [ 79 | [ 80 | nucmer.Runner("ref", "qry", "outfile", coords_header=False), 81 | "show-coords -dTlro -H infile > outfile", 82 | ], 83 | [ 84 | nucmer.Runner("ref", "qry", "outfile"), 85 | "show-coords -dTlro infile > outfile", 86 | ], 87 | ] 88 | 89 | for l in tests: 90 | self.assertEqual(l[0]._show_coords_command("infile", "outfile"), l[1]) 91 | 92 | def test_show_snps_command(self): 93 | """test _show_snps_command""" 94 | tests = [ 95 | [ 96 | nucmer.Runner("ref", "qry", "outfile", snps_header=False), 97 | "show-snps -TClr -H infile > outfile", 98 | ], 99 | [ 100 | nucmer.Runner("ref", "qry", "outfile"), 101 | "show-snps -TClr infile > outfile", 102 | ], 103 | [ 104 | nucmer.Runner("ref", "qry", "outfile", show_snps_C=False), 105 | "show-snps -Tlr infile > outfile", 106 | ], 107 | ] 108 | 109 | for nuc_obj, expected in tests: 110 | self.assertEqual(nuc_obj._show_snps_command("infile", "outfile"), expected) 111 | 112 | def test_write_script_no_snps(self): 113 | """test _write_script no snps""" 114 | tmp_script = "tmp.script.sh" 115 | r = nucmer.Runner("ref", "qry", "outfile") 116 | r._write_script(tmp_script, "ref", "qry", "outfile") 117 | expected = os.path.join(data_dir, "nucmer_test_write_script_no_snps.sh") 118 | self.assertTrue(filecmp.cmp(expected, tmp_script, shallow=False)) 119 | os.unlink(tmp_script) 120 | 121 | def test_write_script_with_snps(self): 122 | """test _write_script with snps""" 123 | tmp_script = "tmp.script.sh" 124 | r = nucmer.Runner("ref", "qry", "outfile", show_snps="outfile.snps") 125 | r._write_script(tmp_script, "ref", "qry", "outfile") 126 | expected = os.path.join(data_dir, "nucmer_test_write_script_with_snps.sh") 127 | self.assertTrue(filecmp.cmp(expected, tmp_script, shallow=False)) 128 | os.unlink(tmp_script) 129 | 130 | def test_run_nucmer(self): 131 | """test run_nucmer""" 132 | qry = os.path.join(data_dir, "nucmer_test_qry.fa") 133 | ref = os.path.join(data_dir, "nucmer_test_ref.fa") 134 | tmp_out = "tmp.nucmer.out" 135 | runner = nucmer.Runner( 136 | ref, qry, tmp_out, coords_header=False, show_snps=True, snps_header=False 137 | ) 138 | runner.run() 139 | expected = os.path.join(data_dir, "nucmer_test_out.coords") 140 | self.assertTrue(filecmp.cmp(tmp_out, expected, shallow=False)) 141 | self.assertTrue( 142 | filecmp.cmp(tmp_out + ".snps", expected + ".snps", shallow=False) 143 | ) 144 | os.unlink(tmp_out) 145 | os.unlink(tmp_out + ".snps") 146 | -------------------------------------------------------------------------------- /tests/snp_file_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from pymummer import snp_file, snp, variant 4 | 5 | data_dir = "tests/data" 6 | 7 | 8 | class TestUtils(unittest.TestCase): 9 | def test_snp_file(self): 10 | """test coords_file""" 11 | expected = [ 12 | "\t".join( 13 | [ 14 | "133", 15 | "G", 16 | ".", 17 | "122", 18 | "1", 19 | "122", 20 | "500", 21 | "489", 22 | "1", 23 | "1", 24 | "ref", 25 | "qry", 26 | ] 27 | ), 28 | "\t".join( 29 | [ 30 | "143", 31 | ".", 32 | "C", 33 | "131", 34 | "1", 35 | "132", 36 | "500", 37 | "489", 38 | "1", 39 | "1", 40 | "ref", 41 | "qry", 42 | ] 43 | ), 44 | "\t".join( 45 | [ 46 | "253", 47 | "T", 48 | "A", 49 | "242", 50 | "120", 51 | "242", 52 | "500", 53 | "489", 54 | "1", 55 | "1", 56 | "ref", 57 | "qry", 58 | ] 59 | ), 60 | ] 61 | 62 | expected = [snp.Snp(x) for x in expected] 63 | 64 | infiles = [ 65 | os.path.join(data_dir, "snp_file_test_with_header.snps"), 66 | os.path.join(data_dir, "snp_file_test_no_header.snps"), 67 | ] 68 | 69 | for fname in infiles: 70 | fr = snp_file.reader(fname) 71 | snps = [x for x in fr] 72 | self.assertEqual(snps, expected) 73 | 74 | def test_get_all_variants(self): 75 | """Test load all variants from file""" 76 | deletion_snps = [ 77 | "\t".join( 78 | [ 79 | "125", 80 | "T", 81 | ".", 82 | "124", 83 | "1", 84 | "124", 85 | "500", 86 | "497", 87 | "1", 88 | "1", 89 | "ref1", 90 | "qry1", 91 | ] 92 | ), 93 | "\t".join( 94 | [ 95 | "126", 96 | "A", 97 | ".", 98 | "124", 99 | "1", 100 | "124", 101 | "500", 102 | "497", 103 | "1", 104 | "1", 105 | "ref1", 106 | "qry1", 107 | ] 108 | ), 109 | "\t".join( 110 | [ 111 | "127", 112 | "C", 113 | ".", 114 | "124", 115 | "1", 116 | "124", 117 | "500", 118 | "497", 119 | "1", 120 | "1", 121 | "ref1", 122 | "qry1", 123 | ] 124 | ), 125 | ] 126 | deletion_snps = [snp.Snp(x) for x in deletion_snps] 127 | deletion_variant = variant.Variant(deletion_snps[0]) 128 | deletion_variant.update_indel(deletion_snps[1]) 129 | deletion_variant.update_indel(deletion_snps[2]) 130 | 131 | just_a_snp = "\t".join( 132 | [ 133 | "386", 134 | "C", 135 | "T", 136 | "383", 137 | "115", 138 | "115", 139 | "500", 140 | "497", 141 | "1", 142 | "1", 143 | "ref1", 144 | "qry1", 145 | ] 146 | ) 147 | snp_variant = variant.Variant(snp.Snp(just_a_snp)) 148 | 149 | insertion_snps = [ 150 | "\t".join( 151 | [ 152 | "479", 153 | ".", 154 | "G", 155 | "480", 156 | "0", 157 | "22", 158 | "500", 159 | "504", 160 | "1", 161 | "1", 162 | "ref2", 163 | "qry2", 164 | ] 165 | ), 166 | "\t".join( 167 | [ 168 | "479", 169 | ".", 170 | "A", 171 | "481", 172 | "0", 173 | "22", 174 | "500", 175 | "504", 176 | "1", 177 | "1", 178 | "ref2", 179 | "qry2", 180 | ] 181 | ), 182 | "\t".join( 183 | [ 184 | "479", 185 | ".", 186 | "T", 187 | "482", 188 | "0", 189 | "22", 190 | "500", 191 | "504", 192 | "1", 193 | "1", 194 | "ref2", 195 | "qry2", 196 | ] 197 | ), 198 | "\t".join( 199 | [ 200 | "479", 201 | ".", 202 | "A", 203 | "483", 204 | "0", 205 | "22", 206 | "500", 207 | "504", 208 | "1", 209 | "1", 210 | "ref2", 211 | "qry2", 212 | ] 213 | ), 214 | ] 215 | insertion_snps = [snp.Snp(x) for x in insertion_snps] 216 | insertion_variant = variant.Variant(insertion_snps[0]) 217 | for i in range(1, len(insertion_snps)): 218 | insertion_variant.update_indel(insertion_snps[i]) 219 | 220 | variants_from_file = snp_file.get_all_variants( 221 | os.path.join(data_dir, "snp_file_test_get_all_variants.snps") 222 | ) 223 | self.assertEqual(len(variants_from_file), 3) 224 | self.assertEqual(variants_from_file[0], deletion_variant) 225 | self.assertEqual(variants_from_file[1], snp_variant) 226 | self.assertEqual(variants_from_file[2], insertion_variant) 227 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased](https://github.com/sanger-pathogens/pymummer/tree/HEAD) 4 | 5 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.10.3...HEAD) 6 | 7 | **Merged pull requests:** 8 | 9 | - update README [\#29](https://github.com/sanger-pathogens/pymummer/pull/29) ([ssjunnebo](https://github.com/ssjunnebo)) 10 | 11 | ## [v0.10.3](https://github.com/sanger-pathogens/pymummer/tree/v0.10.3) (2017-08-21) 12 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.10.2...v0.10.3) 13 | 14 | **Merged pull requests:** 15 | 16 | - Add diagfactor \(-d\) and maxgap \(-g\) options for nucmer [\#28](https://github.com/sanger-pathogens/pymummer/pull/28) ([Adamtaranto](https://github.com/Adamtaranto)) 17 | - update LICENSE [\#27](https://github.com/sanger-pathogens/pymummer/pull/27) ([ssjunnebo](https://github.com/ssjunnebo)) 18 | 19 | ## [v0.10.2](https://github.com/sanger-pathogens/pymummer/tree/v0.10.2) (2017-02-02) 20 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.10.1...v0.10.2) 21 | 22 | **Closed issues:** 23 | 24 | - MUMmer programs not found in path [\#21](https://github.com/sanger-pathogens/pymummer/issues/21) 25 | 26 | **Merged pull requests:** 27 | 28 | - Ref qry translate vars not in matches [\#26](https://github.com/sanger-pathogens/pymummer/pull/26) ([martinghunt](https://github.com/martinghunt)) 29 | 30 | ## [v0.10.1](https://github.com/sanger-pathogens/pymummer/tree/v0.10.1) (2016-11-18) 31 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.10.0...v0.10.1) 32 | 33 | **Merged pull requests:** 34 | 35 | - Indels bug [\#25](https://github.com/sanger-pathogens/pymummer/pull/25) ([martinghunt](https://github.com/martinghunt)) 36 | 37 | ## [v0.10.0](https://github.com/sanger-pathogens/pymummer/tree/v0.10.0) (2016-11-15) 38 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.9.0...v0.10.0) 39 | 40 | **Merged pull requests:** 41 | 42 | - Show snps expose C option [\#24](https://github.com/sanger-pathogens/pymummer/pull/24) ([martinghunt](https://github.com/martinghunt)) 43 | 44 | ## [v0.9.0](https://github.com/sanger-pathogens/pymummer/tree/v0.9.0) (2016-10-17) 45 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.8.1...v0.9.0) 46 | 47 | **Merged pull requests:** 48 | 49 | - Expose mincluster [\#23](https://github.com/sanger-pathogens/pymummer/pull/23) ([martinghunt](https://github.com/martinghunt)) 50 | 51 | ## [v0.8.1](https://github.com/sanger-pathogens/pymummer/tree/v0.8.1) (2016-08-23) 52 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.8.0...v0.8.1) 53 | 54 | ## [v0.8.0](https://github.com/sanger-pathogens/pymummer/tree/v0.8.0) (2016-08-23) 55 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.7.1...v0.8.0) 56 | 57 | **Merged pull requests:** 58 | 59 | - Ref coords from qry coord [\#22](https://github.com/sanger-pathogens/pymummer/pull/22) ([martinghunt](https://github.com/martinghunt)) 60 | 61 | ## [v0.7.1](https://github.com/sanger-pathogens/pymummer/tree/v0.7.1) (2016-04-18) 62 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.7.0...v0.7.1) 63 | 64 | **Merged pull requests:** 65 | 66 | - Make \_\_version\_\_ work [\#20](https://github.com/sanger-pathogens/pymummer/pull/20) ([martinghunt](https://github.com/martinghunt)) 67 | 68 | ## [v0.7.0](https://github.com/sanger-pathogens/pymummer/tree/v0.7.0) (2016-02-09) 69 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.6.1...v0.7.0) 70 | 71 | **Merged pull requests:** 72 | 73 | - Coords finding in matches [\#19](https://github.com/sanger-pathogens/pymummer/pull/19) ([martinghunt](https://github.com/martinghunt)) 74 | 75 | ## [v0.6.1](https://github.com/sanger-pathogens/pymummer/tree/v0.6.1) (2015-10-19) 76 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.6.0...v0.6.1) 77 | 78 | **Merged pull requests:** 79 | 80 | - Remove numpy dependency [\#18](https://github.com/sanger-pathogens/pymummer/pull/18) ([martinghunt](https://github.com/martinghunt)) 81 | 82 | ## [v0.6.0](https://github.com/sanger-pathogens/pymummer/tree/v0.6.0) (2015-10-14) 83 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.5.0...v0.6.0) 84 | 85 | **Merged pull requests:** 86 | 87 | - Convert to mspcrunch [\#17](https://github.com/sanger-pathogens/pymummer/pull/17) ([martinghunt](https://github.com/martinghunt)) 88 | 89 | ## [v0.5.0](https://github.com/sanger-pathogens/pymummer/tree/v0.5.0) (2015-10-09) 90 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.4.0...v0.5.0) 91 | 92 | **Merged pull requests:** 93 | 94 | - Msp crunch output [\#16](https://github.com/sanger-pathogens/pymummer/pull/16) ([martinghunt](https://github.com/martinghunt)) 95 | - Add TravisCI support [\#15](https://github.com/sanger-pathogens/pymummer/pull/15) ([bewt85](https://github.com/bewt85)) 96 | 97 | ## [v0.4.0](https://github.com/sanger-pathogens/pymummer/tree/v0.4.0) (2015-05-28) 98 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.3.0...v0.4.0) 99 | 100 | **Merged pull requests:** 101 | 102 | - Expose diagdiff to nucmer [\#14](https://github.com/sanger-pathogens/pymummer/pull/14) ([martinghunt](https://github.com/martinghunt)) 103 | 104 | ## [v0.3.0](https://github.com/sanger-pathogens/pymummer/tree/v0.3.0) (2015-05-15) 105 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.2.0...v0.3.0) 106 | 107 | ## [v0.2.0](https://github.com/sanger-pathogens/pymummer/tree/v0.2.0) (2015-05-15) 108 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.1.0...v0.2.0) 109 | 110 | **Merged pull requests:** 111 | 112 | - Reverse alignments [\#13](https://github.com/sanger-pathogens/pymummer/pull/13) ([martinghunt](https://github.com/martinghunt)) 113 | - Fix constructing alignment from promer output [\#12](https://github.com/sanger-pathogens/pymummer/pull/12) ([martinghunt](https://github.com/martinghunt)) 114 | - Add promer option; version bump [\#11](https://github.com/sanger-pathogens/pymummer/pull/11) ([martinghunt](https://github.com/martinghunt)) 115 | 116 | ## [v0.1.0](https://github.com/sanger-pathogens/pymummer/tree/v0.1.0) (2015-03-05) 117 | [Full Changelog](https://github.com/sanger-pathogens/pymummer/compare/v0.0.2...v0.1.0) 118 | 119 | **Merged pull requests:** 120 | 121 | - minor version number incremented [\#10](https://github.com/sanger-pathogens/pymummer/pull/10) ([nds](https://github.com/nds)) 122 | - Added --nosimplify toggle to nucmer command [\#9](https://github.com/sanger-pathogens/pymummer/pull/9) ([nds](https://github.com/nds)) 123 | 124 | ## [v0.0.2](https://github.com/sanger-pathogens/pymummer/tree/v0.0.2) (2015-02-02) 125 | **Merged pull requests:** 126 | 127 | - Change to pyfastaq; add to pypi [\#8](https://github.com/sanger-pathogens/pymummer/pull/8) ([martinghunt](https://github.com/martinghunt)) 128 | - put system call code in seprate module [\#7](https://github.com/sanger-pathogens/pymummer/pull/7) ([martinghunt](https://github.com/martinghunt)) 129 | - Add snp\_file to list of modules [\#6](https://github.com/sanger-pathogens/pymummer/pull/6) ([martinghunt](https://github.com/martinghunt)) 130 | - Added documentation in README file [\#5](https://github.com/sanger-pathogens/pymummer/pull/5) ([nds](https://github.com/nds)) 131 | - Add variant calling [\#4](https://github.com/sanger-pathogens/pymummer/pull/4) ([martinghunt](https://github.com/martinghunt)) 132 | - Added some doc lines [\#3](https://github.com/sanger-pathogens/pymummer/pull/3) ([nds](https://github.com/nds)) 133 | - Split into separate files [\#2](https://github.com/sanger-pathogens/pymummer/pull/2) ([martinghunt](https://github.com/martinghunt)) 134 | - Initial commit [\#1](https://github.com/sanger-pathogens/pymummer/pull/1) ([martinghunt](https://github.com/martinghunt)) 135 | 136 | -------------------------------------------------------------------------------- /src/pymummer/alignment.py: -------------------------------------------------------------------------------- 1 | import pyfastaq 2 | from pymummer import variant 3 | 4 | 5 | class Error(Exception): 6 | pass 7 | 8 | 9 | class Alignment: 10 | def __init__(self, line): 11 | """Constructs Alignment object from a line of show-coords -dTlro""" 12 | # nucmer: 13 | # [S1] [E1] [S2] [E2] [LEN 1] [LEN 2] [% IDY] [LEN R] [LEN Q] [FRM] [TAGS] 14 | # 1162 25768 24536 4 24607 24533 99.32 640851 24536 1 -1 ref qry [CONTAINS] 15 | 16 | # promer: 17 | # [S1] [E1] [S2] [E2] [LEN 1] [LEN 2] [% IDY] [% SIM] [% STP] [LEN R] [LEN Q] [FRM] [TAGS] 18 | # 1 1398 4891054 4892445 1398 1392 89.55 93.18 0.21 1398 5349013 1 1 ref qry [CONTAINED] 19 | 20 | fields = line.rstrip().split("\t") 21 | 22 | try: 23 | self.ref_start = int(fields[0]) - 1 24 | self.ref_end = int(fields[1]) - 1 25 | self.qry_start = int(fields[2]) - 1 26 | self.qry_end = int(fields[3]) - 1 27 | self.hit_length_ref = int(fields[4]) 28 | self.hit_length_qry = int(fields[5]) 29 | self.percent_identity = float(fields[6]) 30 | 31 | if len(fields) >= 15: # promer has more fields 32 | self.ref_length = int(fields[9]) 33 | self.qry_length = int(fields[10]) 34 | self.frame = int(fields[11]) 35 | self.ref_name = fields[13] 36 | self.qry_name = fields[14] 37 | else: 38 | self.ref_length = int(fields[7]) 39 | self.qry_length = int(fields[8]) 40 | self.frame = int(fields[9]) 41 | self.ref_name = fields[11] 42 | self.qry_name = fields[12] 43 | except: 44 | raise Error("Error reading this nucmer line:\n" + line) 45 | 46 | def __eq__(self, other): 47 | return type(other) is type(self) and self.__dict__ == other.__dict__ 48 | 49 | def __hash__(self): 50 | return hash( 51 | ( 52 | self.ref_start, 53 | self.ref_end, 54 | self.qry_start, 55 | self.qry_end, 56 | self.hit_length_ref, 57 | self.hit_length_qry, 58 | self.percent_identity, 59 | self.ref_length, 60 | self.qry_length, 61 | self.frame, 62 | self.ref_name, 63 | self.qry_name, 64 | ) 65 | ) 66 | 67 | def _swap(self): 68 | """Swaps the alignment so that the reference becomes the query and vice-versa. Swaps their names, coordinates etc. The frame is not changed""" 69 | self.ref_start, self.qry_start = self.qry_start, self.ref_start 70 | self.ref_end, self.qry_end = self.qry_end, self.ref_end 71 | self.hit_length_ref, self.hit_length_qry = ( 72 | self.hit_length_qry, 73 | self.hit_length_ref, 74 | ) 75 | self.ref_length, self.qry_length = self.qry_length, self.ref_length 76 | self.ref_name, self.qry_name = self.qry_name, self.ref_name 77 | 78 | def qry_coords(self): 79 | """Returns a pyfastaq.intervals.Interval object of the start and end coordinates in the query sequence""" 80 | return pyfastaq.intervals.Interval( 81 | min(self.qry_start, self.qry_end), max(self.qry_start, self.qry_end) 82 | ) 83 | 84 | def ref_coords(self): 85 | """Returns a pyfastaq.intervals.Interval object of the start and end coordinates in the reference sequence""" 86 | return pyfastaq.intervals.Interval( 87 | min(self.ref_start, self.ref_end), max(self.ref_start, self.ref_end) 88 | ) 89 | 90 | def on_same_strand(self): 91 | """Returns true iff the direction of the alignment is the same in the reference and the query""" 92 | return (self.ref_start < self.ref_end) == (self.qry_start < self.qry_end) 93 | 94 | def is_self_hit(self): 95 | """Returns true iff the alignment is of a sequence to itself: names and all coordinates are the same and 100 percent identity""" 96 | return ( 97 | self.ref_name == self.qry_name 98 | and self.ref_start == self.qry_start 99 | and self.ref_end == self.qry_end 100 | and self.percent_identity == 100 101 | ) 102 | 103 | def reverse_query(self): 104 | """Changes the coordinates as if the query sequence has been reverse complemented""" 105 | self.qry_start = self.qry_length - self.qry_start - 1 106 | self.qry_end = self.qry_length - self.qry_end - 1 107 | 108 | def reverse_reference(self): 109 | """Changes the coordinates as if the reference sequence has been reverse complemented""" 110 | self.ref_start = self.ref_length - self.ref_start - 1 111 | self.ref_end = self.ref_length - self.ref_end - 1 112 | 113 | def __str__(self): 114 | """Returns a tab delimited string containing the values of this alignment object""" 115 | return "\t".join( 116 | str(x) 117 | for x in [ 118 | self.ref_start + 1, 119 | self.ref_end + 1, 120 | self.qry_start + 1, 121 | self.qry_end + 1, 122 | self.hit_length_ref, 123 | self.hit_length_qry, 124 | "{0:.2f}".format(self.percent_identity), 125 | self.ref_length, 126 | self.qry_length, 127 | self.frame, 128 | self.ref_name, 129 | self.qry_name, 130 | ] 131 | ) 132 | 133 | def to_msp_crunch(self): 134 | """Returns the alignment as a line in MSPcrunch format. The columns are space-separated and are: 135 | 1. score 136 | 2. percent identity 137 | 3. match start in the query sequence 138 | 4. match end in the query sequence 139 | 5. query sequence name 140 | 6. subject sequence start 141 | 7. subject sequence end 142 | 8. subject sequence name""" 143 | 144 | # we don't know the alignment score. Estimate it. This approximates 1 for a match. 145 | aln_score = int( 146 | self.percent_identity * 0.005 * (self.hit_length_ref + self.hit_length_qry) 147 | ) 148 | 149 | return " ".join( 150 | str(x) 151 | for x in [ 152 | aln_score, 153 | "{0:.2f}".format(self.percent_identity), 154 | self.qry_start + 1, 155 | self.qry_end + 1, 156 | self.qry_name, 157 | self.ref_start + 1, 158 | self.ref_end + 1, 159 | self.ref_name, 160 | ] 161 | ) 162 | 163 | def intersects_variant(self, var): 164 | var_ref_coords = sorted([var.ref_start, var.ref_end]) 165 | var_ref_coords = pyfastaq.intervals.Interval( 166 | var_ref_coords[0], var_ref_coords[1] 167 | ) 168 | var_qry_coords = sorted([var.qry_start, var.qry_end]) 169 | var_qry_coords = pyfastaq.intervals.Interval( 170 | var_qry_coords[0], var_qry_coords[1] 171 | ) 172 | return var_ref_coords.intersects( 173 | self.ref_coords() 174 | ) and var_qry_coords.intersects(self.qry_coords()) 175 | 176 | def qry_coords_from_ref_coord(self, ref_coord, variant_list): 177 | """Given a reference position and a list of variants ([variant.Variant]), 178 | works out the position in the query sequence, accounting for indels. 179 | Returns a tuple: (position, True|False), where second element is whether 180 | or not the ref_coord lies in an indel. If it is, then 181 | returns the corresponding start position 182 | of the indel in the query""" 183 | if self.ref_coords().distance_to_point(ref_coord) > 0: 184 | raise Error( 185 | "Cannot get query coord in qry_coords_from_ref_coord because given ref_coord " 186 | + str(ref_coord) 187 | + " does not lie in nucmer alignment:\n" 188 | + str(self) 189 | ) 190 | 191 | indel_variant_indexes = [] 192 | 193 | for i in range(len(variant_list)): 194 | if variant_list[i].var_type not in {variant.INS, variant.DEL}: 195 | continue 196 | if not self.intersects_variant(variant_list[i]): 197 | continue 198 | if variant_list[i].ref_start <= ref_coord <= variant_list[i].ref_end: 199 | return variant_list[i].qry_start, True 200 | elif variant_list[i].ref_start < ref_coord: 201 | indel_variant_indexes.append(i) 202 | 203 | distance = ref_coord - min(self.ref_start, self.ref_end) 204 | 205 | for i in indel_variant_indexes: 206 | if variant_list[i].var_type == variant.INS: 207 | distance += len(variant_list[i].qry_base) 208 | else: 209 | assert variant_list[i].var_type == variant.DEL 210 | distance -= len(variant_list[i].ref_base) 211 | 212 | if self.on_same_strand(): 213 | return min(self.qry_start, self.qry_end) + distance, False 214 | else: 215 | return max(self.qry_start, self.qry_end) - distance, False 216 | 217 | def ref_coords_from_qry_coord(self, qry_coord, variant_list): 218 | """Given a qryerence position and a list of variants ([variant.Variant]), 219 | works out the position in the ref sequence, accounting for indels. 220 | Returns a tuple: (position, True|False), where second element is whether 221 | or not the qry_coord lies in an indel. If it is, then 222 | returns the corresponding start position 223 | of the indel in the ref""" 224 | if self.qry_coords().distance_to_point(qry_coord) > 0: 225 | raise Error( 226 | "Cannot get ref coord in ref_coords_from_qry_coord because given qry_coord " 227 | + str(qry_coord) 228 | + " does not lie in nucmer alignment:\n" 229 | + str(self) 230 | ) 231 | 232 | indel_variant_indexes = [] 233 | 234 | for i in range(len(variant_list)): 235 | if variant_list[i].var_type not in {variant.INS, variant.DEL}: 236 | continue 237 | if not self.intersects_variant(variant_list[i]): 238 | continue 239 | if variant_list[i].qry_start <= qry_coord <= variant_list[i].qry_end: 240 | return variant_list[i].ref_start, True 241 | elif variant_list[i].qry_start < qry_coord: 242 | indel_variant_indexes.append(i) 243 | 244 | distance = qry_coord - min(self.qry_start, self.qry_end) 245 | 246 | for i in indel_variant_indexes: 247 | if variant_list[i].var_type == variant.DEL: 248 | distance += len(variant_list[i].ref_base) 249 | else: 250 | assert variant_list[i].var_type == variant.INS 251 | distance -= len(variant_list[i].qry_base) 252 | 253 | if self.on_same_strand(): 254 | return min(self.ref_start, self.ref_end) + distance, False 255 | else: 256 | return max(self.ref_start, self.ref_end) - distance, False 257 | -------------------------------------------------------------------------------- /tests/variant_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import copy 3 | import os 4 | from pymummer import variant, snp 5 | 6 | data_dir = "tests/data" 7 | 8 | 9 | class TestVariant(unittest.TestCase): 10 | def test_init(self): 11 | """Test init gets correct variant type""" 12 | lines = [ 13 | ["42", "T", "A", "42", "42", "42", "1000", "1000", "1", "1", "ref", "ref"], 14 | [ 15 | "242", 16 | "G", 17 | ".", 18 | "241", 19 | "1", 20 | "241", 21 | "1000", 22 | "1000", 23 | "1", 24 | "1", 25 | "ref", 26 | "ref", 27 | ], 28 | [ 29 | "300", 30 | ".", 31 | "G", 32 | "298", 33 | "0", 34 | "298", 35 | "1000", 36 | "1000", 37 | "1", 38 | "1", 39 | "ref", 40 | "ref", 41 | ], 42 | ] 43 | 44 | variants = [variant.Variant(snp.Snp("\t".join(x))) for x in lines] 45 | expected = [variant.SNP, variant.DEL, variant.INS] 46 | for i in range(len(lines)): 47 | self.assertEqual(variants[i].var_type, expected[i]) 48 | 49 | def test_update_indel_no_change(self): 50 | """Test update_indel does nothing in the right cases""" 51 | initial_vars = [ 52 | snp.Snp( 53 | "\t".join( 54 | [ 55 | "42", 56 | "A", 57 | "C", 58 | "100", 59 | "x", 60 | "x", 61 | "300", 62 | "400", 63 | "x", 64 | "1", 65 | "ref", 66 | "qry", 67 | ] 68 | ) 69 | ), 70 | snp.Snp( 71 | "\t".join( 72 | [ 73 | "42", 74 | "A", 75 | "C", 76 | "100", 77 | "x", 78 | "x", 79 | "300", 80 | "400", 81 | "x", 82 | "1", 83 | "ref", 84 | "qry", 85 | ] 86 | ) 87 | ), 88 | snp.Snp( 89 | "\t".join( 90 | [ 91 | "42", 92 | "A", 93 | ".", 94 | "100", 95 | "x", 96 | "x", 97 | "300", 98 | "400", 99 | "x", 100 | "1", 101 | "ref", 102 | "qry", 103 | ] 104 | ) 105 | ), 106 | snp.Snp( 107 | "\t".join( 108 | [ 109 | "42", 110 | "A", 111 | ".", 112 | "100", 113 | "x", 114 | "x", 115 | "300", 116 | "400", 117 | "x", 118 | "1", 119 | "ref", 120 | "qry", 121 | ] 122 | ) 123 | ), 124 | snp.Snp( 125 | "\t".join( 126 | [ 127 | "42", 128 | "A", 129 | ".", 130 | "100", 131 | "x", 132 | "x", 133 | "300", 134 | "400", 135 | "x", 136 | "1", 137 | "ref", 138 | "qry", 139 | ] 140 | ) 141 | ), 142 | snp.Snp( 143 | "\t".join( 144 | [ 145 | "42", 146 | "A", 147 | ".", 148 | "100", 149 | "x", 150 | "x", 151 | "300", 152 | "400", 153 | "x", 154 | "1", 155 | "ref", 156 | "qry", 157 | ] 158 | ) 159 | ), 160 | snp.Snp( 161 | "\t".join( 162 | [ 163 | "42", 164 | "A", 165 | ".", 166 | "100", 167 | "x", 168 | "x", 169 | "300", 170 | "400", 171 | "x", 172 | "1", 173 | "ref", 174 | "qry", 175 | ] 176 | ) 177 | ), 178 | snp.Snp( 179 | "\t".join( 180 | [ 181 | "42", 182 | ".", 183 | "A", 184 | "100", 185 | "x", 186 | "x", 187 | "300", 188 | "400", 189 | "x", 190 | "1", 191 | "ref", 192 | "qry", 193 | ] 194 | ) 195 | ), 196 | snp.Snp( 197 | "\t".join( 198 | [ 199 | "42", 200 | ".", 201 | "A", 202 | "100", 203 | "x", 204 | "x", 205 | "300", 206 | "400", 207 | "x", 208 | "1", 209 | "ref", 210 | "qry", 211 | ] 212 | ) 213 | ), 214 | snp.Snp( 215 | "\t".join( 216 | [ 217 | "42", 218 | ".", 219 | "A", 220 | "100", 221 | "x", 222 | "x", 223 | "300", 224 | "400", 225 | "x", 226 | "1", 227 | "ref", 228 | "qry", 229 | ] 230 | ) 231 | ), 232 | snp.Snp( 233 | "\t".join( 234 | [ 235 | "42", 236 | ".", 237 | "A", 238 | "100", 239 | "x", 240 | "x", 241 | "300", 242 | "400", 243 | "x", 244 | "1", 245 | "ref", 246 | "qry", 247 | ] 248 | ) 249 | ), 250 | snp.Snp( 251 | "\t".join( 252 | [ 253 | "42", 254 | ".", 255 | "A", 256 | "100", 257 | "x", 258 | "x", 259 | "300", 260 | "400", 261 | "x", 262 | "1", 263 | "ref", 264 | "qry", 265 | ] 266 | ) 267 | ), 268 | ] 269 | 270 | to_add = [ 271 | snp.Snp( 272 | "\t".join( 273 | [ 274 | "142", 275 | "A", 276 | ".", 277 | "1000", 278 | "x", 279 | "x", 280 | "2000", 281 | "3000", 282 | "x", 283 | "1", 284 | "ref", 285 | "qry", 286 | ] 287 | ) 288 | ), 289 | snp.Snp( 290 | "\t".join( 291 | [ 292 | "142", 293 | ".", 294 | "A", 295 | "1000", 296 | "x", 297 | "x", 298 | "2000", 299 | "3000", 300 | "x", 301 | "1", 302 | "ref", 303 | "qry", 304 | ] 305 | ) 306 | ), 307 | snp.Snp( 308 | "\t".join( 309 | [ 310 | "43", 311 | "A", 312 | ".", 313 | "100", 314 | "x", 315 | "x", 316 | "300", 317 | "400", 318 | "x", 319 | "1", 320 | "ref2", 321 | "qry", 322 | ] 323 | ) 324 | ), 325 | snp.Snp( 326 | "\t".join( 327 | [ 328 | "43", 329 | "A", 330 | ".", 331 | "100", 332 | "x", 333 | "x", 334 | "300", 335 | "400", 336 | "x", 337 | "1", 338 | "ref", 339 | "qry2", 340 | ] 341 | ) 342 | ), 343 | snp.Snp( 344 | "\t".join( 345 | [ 346 | "44", 347 | "A", 348 | ".", 349 | "100", 350 | "x", 351 | "x", 352 | "300", 353 | "400", 354 | "x", 355 | "1", 356 | "ref", 357 | "qry", 358 | ] 359 | ) 360 | ), 361 | snp.Snp( 362 | "\t".join( 363 | [ 364 | "42", 365 | "A", 366 | ".", 367 | "100", 368 | "x", 369 | "x", 370 | "300", 371 | "400", 372 | "x", 373 | "1", 374 | "ref", 375 | "qry", 376 | ] 377 | ) 378 | ), 379 | snp.Snp( 380 | "\t".join( 381 | [ 382 | "43", 383 | ".", 384 | "A", 385 | "100", 386 | "x", 387 | "x", 388 | "300", 389 | "400", 390 | "x", 391 | "1", 392 | "ref", 393 | "qry", 394 | ] 395 | ) 396 | ), 397 | snp.Snp( 398 | "\t".join( 399 | [ 400 | "43", 401 | ".", 402 | "A", 403 | "100", 404 | "x", 405 | "x", 406 | "300", 407 | "400", 408 | "x", 409 | "1", 410 | "ref2", 411 | "qry", 412 | ] 413 | ) 414 | ), 415 | snp.Snp( 416 | "\t".join( 417 | [ 418 | "43", 419 | ".", 420 | "A", 421 | "100", 422 | "x", 423 | "x", 424 | "300", 425 | "400", 426 | "x", 427 | "1", 428 | "ref", 429 | "qry2", 430 | ] 431 | ) 432 | ), 433 | snp.Snp( 434 | "\t".join( 435 | [ 436 | "44", 437 | ".", 438 | "A", 439 | "100", 440 | "x", 441 | "x", 442 | "300", 443 | "400", 444 | "x", 445 | "1", 446 | "ref", 447 | "qry", 448 | ] 449 | ) 450 | ), 451 | snp.Snp( 452 | "\t".join( 453 | [ 454 | "42", 455 | ".", 456 | "A", 457 | "100", 458 | "x", 459 | "x", 460 | "300", 461 | "400", 462 | "x", 463 | "1", 464 | "ref", 465 | "qry", 466 | ] 467 | ) 468 | ), 469 | snp.Snp( 470 | "\t".join( 471 | [ 472 | "42", 473 | "A", 474 | ".", 475 | "100", 476 | "x", 477 | "x", 478 | "300", 479 | "400", 480 | "x", 481 | "1", 482 | "ref", 483 | "qry", 484 | ] 485 | ) 486 | ), 487 | ] 488 | 489 | assert len(initial_vars) == len(to_add) 490 | 491 | for i in range(len(initial_vars)): 492 | var = variant.Variant(initial_vars[i]) 493 | var_original = copy.copy(var) 494 | self.assertFalse(var.update_indel(to_add[i])) 495 | self.assertEqual(var, var_original) 496 | 497 | def test_update_indel_insertion(self): 498 | """Test update_indel extends insertions correctly""" 499 | insertion = variant.Variant( 500 | snp.Snp( 501 | "\t".join( 502 | [ 503 | "42", 504 | ".", 505 | "A", 506 | "100", 507 | "x", 508 | "x", 509 | "300", 510 | "400", 511 | "x", 512 | "-1", 513 | "ref", 514 | "qry", 515 | ] 516 | ) 517 | ) 518 | ) 519 | to_add = snp.Snp( 520 | "\t".join( 521 | ["42", ".", "C", "101", "x", "x", "300", "400", "x", "-1", "ref", "qry"] 522 | ) 523 | ) 524 | expected = copy.copy(insertion) 525 | # coords stored zero-based, so subtract 1 from the real expected coords 526 | expected.ref_start = 41 527 | expected.ref_end = 41 528 | expected.ref_length = 300 529 | expected.ref_name = "ref" 530 | expected.ref_base = "." 531 | expected.qry_start = 99 532 | expected.qry_end = 100 533 | expected.qry_length = 400 534 | expected.qry_name = "qry" 535 | expected.qry_base = "AC" 536 | self.assertTrue(insertion.update_indel(to_add)) 537 | self.assertEqual(expected, insertion) 538 | 539 | def test_update_indel_deletion(self): 540 | """Test update_indel extends deletions correctly""" 541 | deletion = variant.Variant( 542 | snp.Snp( 543 | "\t".join( 544 | [ 545 | "42", 546 | "A", 547 | ".", 548 | "100", 549 | "x", 550 | "x", 551 | "300", 552 | "400", 553 | "x", 554 | "1", 555 | "ref", 556 | "qry", 557 | ] 558 | ) 559 | ) 560 | ) 561 | to_add = snp.Snp( 562 | "\t".join( 563 | ["43", "C", ".", "100", "x", "x", "300", "400", "x", "1", "ref", "qry"] 564 | ) 565 | ) 566 | expected = copy.copy(deletion) 567 | # coords stored zero-based, so subtract 1 from the real expected coords 568 | expected.ref_start = 41 569 | expected.ref_end = 42 570 | expected.ref_length = 300 571 | expected.ref_name = "ref" 572 | expected.ref_base = "AC" 573 | expected.qry_start = 99 574 | expected.qry_end = 99 575 | expected.qry_length = 400 576 | expected.qry_name = "qry" 577 | expected.qry_base = "." 578 | self.assertTrue(deletion.update_indel(to_add)) 579 | self.assertEqual(expected, deletion) 580 | -------------------------------------------------------------------------------- /tests/alignment_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import pyfastaq 4 | from pymummer import alignment, snp, variant 5 | 6 | data_dir = "tests/data" 7 | 8 | 9 | class TestNucmer(unittest.TestCase): 10 | def test_init_nucmer(self): 11 | """test __init__ nucmer""" 12 | line = "\t".join( 13 | [ 14 | "1", 15 | "100", 16 | "2", 17 | "200", 18 | "101", 19 | "202", 20 | "42.42", 21 | "123", 22 | "456", 23 | "-1", 24 | "0", 25 | "ref", 26 | "qry", 27 | "[FOO]", 28 | ] 29 | ) 30 | a = alignment.Alignment(line) 31 | self.assertEqual(a.ref_start, 0) 32 | self.assertEqual(a.ref_end, 99) 33 | self.assertEqual(a.qry_start, 1) 34 | self.assertEqual(a.qry_end, 199) 35 | self.assertEqual(a.hit_length_ref, 101) 36 | self.assertEqual(a.hit_length_qry, 202) 37 | self.assertEqual(a.percent_identity, 42.42) 38 | self.assertEqual(a.ref_length, 123) 39 | self.assertEqual(a.qry_length, 456) 40 | self.assertEqual(a.frame, -1) 41 | self.assertEqual(a.ref_name, "ref") 42 | self.assertEqual(a.qry_name, "qry") 43 | 44 | def test_init_promer(self): 45 | """test __init__ promer""" 46 | line = "\t".join( 47 | [ 48 | "1", 49 | "1398", 50 | "4891054", 51 | "4892445", 52 | "1398", 53 | "1392", 54 | "89.55", 55 | "93.18", 56 | "0.21", 57 | "1398", 58 | "5349013", 59 | "1", 60 | "1", 61 | "ref", 62 | "qry", 63 | "[CONTAINED]", 64 | ] 65 | ) 66 | 67 | a = alignment.Alignment(line) 68 | self.assertEqual(a.ref_start, 0) 69 | self.assertEqual(a.ref_end, 1397) 70 | self.assertEqual(a.qry_start, 4891053) 71 | self.assertEqual(a.qry_end, 4892444) 72 | self.assertEqual(a.hit_length_ref, 1398) 73 | self.assertEqual(a.hit_length_qry, 1392) 74 | self.assertEqual(a.percent_identity, 89.55) 75 | self.assertEqual(a.ref_length, 1398) 76 | self.assertEqual(a.qry_length, 5349013) 77 | self.assertEqual(a.frame, 1) 78 | self.assertEqual(a.ref_name, "ref") 79 | self.assertEqual(a.qry_name, "qry") 80 | 81 | def test_swap(self): 82 | """test swap""" 83 | l_in = [ 84 | "1", 85 | "100", 86 | "2", 87 | "200", 88 | "101", 89 | "202", 90 | "42.42", 91 | "123", 92 | "456", 93 | "-1", 94 | "0", 95 | "ref", 96 | "qry", 97 | ] 98 | l_out = [ 99 | "2", 100 | "200", 101 | "1", 102 | "100", 103 | "202", 104 | "101", 105 | "42.42", 106 | "456", 107 | "123", 108 | "-1", 109 | "0", 110 | "qry", 111 | "ref", 112 | ] 113 | a_in = alignment.Alignment("\t".join(l_in)) 114 | a_in._swap() 115 | self.assertEqual(a_in, alignment.Alignment("\t".join(l_out))) 116 | a_in._swap() 117 | self.assertEqual(a_in, alignment.Alignment("\t".join(l_in))) 118 | 119 | def test_qry_coords(self): 120 | """Test qry_coords""" 121 | hits = [ 122 | "\t".join( 123 | [ 124 | "1", 125 | "100", 126 | "1", 127 | "100", 128 | "100", 129 | "100", 130 | "100.00", 131 | "1000", 132 | "1000", 133 | "1", 134 | "1", 135 | "ref", 136 | "qry", 137 | ] 138 | ), 139 | "\t".join( 140 | [ 141 | "1", 142 | "101", 143 | "100", 144 | "1", 145 | "100", 146 | "100", 147 | "100.00", 148 | "1000", 149 | "1000", 150 | "1", 151 | "1", 152 | "ref", 153 | "qry", 154 | ] 155 | ), 156 | ] 157 | for h in hits: 158 | a = alignment.Alignment(h) 159 | self.assertEqual(pyfastaq.intervals.Interval(0, 99), a.qry_coords()) 160 | 161 | def test_ref_coords(self): 162 | """Test ref_coords""" 163 | hits = [ 164 | "\t".join( 165 | [ 166 | "1", 167 | "100", 168 | "1", 169 | "100", 170 | "100", 171 | "100", 172 | "100.00", 173 | "1000", 174 | "1000", 175 | "1", 176 | "1", 177 | "ref", 178 | "ref", 179 | ] 180 | ), 181 | "\t".join( 182 | [ 183 | "100", 184 | "1", 185 | "100", 186 | "1", 187 | "100", 188 | "100", 189 | "100.00", 190 | "1000", 191 | "1000", 192 | "1", 193 | "1", 194 | "ref", 195 | "ref", 196 | ] 197 | ), 198 | ] 199 | for h in hits: 200 | a = alignment.Alignment(h) 201 | self.assertEqual(pyfastaq.intervals.Interval(0, 99), a.ref_coords()) 202 | 203 | def test_on_same_strand(self): 204 | """test on_same_strand""" 205 | self.assertTrue( 206 | alignment.Alignment( 207 | "\t".join( 208 | [ 209 | "1", 210 | "100", 211 | "1", 212 | "100", 213 | "100", 214 | "100", 215 | "100.00", 216 | "1000", 217 | "1000", 218 | "1", 219 | "1", 220 | "ref", 221 | "ref", 222 | ] 223 | ) 224 | ).on_same_strand() 225 | ) 226 | self.assertTrue( 227 | alignment.Alignment( 228 | "\t".join( 229 | [ 230 | "100", 231 | "1", 232 | "100", 233 | "1", 234 | "100", 235 | "100", 236 | "100.00", 237 | "1000", 238 | "1000", 239 | "1", 240 | "1", 241 | "ref", 242 | "ref", 243 | ] 244 | ) 245 | ).on_same_strand() 246 | ) 247 | self.assertFalse( 248 | alignment.Alignment( 249 | "\t".join( 250 | [ 251 | "1", 252 | "100", 253 | "100", 254 | "1", 255 | "100", 256 | "100", 257 | "100.00", 258 | "1000", 259 | "1000", 260 | "1", 261 | "1", 262 | "ref", 263 | "ref", 264 | ] 265 | ) 266 | ).on_same_strand() 267 | ) 268 | self.assertFalse( 269 | alignment.Alignment( 270 | "\t".join( 271 | [ 272 | "100", 273 | "1", 274 | "1", 275 | "100", 276 | "100", 277 | "100", 278 | "100.00", 279 | "1000", 280 | "1000", 281 | "1", 282 | "1", 283 | "ref", 284 | "ref", 285 | ] 286 | ) 287 | ).on_same_strand() 288 | ) 289 | 290 | def test_is_self_hit(self): 291 | """Test is_self_hit""" 292 | tests = [ 293 | ( 294 | "\t".join( 295 | [ 296 | "1", 297 | "100", 298 | "1", 299 | "100", 300 | "100", 301 | "100", 302 | "100.00", 303 | "1000", 304 | "1000", 305 | "1", 306 | "1", 307 | "ref", 308 | "ref", 309 | ] 310 | ), 311 | True, 312 | ), 313 | ( 314 | "\t".join( 315 | [ 316 | "1", 317 | "101", 318 | "1", 319 | "100", 320 | "100", 321 | "100", 322 | "100.00", 323 | "1000", 324 | "1000", 325 | "1", 326 | "1", 327 | "ref", 328 | "ref", 329 | ] 330 | ), 331 | False, 332 | ), 333 | ( 334 | "\t".join( 335 | [ 336 | "2", 337 | "100", 338 | "1", 339 | "100", 340 | "100", 341 | "100", 342 | "100.00", 343 | "1000", 344 | "1000", 345 | "1", 346 | "1", 347 | "ref", 348 | "ref", 349 | ] 350 | ), 351 | False, 352 | ), 353 | ( 354 | "\t".join( 355 | [ 356 | "1", 357 | "100", 358 | "1", 359 | "100", 360 | "100", 361 | "100", 362 | "100.00", 363 | "1000", 364 | "1000", 365 | "1", 366 | "1", 367 | "ref", 368 | "ref2", 369 | ] 370 | ), 371 | False, 372 | ), 373 | ( 374 | "\t".join( 375 | [ 376 | "1", 377 | "100", 378 | "1", 379 | "100", 380 | "100", 381 | "100", 382 | "99.9", 383 | "1000", 384 | "1000", 385 | "1", 386 | "1", 387 | "ref", 388 | "ref", 389 | ] 390 | ), 391 | False, 392 | ), 393 | ] 394 | 395 | for t in tests: 396 | a = alignment.Alignment(t[0]) 397 | self.assertEqual(a.is_self_hit(), t[1]) 398 | 399 | def test_reverse_reference(self): 400 | """Test reverse_reference""" 401 | aln = alignment.Alignment( 402 | "\t".join( 403 | [ 404 | "100", 405 | "142", 406 | "1", 407 | "42", 408 | "43", 409 | "42", 410 | "100.00", 411 | "150", 412 | "100", 413 | "1", 414 | "1", 415 | "ref", 416 | "qry", 417 | ] 418 | ) 419 | ) 420 | expected = alignment.Alignment( 421 | "\t".join( 422 | [ 423 | "51", 424 | "9", 425 | "1", 426 | "42", 427 | "43", 428 | "42", 429 | "100.00", 430 | "150", 431 | "100", 432 | "1", 433 | "1", 434 | "ref", 435 | "qry", 436 | ] 437 | ) 438 | ) 439 | aln.reverse_reference() 440 | self.assertEqual(expected, aln) 441 | 442 | def test_reverse_query(self): 443 | """Test reverse_query""" 444 | aln = alignment.Alignment( 445 | "\t".join( 446 | [ 447 | "100", 448 | "142", 449 | "1", 450 | "42", 451 | "43", 452 | "42", 453 | "100.00", 454 | "150", 455 | "100", 456 | "1", 457 | "1", 458 | "ref", 459 | "qry", 460 | ] 461 | ) 462 | ) 463 | expected = alignment.Alignment( 464 | "\t".join( 465 | [ 466 | "100", 467 | "142", 468 | "100", 469 | "59", 470 | "43", 471 | "42", 472 | "100.00", 473 | "150", 474 | "100", 475 | "1", 476 | "1", 477 | "ref", 478 | "qry", 479 | ] 480 | ) 481 | ) 482 | aln.reverse_query() 483 | self.assertEqual(expected, aln) 484 | 485 | def test_str(self): 486 | """Test __str__""" 487 | l_in = [ 488 | "1", 489 | "100", 490 | "2", 491 | "200", 492 | "101", 493 | "202", 494 | "42.42", 495 | "123", 496 | "456", 497 | "-1", 498 | "0", 499 | "ref", 500 | "qry", 501 | ] 502 | # the 10th column (counting from zero) is ignored and so not output by __str__ 503 | l_out = l_in[:10] + l_in[11:] 504 | a = alignment.Alignment("\t".join(l_in)) 505 | self.assertEqual(str(a), "\t".join(l_out)) 506 | 507 | def test_to_msp_crunch(self): 508 | """Test to_msp_crunch""" 509 | l_in = [ 510 | "100", 511 | "110", 512 | "1", 513 | "10", 514 | "10", 515 | "11", 516 | "80.00", 517 | "123", 518 | "456", 519 | "-1", 520 | "0", 521 | "ref", 522 | "qry", 523 | ] 524 | a = alignment.Alignment("\t".join(l_in)) 525 | expected = "8 80.00 1 10 qry 100 110 ref" 526 | self.assertEqual(expected, a.to_msp_crunch()) 527 | 528 | def test_intersects_variant(self): 529 | "Test intersects_variant" "" 530 | snp0 = snp.Snp( 531 | "100\tA\t.\t600\t75\t77\t1\t0\t606\t1700\t1\t1\tref\tqry" 532 | ) # 100 in ref, 600 in qry 533 | indel = variant.Variant(snp0) 534 | 535 | aln1 = alignment.Alignment( 536 | "100\t500\t600\t1000\t501\t501\t100.00\t600\t1700\t1\t1\tref\tqry" 537 | ) 538 | aln2 = alignment.Alignment( 539 | "101\t500\t600\t1000\t501\t501\t100.00\t600\t1700\t1\t1\tref\tqry" 540 | ) 541 | aln3 = alignment.Alignment( 542 | "100\t500\t601\t1000\t501\t501\t100.00\t600\t1700\t1\t1\tref\tqry" 543 | ) 544 | aln4 = alignment.Alignment( 545 | "101\t500\t601\t1000\t501\t501\t100.00\t600\t1700\t1\t1\tref\tqry" 546 | ) 547 | 548 | self.assertTrue(aln1.intersects_variant(indel)) 549 | self.assertFalse(aln2.intersects_variant(indel)) 550 | self.assertFalse(aln3.intersects_variant(indel)) 551 | self.assertFalse(aln4.intersects_variant(indel)) 552 | 553 | def test_qry_coords_from_ref_coord_test_bad_ref_coord(self): 554 | """Test qry_coords_from_ref_coord with bad ref coords""" 555 | aln = alignment.Alignment( 556 | "\t".join( 557 | [ 558 | "100", 559 | "200", 560 | "1", 561 | "100", 562 | "100", 563 | "100", 564 | "100.00", 565 | "300", 566 | "300", 567 | "1", 568 | "1", 569 | "ref", 570 | "qry", 571 | ] 572 | ) 573 | ) 574 | with self.assertRaises(alignment.Error): 575 | got = aln.qry_coords_from_ref_coord(98, []) 576 | 577 | with self.assertRaises(alignment.Error): 578 | got = aln.qry_coords_from_ref_coord(200, []) 579 | 580 | def test_qry_coords_from_ref_coord_test_same_strand(self): 581 | """Test qry_coords_from_ref_coord on same strand""" 582 | aln = alignment.Alignment( 583 | "\t".join( 584 | [ 585 | "100", 586 | "200", 587 | "1", 588 | "101", 589 | "100", 590 | "100", 591 | "100.00", 592 | "300", 593 | "300", 594 | "1", 595 | "1", 596 | "ref", 597 | "qry", 598 | ] 599 | ) 600 | ) 601 | snp0 = snp.Snp( 602 | "\t".join( 603 | ["140", "A", "T", "40", "x", "x", "300", "300", "x", "1", "ref", "qry"] 604 | ) 605 | ) # snp 606 | snp0 = variant.Variant(snp0) 607 | snp1 = snp.Snp( 608 | "\t".join( 609 | ["140", "A", ".", "40", "x", "x", "300", "300", "x", "1", "ref", "qry"] 610 | ) 611 | ) # del from qry 612 | snp2 = snp.Snp( 613 | "\t".join( 614 | ["141", "C", ".", "40", "x", "x", "300", "300", "x", "1", "ref", "qry"] 615 | ) 616 | ) # del from qry 617 | del1 = variant.Variant(snp1) 618 | del2 = variant.Variant(snp1) 619 | self.assertTrue(del2.update_indel(snp2)) 620 | snp3 = snp.Snp( 621 | "\t".join( 622 | ["150", ".", "A", "50", "x", "x", "300", "300", "x", "1", "ref", "qry"] 623 | ) 624 | ) # del from ref 625 | snp4 = snp.Snp( 626 | "\t".join( 627 | ["150", ".", "C", "51", "x", "x", "300", "300", "x", "1", "ref", "qry"] 628 | ) 629 | ) # del from ref 630 | snp5 = snp.Snp( 631 | "\t".join( 632 | ["150", ".", "G", "52", "x", "x", "300", "300", "x", "1", "ref", "qry"] 633 | ) 634 | ) # del from ref 635 | ins1 = variant.Variant(snp3) 636 | ins2 = variant.Variant(snp3) 637 | self.assertTrue(ins2.update_indel(snp4)) 638 | self.assertTrue(ins2.update_indel(snp5)) 639 | 640 | tests = [ 641 | (99, [], (0, False)), 642 | (100, [], (1, False)), 643 | (199, [], (100, False)), 644 | (119, [del1], (20, False)), 645 | (149, [], (50, False)), 646 | (149, [del1], (49, False)), 647 | (149, [del2], (48, False)), 648 | (159, [], (60, False)), 649 | (159, [ins1], (61, False)), 650 | (159, [ins2], (63, False)), 651 | (159, [del1, ins1], (60, False)), 652 | (159, [del1, ins2], (62, False)), 653 | (159, [del2, ins1], (59, False)), 654 | (159, [del2, ins2], (61, False)), 655 | (139, [del1], (39, True)), 656 | (139, [snp0], (40, False)), 657 | (149, [ins1], (49, True)), 658 | ] 659 | 660 | for ref_coord, variant_list, expected in tests: 661 | got = aln.qry_coords_from_ref_coord(ref_coord, variant_list) 662 | self.assertEqual(expected, got) 663 | # if we reverse the direction of hit in query and reference, should get the same answer 664 | aln.qry_start, aln.qry_end = aln.qry_end, aln.qry_start 665 | aln.ref_start, aln.ref_end = aln.ref_end, aln.ref_start 666 | got = aln.qry_coords_from_ref_coord(ref_coord, variant_list) 667 | self.assertEqual(expected, got) 668 | aln.qry_start, aln.qry_end = aln.qry_end, aln.qry_start 669 | aln.ref_start, aln.ref_end = aln.ref_end, aln.ref_start 670 | 671 | def test_qry_coords_from_ref_coord_test_different_strand(self): 672 | """Test qry_coords_from_ref_coord on different strand""" 673 | aln = alignment.Alignment( 674 | "\t".join( 675 | [ 676 | "100", 677 | "200", 678 | "101", 679 | "1", 680 | "100", 681 | "100", 682 | "100.00", 683 | "300", 684 | "300", 685 | "1", 686 | "1", 687 | "ref", 688 | "qry", 689 | ] 690 | ) 691 | ) 692 | snp0 = snp.Snp( 693 | "\t".join( 694 | ["140", "A", "T", "40", "x", "x", "300", "300", "x", "1", "ref", "qry"] 695 | ) 696 | ) # snp 697 | snp0 = variant.Variant(snp0) 698 | snp1 = snp.Snp( 699 | "\t".join( 700 | ["140", "A", ".", "40", "x", "x", "300", "300", "x", "1", "ref", "qry"] 701 | ) 702 | ) # del from qry 703 | snp2 = snp.Snp( 704 | "\t".join( 705 | ["141", "C", ".", "40", "x", "x", "300", "300", "x", "1", "ref", "qry"] 706 | ) 707 | ) # del from qry 708 | del1 = variant.Variant(snp1) 709 | del2 = variant.Variant(snp1) 710 | self.assertTrue(del2.update_indel(snp2)) 711 | snp3 = snp.Snp( 712 | "\t".join( 713 | ["150", ".", "A", "50", "x", "x", "300", "300", "x", "1", "ref", "qry"] 714 | ) 715 | ) # del from ref 716 | snp4 = snp.Snp( 717 | "\t".join( 718 | ["150", ".", "C", "51", "x", "x", "300", "300", "x", "1", "ref", "qry"] 719 | ) 720 | ) # del from ref 721 | snp5 = snp.Snp( 722 | "\t".join( 723 | ["150", ".", "G", "52", "x", "x", "300", "300", "x", "1", "ref", "qry"] 724 | ) 725 | ) # del from ref 726 | ins1 = variant.Variant(snp3) 727 | ins2 = variant.Variant(snp3) 728 | self.assertTrue(ins2.update_indel(snp4)) 729 | self.assertTrue(ins2.update_indel(snp5)) 730 | 731 | tests = [ 732 | (99, [], (100, False)), 733 | (100, [], (99, False)), 734 | (199, [], (0, False)), 735 | (119, [], (80, False)), 736 | (119, [del1], (80, False)), 737 | (149, [], (50, False)), 738 | (149, [del1], (51, False)), 739 | (149, [del2], (52, False)), 740 | (159, [], (40, False)), 741 | (159, [ins1], (39, False)), 742 | (159, [ins2], (37, False)), 743 | (159, [del1, ins1], (40, False)), 744 | (159, [del1, ins2], (38, False)), 745 | (159, [del2, ins1], (41, False)), 746 | (159, [del2, ins2], (39, False)), 747 | (139, [del1], (39, True)), 748 | (139, [snp0], (60, False)), 749 | (149, [ins1], (49, True)), 750 | ] 751 | 752 | for ref_coord, variant_list, expected in tests: 753 | got = aln.qry_coords_from_ref_coord(ref_coord, variant_list) 754 | self.assertEqual(expected, got) 755 | # if we reverse the direction of hit in query and reference, should get the same answer 756 | aln.qry_start, aln.qry_end = aln.qry_end, aln.qry_start 757 | aln.ref_start, aln.ref_end = aln.ref_end, aln.ref_start 758 | got = aln.qry_coords_from_ref_coord(ref_coord, variant_list) 759 | self.assertEqual(expected, got) 760 | aln.qry_start, aln.qry_end = aln.qry_end, aln.qry_start 761 | aln.ref_start, aln.ref_end = aln.ref_end, aln.ref_start 762 | 763 | def test_qry_coords_from_ref_coord_when_variant_not_in_nucmer_match(self): 764 | """Test ref_coords_from_qry_coord when variant not in nucmer match""" 765 | aln = alignment.Alignment( 766 | "1\t606\t596\t1201\t606\t606\t100.00\t606\t1700\t1\t1\tref\tqry" 767 | ) 768 | snp0 = snp.Snp("127\tA\t.\t77\t75\t77\t1\t0\t606\t1700\t1\t1\tref\tqry") 769 | indel = variant.Variant(snp0) 770 | self.assertEqual((595, False), aln.qry_coords_from_ref_coord(0, [])) 771 | self.assertEqual((595, False), aln.qry_coords_from_ref_coord(0, [indel])) 772 | self.assertEqual((995, False), aln.qry_coords_from_ref_coord(400, [])) 773 | self.assertEqual((995, False), aln.qry_coords_from_ref_coord(400, [indel])) 774 | self.assertEqual((1200, False), aln.qry_coords_from_ref_coord(605, [])) 775 | self.assertEqual((1200, False), aln.qry_coords_from_ref_coord(605, [indel])) 776 | 777 | def test_ref_coords_from_qry_coord_test_same_strand(self): 778 | """Test ref_coords_from_qry_coord on same strand""" 779 | aln = alignment.Alignment( 780 | "\t".join( 781 | [ 782 | "1", 783 | "101", 784 | "100", 785 | "200", 786 | "100", 787 | "100", 788 | "100.00", 789 | "300", 790 | "300", 791 | "1", 792 | "1", 793 | "ref", 794 | "qry", 795 | ] 796 | ) 797 | ) 798 | snp0 = snp.Snp( 799 | "\t".join( 800 | ["40", "T", "A", "140", "x", "x", "300", "300", "x", "1", "ref", "qry"] 801 | ) 802 | ) # snp 803 | snp0 = variant.Variant(snp0) 804 | snp1 = snp.Snp( 805 | "\t".join( 806 | ["40", ".", "A", "140", "x", "x", "300", "300", "x", "1", "ref", "qry"] 807 | ) 808 | ) # del from ref 809 | snp2 = snp.Snp( 810 | "\t".join( 811 | ["40", ".", "C", "141", "x", "x", "300", "300", "x", "1", "ref", "qry"] 812 | ) 813 | ) # del from ref 814 | del1 = variant.Variant(snp1) 815 | del2 = variant.Variant(snp1) 816 | self.assertTrue(del2.update_indel(snp2)) 817 | snp3 = snp.Snp( 818 | "\t".join( 819 | ["50", "A", ".", "150", "x", "x", "300", "300", "x", "1", "ref", "qry"] 820 | ) 821 | ) # del from qry 822 | snp4 = snp.Snp( 823 | "\t".join( 824 | ["51", "C", ".", "150", "x", "x", "300", "300", "x", "1", "ref", "qry"] 825 | ) 826 | ) # del from qry 827 | snp5 = snp.Snp( 828 | "\t".join( 829 | ["52", "G", ".", "150", "x", "x", "300", "300", "x", "1", "ref", "qry"] 830 | ) 831 | ) # del from qry 832 | ins1 = variant.Variant(snp3) 833 | ins2 = variant.Variant(snp3) 834 | self.assertTrue(ins2.update_indel(snp4)) 835 | self.assertTrue(ins2.update_indel(snp5)) 836 | 837 | tests = [ 838 | (99, [], (0, False)), 839 | (100, [], (1, False)), 840 | (199, [], (100, False)), 841 | (119, [del1], (20, False)), 842 | (149, [], (50, False)), 843 | (149, [del1], (49, False)), 844 | (149, [del2], (48, False)), 845 | (159, [], (60, False)), 846 | (159, [ins1], (61, False)), 847 | (159, [ins2], (63, False)), 848 | (159, [del1, ins1], (60, False)), 849 | (159, [del1, ins2], (62, False)), 850 | (159, [del2, ins1], (59, False)), 851 | (159, [del2, ins2], (61, False)), 852 | (139, [del1], (39, True)), 853 | (139, [snp0], (40, False)), 854 | (149, [ins1], (49, True)), 855 | ] 856 | 857 | for qry_coord, variant_list, expected in tests: 858 | got = aln.ref_coords_from_qry_coord(qry_coord, variant_list) 859 | self.assertEqual(expected, got) 860 | # if we reverse the direction of hit in query and qryerence, should get the same answer 861 | aln.ref_start, aln.ref_end = aln.ref_end, aln.ref_start 862 | aln.qry_start, aln.qry_end = aln.qry_end, aln.qry_start 863 | got = aln.ref_coords_from_qry_coord(qry_coord, variant_list) 864 | self.assertEqual(expected, got) 865 | aln.ref_start, aln.ref_end = aln.ref_end, aln.ref_start 866 | aln.qry_start, aln.qry_end = aln.qry_end, aln.qry_start 867 | 868 | def test_ref_coords_from_qry_coord_when_variant_not_in_nucmer_match(self): 869 | """Test ref_coords_from_qry_coord when variant not in nucmer match""" 870 | aln = alignment.Alignment( 871 | "1\t606\t596\t1201\t606\t606\t100.00\t606\t1700\t1\t1\tref\tqry" 872 | ) 873 | snp0 = snp.Snp("127\tA\t.\t77\t75\t77\t1\t0\t606\t1700\t1\t1\tref\tqry") 874 | indel = variant.Variant(snp0) 875 | self.assertEqual((0, False), aln.ref_coords_from_qry_coord(595, [])) 876 | self.assertEqual((0, False), aln.ref_coords_from_qry_coord(595, [indel])) 877 | self.assertEqual((400, False), aln.ref_coords_from_qry_coord(995, [])) 878 | self.assertEqual((400, False), aln.ref_coords_from_qry_coord(995, [indel])) 879 | self.assertEqual((605, False), aln.ref_coords_from_qry_coord(1200, [])) 880 | self.assertEqual((605, False), aln.ref_coords_from_qry_coord(1200, [indel])) 881 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 - 2017 by Genome Research Ltd. 2 | 3 | This is free software, licensed under: 4 | 5 | GNU GENERAL PUBLIC LICENSE 6 | Version 3, 29 June 2007 7 | 8 | Copyright (C) 2007 Free Software Foundation, Inc. 9 | Everyone is permitted to copy and distribute verbatim copies 10 | of this license document, but changing it is not allowed. 11 | 12 | Preamble 13 | 14 | The GNU General Public License is a free, copyleft license for 15 | software and other kinds of works. 16 | 17 | The licenses for most software and other practical works are designed 18 | to take away your freedom to share and change the works. By contrast, 19 | the GNU General Public License is intended to guarantee your freedom to 20 | share and change all versions of a program--to make sure it remains free 21 | software for all its users. We, the Free Software Foundation, use the 22 | GNU General Public License for most of our software; it applies also to 23 | any other work released this way by its authors. You can apply it to 24 | your programs, too. 25 | 26 | When we speak of free software, we are referring to freedom, not 27 | price. Our General Public Licenses are designed to make sure that you 28 | have the freedom to distribute copies of free software (and charge for 29 | them if you wish), that you receive source code or can get it if you 30 | want it, that you can change the software or use pieces of it in new 31 | free programs, and that you know you can do these things. 32 | 33 | To protect your rights, we need to prevent others from denying you 34 | these rights or asking you to surrender the rights. Therefore, you have 35 | certain responsibilities if you distribute copies of the software, or if 36 | you modify it: responsibilities to respect the freedom of others. 37 | 38 | For example, if you distribute copies of such a program, whether 39 | gratis or for a fee, you must pass on to the recipients the same 40 | freedoms that you received. You must make sure that they, too, receive 41 | or can get the source code. And you must show them these terms so they 42 | know their rights. 43 | 44 | Developers that use the GNU GPL protect your rights with two steps: 45 | (1) assert copyright on the software, and (2) offer you this License 46 | giving you legal permission to copy, distribute and/or modify it. 47 | 48 | For the developers' and authors' protection, the GPL clearly explains 49 | that there is no warranty for this free software. For both users' and 50 | authors' sake, the GPL requires that modified versions be marked as 51 | changed, so that their problems will not be attributed erroneously to 52 | authors of previous versions. 53 | 54 | Some devices are designed to deny users access to install or run 55 | modified versions of the software inside them, although the manufacturer 56 | can do so. This is fundamentally incompatible with the aim of 57 | protecting users' freedom to change the software. The systematic 58 | pattern of such abuse occurs in the area of products for individuals to 59 | use, which is precisely where it is most unacceptable. Therefore, we 60 | have designed this version of the GPL to prohibit the practice for those 61 | products. If such problems arise substantially in other domains, we 62 | stand ready to extend this provision to those domains in future versions 63 | of the GPL, as needed to protect the freedom of users. 64 | 65 | Finally, every program is threatened constantly by software patents. 66 | States should not allow patents to restrict development and use of 67 | software on general-purpose computers, but in those that do, we wish to 68 | avoid the special danger that patents applied to a free program could 69 | make it effectively proprietary. To prevent this, the GPL assures that 70 | patents cannot be used to render the program non-free. 71 | 72 | The precise terms and conditions for copying, distribution and 73 | modification follow. 74 | 75 | TERMS AND CONDITIONS 76 | 77 | 0. Definitions. 78 | 79 | "This License" refers to version 3 of the GNU General Public License. 80 | 81 | "Copyright" also means copyright-like laws that apply to other kinds of 82 | works, such as semiconductor masks. 83 | 84 | "The Program" refers to any copyrightable work licensed under this 85 | License. Each licensee is addressed as "you". "Licensees" and 86 | "recipients" may be individuals or organizations. 87 | 88 | To "modify" a work means to copy from or adapt all or part of the work 89 | in a fashion requiring copyright permission, other than the making of an 90 | exact copy. The resulting work is called a "modified version" of the 91 | earlier work or a work "based on" the earlier work. 92 | 93 | A "covered work" means either the unmodified Program or a work based 94 | on the Program. 95 | 96 | To "propagate" a work means to do anything with it that, without 97 | permission, would make you directly or secondarily liable for 98 | infringement under applicable copyright law, except executing it on a 99 | computer or modifying a private copy. Propagation includes copying, 100 | distribution (with or without modification), making available to the 101 | public, and in some countries other activities as well. 102 | 103 | To "convey" a work means any kind of propagation that enables other 104 | parties to make or receive copies. Mere interaction with a user through 105 | a computer network, with no transfer of a copy, is not conveying. 106 | 107 | An interactive user interface displays "Appropriate Legal Notices" 108 | to the extent that it includes a convenient and prominently visible 109 | feature that (1) displays an appropriate copyright notice, and (2) 110 | tells the user that there is no warranty for the work (except to the 111 | extent that warranties are provided), that licensees may convey the 112 | work under this License, and how to view a copy of this License. If 113 | the interface presents a list of user commands or options, such as a 114 | menu, a prominent item in the list meets this criterion. 115 | 116 | 1. Source Code. 117 | 118 | The "source code" for a work means the preferred form of the work 119 | for making modifications to it. "Object code" means any non-source 120 | form of a work. 121 | 122 | A "Standard Interface" means an interface that either is an official 123 | standard defined by a recognized standards body, or, in the case of 124 | interfaces specified for a particular programming language, one that 125 | is widely used among developers working in that language. 126 | 127 | The "System Libraries" of an executable work include anything, other 128 | than the work as a whole, that (a) is included in the normal form of 129 | packaging a Major Component, but which is not part of that Major 130 | Component, and (b) serves only to enable use of the work with that 131 | Major Component, or to implement a Standard Interface for which an 132 | implementation is available to the public in source code form. A 133 | "Major Component", in this context, means a major essential component 134 | (kernel, window system, and so on) of the specific operating system 135 | (if any) on which the executable work runs, or a compiler used to 136 | produce the work, or an object code interpreter used to run it. 137 | 138 | The "Corresponding Source" for a work in object code form means all 139 | the source code needed to generate, install, and (for an executable 140 | work) run the object code and to modify the work, including scripts to 141 | control those activities. However, it does not include the work's 142 | System Libraries, or general-purpose tools or generally available free 143 | programs which are used unmodified in performing those activities but 144 | which are not part of the work. For example, Corresponding Source 145 | includes interface definition files associated with source files for 146 | the work, and the source code for shared libraries and dynamically 147 | linked subprograms that the work is specifically designed to require, 148 | such as by intimate data communication or control flow between those 149 | subprograms and other parts of the work. 150 | 151 | The Corresponding Source need not include anything that users 152 | can regenerate automatically from other parts of the Corresponding 153 | Source. 154 | 155 | The Corresponding Source for a work in source code form is that 156 | same work. 157 | 158 | 2. Basic Permissions. 159 | 160 | All rights granted under this License are granted for the term of 161 | copyright on the Program, and are irrevocable provided the stated 162 | conditions are met. This License explicitly affirms your unlimited 163 | permission to run the unmodified Program. The output from running a 164 | covered work is covered by this License only if the output, given its 165 | content, constitutes a covered work. This License acknowledges your 166 | rights of fair use or other equivalent, as provided by copyright law. 167 | 168 | You may make, run and propagate covered works that you do not 169 | convey, without conditions so long as your license otherwise remains 170 | in force. You may convey covered works to others for the sole purpose 171 | of having them make modifications exclusively for you, or provide you 172 | with facilities for running those works, provided that you comply with 173 | the terms of this License in conveying all material for which you do 174 | not control copyright. Those thus making or running the covered works 175 | for you must do so exclusively on your behalf, under your direction 176 | and control, on terms that prohibit them from making any copies of 177 | your copyrighted material outside their relationship with you. 178 | 179 | Conveying under any other circumstances is permitted solely under 180 | the conditions stated below. Sublicensing is not allowed; section 10 181 | makes it unnecessary. 182 | 183 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 184 | 185 | No covered work shall be deemed part of an effective technological 186 | measure under any applicable law fulfilling obligations under article 187 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 188 | similar laws prohibiting or restricting circumvention of such 189 | measures. 190 | 191 | When you convey a covered work, you waive any legal power to forbid 192 | circumvention of technological measures to the extent such circumvention 193 | is effected by exercising rights under this License with respect to 194 | the covered work, and you disclaim any intention to limit operation or 195 | modification of the work as a means of enforcing, against the work's 196 | users, your or third parties' legal rights to forbid circumvention of 197 | technological measures. 198 | 199 | 4. Conveying Verbatim Copies. 200 | 201 | You may convey verbatim copies of the Program's source code as you 202 | receive it, in any medium, provided that you conspicuously and 203 | appropriately publish on each copy an appropriate copyright notice; 204 | keep intact all notices stating that this License and any 205 | non-permissive terms added in accord with section 7 apply to the code; 206 | keep intact all notices of the absence of any warranty; and give all 207 | recipients a copy of this License along with the Program. 208 | 209 | You may charge any price or no price for each copy that you convey, 210 | and you may offer support or warranty protection for a fee. 211 | 212 | 5. Conveying Modified Source Versions. 213 | 214 | You may convey a work based on the Program, or the modifications to 215 | produce it from the Program, in the form of source code under the 216 | terms of section 4, provided that you also meet all of these conditions: 217 | 218 | a) The work must carry prominent notices stating that you modified 219 | it, and giving a relevant date. 220 | 221 | b) The work must carry prominent notices stating that it is 222 | released under this License and any conditions added under section 223 | 7. This requirement modifies the requirement in section 4 to 224 | "keep intact all notices". 225 | 226 | c) You must license the entire work, as a whole, under this 227 | License to anyone who comes into possession of a copy. This 228 | License will therefore apply, along with any applicable section 7 229 | additional terms, to the whole of the work, and all its parts, 230 | regardless of how they are packaged. This License gives no 231 | permission to license the work in any other way, but it does not 232 | invalidate such permission if you have separately received it. 233 | 234 | d) If the work has interactive user interfaces, each must display 235 | Appropriate Legal Notices; however, if the Program has interactive 236 | interfaces that do not display Appropriate Legal Notices, your 237 | work need not make them do so. 238 | 239 | A compilation of a covered work with other separate and independent 240 | works, which are not by their nature extensions of the covered work, 241 | and which are not combined with it such as to form a larger program, 242 | in or on a volume of a storage or distribution medium, is called an 243 | "aggregate" if the compilation and its resulting copyright are not 244 | used to limit the access or legal rights of the compilation's users 245 | beyond what the individual works permit. Inclusion of a covered work 246 | in an aggregate does not cause this License to apply to the other 247 | parts of the aggregate. 248 | 249 | 6. Conveying Non-Source Forms. 250 | 251 | You may convey a covered work in object code form under the terms 252 | of sections 4 and 5, provided that you also convey the 253 | machine-readable Corresponding Source under the terms of this License, 254 | in one of these ways: 255 | 256 | a) Convey the object code in, or embodied in, a physical product 257 | (including a physical distribution medium), accompanied by the 258 | Corresponding Source fixed on a durable physical medium 259 | customarily used for software interchange. 260 | 261 | b) Convey the object code in, or embodied in, a physical product 262 | (including a physical distribution medium), accompanied by a 263 | written offer, valid for at least three years and valid for as 264 | long as you offer spare parts or customer support for that product 265 | model, to give anyone who possesses the object code either (1) a 266 | copy of the Corresponding Source for all the software in the 267 | product that is covered by this License, on a durable physical 268 | medium customarily used for software interchange, for a price no 269 | more than your reasonable cost of physically performing this 270 | conveying of source, or (2) access to copy the 271 | Corresponding Source from a network server at no charge. 272 | 273 | c) Convey individual copies of the object code with a copy of the 274 | written offer to provide the Corresponding Source. This 275 | alternative is allowed only occasionally and noncommercially, and 276 | only if you received the object code with such an offer, in accord 277 | with subsection 6b. 278 | 279 | d) Convey the object code by offering access from a designated 280 | place (gratis or for a charge), and offer equivalent access to the 281 | Corresponding Source in the same way through the same place at no 282 | further charge. You need not require recipients to copy the 283 | Corresponding Source along with the object code. If the place to 284 | copy the object code is a network server, the Corresponding Source 285 | may be on a different server (operated by you or a third party) 286 | that supports equivalent copying facilities, provided you maintain 287 | clear directions next to the object code saying where to find the 288 | Corresponding Source. Regardless of what server hosts the 289 | Corresponding Source, you remain obligated to ensure that it is 290 | available for as long as needed to satisfy these requirements. 291 | 292 | e) Convey the object code using peer-to-peer transmission, provided 293 | you inform other peers where the object code and Corresponding 294 | Source of the work are being offered to the general public at no 295 | charge under subsection 6d. 296 | 297 | A separable portion of the object code, whose source code is excluded 298 | from the Corresponding Source as a System Library, need not be 299 | included in conveying the object code work. 300 | 301 | A "User Product" is either (1) a "consumer product", which means any 302 | tangible personal property which is normally used for personal, family, 303 | or household purposes, or (2) anything designed or sold for incorporation 304 | into a dwelling. In determining whether a product is a consumer product, 305 | doubtful cases shall be resolved in favor of coverage. For a particular 306 | product received by a particular user, "normally used" refers to a 307 | typical or common use of that class of product, regardless of the status 308 | of the particular user or of the way in which the particular user 309 | actually uses, or expects or is expected to use, the product. A product 310 | is a consumer product regardless of whether the product has substantial 311 | commercial, industrial or non-consumer uses, unless such uses represent 312 | the only significant mode of use of the product. 313 | 314 | "Installation Information" for a User Product means any methods, 315 | procedures, authorization keys, or other information required to install 316 | and execute modified versions of a covered work in that User Product from 317 | a modified version of its Corresponding Source. The information must 318 | suffice to ensure that the continued functioning of the modified object 319 | code is in no case prevented or interfered with solely because 320 | modification has been made. 321 | 322 | If you convey an object code work under this section in, or with, or 323 | specifically for use in, a User Product, and the conveying occurs as 324 | part of a transaction in which the right of possession and use of the 325 | User Product is transferred to the recipient in perpetuity or for a 326 | fixed term (regardless of how the transaction is characterized), the 327 | Corresponding Source conveyed under this section must be accompanied 328 | by the Installation Information. But this requirement does not apply 329 | if neither you nor any third party retains the ability to install 330 | modified object code on the User Product (for example, the work has 331 | been installed in ROM). 332 | 333 | The requirement to provide Installation Information does not include a 334 | requirement to continue to provide support service, warranty, or updates 335 | for a work that has been modified or installed by the recipient, or for 336 | the User Product in which it has been modified or installed. Access to a 337 | network may be denied when the modification itself materially and 338 | adversely affects the operation of the network or violates the rules and 339 | protocols for communication across the network. 340 | 341 | Corresponding Source conveyed, and Installation Information provided, 342 | in accord with this section must be in a format that is publicly 343 | documented (and with an implementation available to the public in 344 | source code form), and must require no special password or key for 345 | unpacking, reading or copying. 346 | 347 | 7. Additional Terms. 348 | 349 | "Additional permissions" are terms that supplement the terms of this 350 | License by making exceptions from one or more of its conditions. 351 | Additional permissions that are applicable to the entire Program shall 352 | be treated as though they were included in this License, to the extent 353 | that they are valid under applicable law. If additional permissions 354 | apply only to part of the Program, that part may be used separately 355 | under those permissions, but the entire Program remains governed by 356 | this License without regard to the additional permissions. 357 | 358 | When you convey a copy of a covered work, you may at your option 359 | remove any additional permissions from that copy, or from any part of 360 | it. (Additional permissions may be written to require their own 361 | removal in certain cases when you modify the work.) You may place 362 | additional permissions on material, added by you to a covered work, 363 | for which you have or can give appropriate copyright permission. 364 | 365 | Notwithstanding any other provision of this License, for material you 366 | add to a covered work, you may (if authorized by the copyright holders of 367 | that material) supplement the terms of this License with terms: 368 | 369 | a) Disclaiming warranty or limiting liability differently from the 370 | terms of sections 15 and 16 of this License; or 371 | 372 | b) Requiring preservation of specified reasonable legal notices or 373 | author attributions in that material or in the Appropriate Legal 374 | Notices displayed by works containing it; or 375 | 376 | c) Prohibiting misrepresentation of the origin of that material, or 377 | requiring that modified versions of such material be marked in 378 | reasonable ways as different from the original version; or 379 | 380 | d) Limiting the use for publicity purposes of names of licensors or 381 | authors of the material; or 382 | 383 | e) Declining to grant rights under trademark law for use of some 384 | trade names, trademarks, or service marks; or 385 | 386 | f) Requiring indemnification of licensors and authors of that 387 | material by anyone who conveys the material (or modified versions of 388 | it) with contractual assumptions of liability to the recipient, for 389 | any liability that these contractual assumptions directly impose on 390 | those licensors and authors. 391 | 392 | All other non-permissive additional terms are considered "further 393 | restrictions" within the meaning of section 10. If the Program as you 394 | received it, or any part of it, contains a notice stating that it is 395 | governed by this License along with a term that is a further 396 | restriction, you may remove that term. If a license document contains 397 | a further restriction but permits relicensing or conveying under this 398 | License, you may add to a covered work material governed by the terms 399 | of that license document, provided that the further restriction does 400 | not survive such relicensing or conveying. 401 | 402 | If you add terms to a covered work in accord with this section, you 403 | must place, in the relevant source files, a statement of the 404 | additional terms that apply to those files, or a notice indicating 405 | where to find the applicable terms. 406 | 407 | Additional terms, permissive or non-permissive, may be stated in the 408 | form of a separately written license, or stated as exceptions; 409 | the above requirements apply either way. 410 | 411 | 8. Termination. 412 | 413 | You may not propagate or modify a covered work except as expressly 414 | provided under this License. Any attempt otherwise to propagate or 415 | modify it is void, and will automatically terminate your rights under 416 | this License (including any patent licenses granted under the third 417 | paragraph of section 11). 418 | 419 | However, if you cease all violation of this License, then your 420 | license from a particular copyright holder is reinstated (a) 421 | provisionally, unless and until the copyright holder explicitly and 422 | finally terminates your license, and (b) permanently, if the copyright 423 | holder fails to notify you of the violation by some reasonable means 424 | prior to 60 days after the cessation. 425 | 426 | Moreover, your license from a particular copyright holder is 427 | reinstated permanently if the copyright holder notifies you of the 428 | violation by some reasonable means, this is the first time you have 429 | received notice of violation of this License (for any work) from that 430 | copyright holder, and you cure the violation prior to 30 days after 431 | your receipt of the notice. 432 | 433 | Termination of your rights under this section does not terminate the 434 | licenses of parties who have received copies or rights from you under 435 | this License. If your rights have been terminated and not permanently 436 | reinstated, you do not qualify to receive new licenses for the same 437 | material under section 10. 438 | 439 | 9. Acceptance Not Required for Having Copies. 440 | 441 | You are not required to accept this License in order to receive or 442 | run a copy of the Program. Ancillary propagation of a covered work 443 | occurring solely as a consequence of using peer-to-peer transmission 444 | to receive a copy likewise does not require acceptance. However, 445 | nothing other than this License grants you permission to propagate or 446 | modify any covered work. These actions infringe copyright if you do 447 | not accept this License. Therefore, by modifying or propagating a 448 | covered work, you indicate your acceptance of this License to do so. 449 | 450 | 10. Automatic Licensing of Downstream Recipients. 451 | 452 | Each time you convey a covered work, the recipient automatically 453 | receives a license from the original licensors, to run, modify and 454 | propagate that work, subject to this License. You are not responsible 455 | for enforcing compliance by third parties with this License. 456 | 457 | An "entity transaction" is a transaction transferring control of an 458 | organization, or substantially all assets of one, or subdividing an 459 | organization, or merging organizations. If propagation of a covered 460 | work results from an entity transaction, each party to that 461 | transaction who receives a copy of the work also receives whatever 462 | licenses to the work the party's predecessor in interest had or could 463 | give under the previous paragraph, plus a right to possession of the 464 | Corresponding Source of the work from the predecessor in interest, if 465 | the predecessor has it or can get it with reasonable efforts. 466 | 467 | You may not impose any further restrictions on the exercise of the 468 | rights granted or affirmed under this License. For example, you may 469 | not impose a license fee, royalty, or other charge for exercise of 470 | rights granted under this License, and you may not initiate litigation 471 | (including a cross-claim or counterclaim in a lawsuit) alleging that 472 | any patent claim is infringed by making, using, selling, offering for 473 | sale, or importing the Program or any portion of it. 474 | 475 | 11. Patents. 476 | 477 | A "contributor" is a copyright holder who authorizes use under this 478 | License of the Program or a work on which the Program is based. The 479 | work thus licensed is called the contributor's "contributor version". 480 | 481 | A contributor's "essential patent claims" are all patent claims 482 | owned or controlled by the contributor, whether already acquired or 483 | hereafter acquired, that would be infringed by some manner, permitted 484 | by this License, of making, using, or selling its contributor version, 485 | but do not include claims that would be infringed only as a 486 | consequence of further modification of the contributor version. For 487 | purposes of this definition, "control" includes the right to grant 488 | patent sublicenses in a manner consistent with the requirements of 489 | this License. 490 | 491 | Each contributor grants you a non-exclusive, worldwide, royalty-free 492 | patent license under the contributor's essential patent claims, to 493 | make, use, sell, offer for sale, import and otherwise run, modify and 494 | propagate the contents of its contributor version. 495 | 496 | In the following three paragraphs, a "patent license" is any express 497 | agreement or commitment, however denominated, not to enforce a patent 498 | (such as an express permission to practice a patent or covenant not to 499 | sue for patent infringement). To "grant" such a patent license to a 500 | party means to make such an agreement or commitment not to enforce a 501 | patent against the party. 502 | 503 | If you convey a covered work, knowingly relying on a patent license, 504 | and the Corresponding Source of the work is not available for anyone 505 | to copy, free of charge and under the terms of this License, through a 506 | publicly available network server or other readily accessible means, 507 | then you must either (1) cause the Corresponding Source to be so 508 | available, or (2) arrange to deprive yourself of the benefit of the 509 | patent license for this particular work, or (3) arrange, in a manner 510 | consistent with the requirements of this License, to extend the patent 511 | license to downstream recipients. "Knowingly relying" means you have 512 | actual knowledge that, but for the patent license, your conveying the 513 | covered work in a country, or your recipient's use of the covered work 514 | in a country, would infringe one or more identifiable patents in that 515 | country that you have reason to believe are valid. 516 | 517 | If, pursuant to or in connection with a single transaction or 518 | arrangement, you convey, or propagate by procuring conveyance of, a 519 | covered work, and grant a patent license to some of the parties 520 | receiving the covered work authorizing them to use, propagate, modify 521 | or convey a specific copy of the covered work, then the patent license 522 | you grant is automatically extended to all recipients of the covered 523 | work and works based on it. 524 | 525 | A patent license is "discriminatory" if it does not include within 526 | the scope of its coverage, prohibits the exercise of, or is 527 | conditioned on the non-exercise of one or more of the rights that are 528 | specifically granted under this License. You may not convey a covered 529 | work if you are a party to an arrangement with a third party that is 530 | in the business of distributing software, under which you make payment 531 | to the third party based on the extent of your activity of conveying 532 | the work, and under which the third party grants, to any of the 533 | parties who would receive the covered work from you, a discriminatory 534 | patent license (a) in connection with copies of the covered work 535 | conveyed by you (or copies made from those copies), or (b) primarily 536 | for and in connection with specific products or compilations that 537 | contain the covered work, unless you entered into that arrangement, 538 | or that patent license was granted, prior to 28 March 2007. 539 | 540 | Nothing in this License shall be construed as excluding or limiting 541 | any implied license or other defenses to infringement that may 542 | otherwise be available to you under applicable patent law. 543 | 544 | 12. No Surrender of Others' Freedom. 545 | 546 | If conditions are imposed on you (whether by court order, agreement or 547 | otherwise) that contradict the conditions of this License, they do not 548 | excuse you from the conditions of this License. If you cannot convey a 549 | covered work so as to satisfy simultaneously your obligations under this 550 | License and any other pertinent obligations, then as a consequence you may 551 | not convey it at all. For example, if you agree to terms that obligate you 552 | to collect a royalty for further conveying from those to whom you convey 553 | the Program, the only way you could satisfy both those terms and this 554 | License would be to refrain entirely from conveying the Program. 555 | 556 | 13. Use with the GNU Affero General Public License. 557 | 558 | Notwithstanding any other provision of this License, you have 559 | permission to link or combine any covered work with a work licensed 560 | under version 3 of the GNU Affero General Public License into a single 561 | combined work, and to convey the resulting work. The terms of this 562 | License will continue to apply to the part which is the covered work, 563 | but the special requirements of the GNU Affero General Public License, 564 | section 13, concerning interaction through a network will apply to the 565 | combination as such. 566 | 567 | 14. Revised Versions of this License. 568 | 569 | The Free Software Foundation may publish revised and/or new versions of 570 | the GNU General Public License from time to time. Such new versions will 571 | be similar in spirit to the present version, but may differ in detail to 572 | address new problems or concerns. 573 | 574 | Each version is given a distinguishing version number. If the 575 | Program specifies that a certain numbered version of the GNU General 576 | Public License "or any later version" applies to it, you have the 577 | option of following the terms and conditions either of that numbered 578 | version or of any later version published by the Free Software 579 | Foundation. If the Program does not specify a version number of the 580 | GNU General Public License, you may choose any version ever published 581 | by the Free Software Foundation. 582 | 583 | If the Program specifies that a proxy can decide which future 584 | versions of the GNU General Public License can be used, that proxy's 585 | public statement of acceptance of a version permanently authorizes you 586 | to choose that version for the Program. 587 | 588 | Later license versions may give you additional or different 589 | permissions. However, no additional obligations are imposed on any 590 | author or copyright holder as a result of your choosing to follow a 591 | later version. 592 | 593 | 15. Disclaimer of Warranty. 594 | 595 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 596 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 597 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 598 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 599 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 600 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 601 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 602 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 603 | 604 | 16. Limitation of Liability. 605 | 606 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 607 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 608 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 609 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 610 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 611 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 612 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 613 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 614 | SUCH DAMAGES. 615 | 616 | 17. Interpretation of Sections 15 and 16. 617 | 618 | If the disclaimer of warranty and limitation of liability provided 619 | above cannot be given local legal effect according to their terms, 620 | reviewing courts shall apply local law that most closely approximates 621 | an absolute waiver of all civil liability in connection with the 622 | Program, unless a warranty or assumption of liability accompanies a 623 | copy of the Program in return for a fee. 624 | 625 | END OF TERMS AND CONDITIONS 626 | 627 | How to Apply These Terms to Your New Programs 628 | 629 | If you develop a new program, and you want it to be of the greatest 630 | possible use to the public, the best way to achieve this is to make it 631 | free software which everyone can redistribute and change under these terms. 632 | 633 | To do so, attach the following notices to the program. It is safest 634 | to attach them to the start of each source file to most effectively 635 | state the exclusion of warranty; and each file should have at least 636 | the "copyright" line and a pointer to where the full notice is found. 637 | 638 | {one line to give the program's name and a brief idea of what it does.} 639 | Copyright (C) {year} {name of author} 640 | 641 | This program is free software: you can redistribute it and/or modify 642 | it under the terms of the GNU General Public License as published by 643 | the Free Software Foundation, either version 3 of the License, or 644 | (at your option) any later version. 645 | 646 | This program is distributed in the hope that it will be useful, 647 | but WITHOUT ANY WARRANTY; without even the implied warranty of 648 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 649 | GNU General Public License for more details. 650 | 651 | You should have received a copy of the GNU General Public License 652 | along with this program. If not, see . 653 | 654 | Also add information on how to contact you by electronic and paper mail. 655 | 656 | If the program does terminal interaction, make it output a short 657 | notice like this when it starts in an interactive mode: 658 | 659 | {project} Copyright (C) {year} {fullname} 660 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 661 | This is free software, and you are welcome to redistribute it 662 | under certain conditions; type `show c' for details. 663 | 664 | The hypothetical commands `show w' and `show c' should show the appropriate 665 | parts of the General Public License. Of course, your program's commands 666 | might be different; for a GUI interface, you would use an "about box". 667 | 668 | You should also get your employer (if you work as a programmer) or school, 669 | if any, to sign a "copyright disclaimer" for the program, if necessary. 670 | For more information on this, and how to apply and follow the GNU GPL, see 671 | . 672 | 673 | The GNU General Public License does not permit incorporating your program 674 | into proprietary programs. If your program is a subroutine library, you 675 | may consider it more useful to permit linking proprietary applications with 676 | the library. If this is what you want to do, use the GNU Lesser General 677 | Public License instead of this License. But first, please read 678 | . 679 | 680 | --------------------------------------------------------------------------------