├── 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 | [](https://travis-ci.org/sanger-pathogens/pymummer)
6 | [](https://github.com/sanger-pathogens/pymummer/blob/master/LICENSE)
7 | [](http://bioconda.github.io/recipes/pymummer/README.html)
8 | [](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 |
--------------------------------------------------------------------------------