nonhuman-screen 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonhuman_screen-0.1.0/LICENSE +21 -0
- nonhuman_screen-0.1.0/PKG-INFO +168 -0
- nonhuman_screen-0.1.0/README.md +145 -0
- nonhuman_screen-0.1.0/pyproject.toml +46 -0
- nonhuman_screen-0.1.0/setup.cfg +4 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen/__init__.py +63 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen/alleles.py +85 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen/bam.py +216 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen/cli.py +187 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen/engine.py +828 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen/py.typed +0 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen/result.py +138 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen/votes.py +59 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen.egg-info/PKG-INFO +168 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen.egg-info/SOURCES.txt +22 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen.egg-info/dependency_links.txt +1 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen.egg-info/entry_points.txt +2 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen.egg-info/requires.txt +7 -0
- nonhuman_screen-0.1.0/src/nonhuman_screen.egg-info/top_level.txt +1 -0
- nonhuman_screen-0.1.0/tests/test_alleles.py +68 -0
- nonhuman_screen-0.1.0/tests/test_bam.py +130 -0
- nonhuman_screen-0.1.0/tests/test_engine.py +1528 -0
- nonhuman_screen-0.1.0/tests/test_http_integration.py +61 -0
- nonhuman_screen-0.1.0/tests/test_integration.py +392 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jlanej
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nonhuman-screen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Classify sequencing reads by non-human taxonomic content (kraken2), including the allele-based non-human fraction of reads supporting a variant.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/jlanej/nonhuman-screen
|
|
7
|
+
Project-URL: Documentation, https://github.com/jlanej/nonhuman-screen/tree/main/docs
|
|
8
|
+
Keywords: kraken2,contamination,metagenomics,non-human,taxonomy,variant,bioinformatics,sequencing
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: bam
|
|
18
|
+
Requires-Dist: pysam>=0.21.0; extra == "bam"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
21
|
+
Requires-Dist: pysam>=0.21.0; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# nonhuman-screen
|
|
25
|
+
|
|
26
|
+
Classify sequencing reads by **non-human taxonomic content** using
|
|
27
|
+
[kraken2](https://github.com/DerrickWood/kraken2), and reduce the per-read
|
|
28
|
+
verdicts to a **non-human fraction (NHF)** plus per-domain breakdowns
|
|
29
|
+
(bacterial / archaeal / fungal / protist / viral / UniVec-Core).
|
|
30
|
+
|
|
31
|
+
The headline capability is the **allele-based NHF**: given a BAM/CRAM and a
|
|
32
|
+
variant, compute the non-human fraction of the reads that support that
|
|
33
|
+
variant's ALT allele — in one call, no boilerplate:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from nonhuman_screen import classify_variant_alt_reads
|
|
37
|
+
|
|
38
|
+
v = classify_variant_alt_reads(
|
|
39
|
+
"sample.bam", "kraken2_db", "chr1", 12345, "A", "T", ref_fasta="ref.fa",
|
|
40
|
+
)
|
|
41
|
+
v.nonhuman_fraction # 0.0–1.0 over ALT-supporting reads
|
|
42
|
+
v.fractions.bacterial # per-domain breakdown
|
|
43
|
+
v.supporting_reads # denominator
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`nonhuman-screen` is **sample-agnostic**: it classifies whatever reads you hand
|
|
47
|
+
it and never asks whose they are. The same call works on a proband, a parent, a
|
|
48
|
+
tumour, or a plain FASTQ — so it can back higher-level analyses (e.g. flagging
|
|
49
|
+
inherited variant calls that are actually parental contamination) without
|
|
50
|
+
knowing anything about pedigrees.
|
|
51
|
+
|
|
52
|
+
> Extracted from [`kmer-denovo-filter`](https://github.com/jlanej/kmer_denovo_filter),
|
|
53
|
+
> where this methodology was originally developed, so it can be reused
|
|
54
|
+
> independently.
|
|
55
|
+
|
|
56
|
+
## How it works (in one paragraph)
|
|
57
|
+
|
|
58
|
+
Reads are written to a temporary FASTQ and classified by kraken2's LCA
|
|
59
|
+
algorithm. Each read's assigned taxid is mapped to a domain using the
|
|
60
|
+
database's `nodes.dmp` taxonomy. A read counts as **non-human** only if it is
|
|
61
|
+
classified and lies outside the human lineage, outside the human clade, and
|
|
62
|
+
outside UniVec-Core. Two conservative guards reduce false positives: a
|
|
63
|
+
**human-homology guard** (any k-mer voting for human, taxid 9606, drops the read
|
|
64
|
+
from every non-human numerator — important for integrating viruses such as
|
|
65
|
+
HBV/HPV/ERVs) and **UniVec-Core exclusion** (synthetic vector/adapter sequences,
|
|
66
|
+
taxid 81077, are tracked separately and never counted as contamination). See
|
|
67
|
+
[docs/methodology.md](docs/methodology.md).
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install nonhuman-screen # core engine (standard library only)
|
|
73
|
+
pip install 'nonhuman-screen[bam]' # + BAM/CRAM & allele-based NHF (pysam)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The **`bam` extra is required for the BAM/allele helpers and for the
|
|
77
|
+
`nonhuman-screen` CLI** (both classify modes import pysam). The core install
|
|
78
|
+
covers only `Kraken2Runner.classify_sequences` on in-memory reads.
|
|
79
|
+
|
|
80
|
+
You also need the `kraken2` binary on `PATH` and a kraken2 database — see
|
|
81
|
+
[docs/database.md](docs/database.md).
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
### Allele-based NHF for many variants (batched — one kraken2 call)
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from nonhuman_screen import classify_variants_alt_reads
|
|
89
|
+
|
|
90
|
+
variants = [("chr1", 12345, "A", "T"), ("chr2", 999, "G", "GAC")] # pos 0-based
|
|
91
|
+
for v in classify_variants_alt_reads("sample.bam", "kraken2_db", variants,
|
|
92
|
+
ref_fasta="ref.fa"):
|
|
93
|
+
print(v.variant_key, v.nonhuman_fraction, v.supporting_reads)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The reads supporting all variants' ALT alleles are gathered and classified in a
|
|
97
|
+
**single** kraken2 invocation (its database load dominates runtime), then split
|
|
98
|
+
back per variant. You never re-implement fetch/allele-filter/classify yourself.
|
|
99
|
+
|
|
100
|
+
### Classify an arbitrary set of reads
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from nonhuman_screen import Kraken2Runner
|
|
104
|
+
|
|
105
|
+
result = Kraken2Runner("kraken2_db").classify_sequences(
|
|
106
|
+
{"read1": "ACGT...", "read2": "TTGCA..."}
|
|
107
|
+
)
|
|
108
|
+
result.nonhuman_fraction # 0.0–1.0
|
|
109
|
+
result.fractions() # TaxonomicFractions(nonhuman=…, bacterial=…, …)
|
|
110
|
+
result.taxonomy_available # False if nodes.dmp was missing (fractions unreliable)
|
|
111
|
+
result.nonhuman_read_names # set of read names
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Command line
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Per-variant allele NHF table (headline)
|
|
118
|
+
nonhuman-screen classify \
|
|
119
|
+
--bam sample.bam --kraken2-db kraken2_db --ref-fasta ref.fa \
|
|
120
|
+
--variants candidates.vcf.gz --out-prefix sample_contam
|
|
121
|
+
# -> sample_contam.variant_nhf.tsv, sample_contam.summary.json
|
|
122
|
+
|
|
123
|
+
# Whole-BAM summary
|
|
124
|
+
nonhuman-screen classify --bam sample.bam --kraken2-db kraken2_db
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
See [docs/cli.md](docs/cli.md).
|
|
128
|
+
|
|
129
|
+
## Coordinate convention
|
|
130
|
+
|
|
131
|
+
All `pos` arguments in the Python API are **0-based** (pysam reference
|
|
132
|
+
coordinates); variant keys are `"{chrom}:{pos}:{ref}:{alt}"`. The CLI reads a
|
|
133
|
+
VCF and handles the conversion for you.
|
|
134
|
+
|
|
135
|
+
## API summary
|
|
136
|
+
|
|
137
|
+
| Symbol | Needs `[bam]` | Purpose |
|
|
138
|
+
|---|:---:|---|
|
|
139
|
+
| `Kraken2Runner.classify_sequences(seqs)` | no | Classify `{name: seq}` → `ClassificationResult` |
|
|
140
|
+
| `ClassificationResult` | no | Per-domain read-name sets, counts, `fractions()`, `nonhuman_fraction`, `taxonomy_available` |
|
|
141
|
+
| `TaxonomicFractions` | no | Per-domain fractions; `from_result` / `over_reads` |
|
|
142
|
+
| `read_supports_alt(read, variant_pos, ref, alt)` | no | Does an aligned read carry the ALT allele? |
|
|
143
|
+
| `parse_kmer_votes(kmer_string)` | no | Parse kraken2 per-read k-mer detail into taxid votes |
|
|
144
|
+
| `VariantNHF` | no | Result of the allele-based helpers: `variant_key`, `nonhuman_fraction`, `supporting_reads`, `fractions`, `to_dict()` (pos 0-based) |
|
|
145
|
+
| `classify_variant_alt_reads(...)` | yes | Allele-based NHF for one variant → `VariantNHF` |
|
|
146
|
+
| `classify_variants_alt_reads(...)` | yes | Batched allele-based NHF for many variants |
|
|
147
|
+
| `classify_reads_from_bam(...)` | yes | Classify reads by name and/or locus |
|
|
148
|
+
| `reads_supporting_alt(...)` | yes | Read names supporting an ALT allele |
|
|
149
|
+
|
|
150
|
+
## Docker
|
|
151
|
+
|
|
152
|
+
The included [`Dockerfile`](Dockerfile) installs the pinned kraken2 (v2.17.1) and
|
|
153
|
+
the package with the `[bam]` extra:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
docker build -t nonhuman-screen .
|
|
157
|
+
|
|
158
|
+
docker run --rm -v "$PWD:/data" nonhuman-screen \
|
|
159
|
+
classify --bam /data/sample.bam --kraken2-db /data/kraken2_db \
|
|
160
|
+
--variants /data/calls.vcf.gz --out-prefix /data/contam
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The image entrypoint is `nonhuman-screen`; mount your BAM and kraken2 database in.
|
|
164
|
+
You still supply the kraken2 database yourself (see [docs/database.md](docs/database.md)).
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# nonhuman-screen
|
|
2
|
+
|
|
3
|
+
Classify sequencing reads by **non-human taxonomic content** using
|
|
4
|
+
[kraken2](https://github.com/DerrickWood/kraken2), and reduce the per-read
|
|
5
|
+
verdicts to a **non-human fraction (NHF)** plus per-domain breakdowns
|
|
6
|
+
(bacterial / archaeal / fungal / protist / viral / UniVec-Core).
|
|
7
|
+
|
|
8
|
+
The headline capability is the **allele-based NHF**: given a BAM/CRAM and a
|
|
9
|
+
variant, compute the non-human fraction of the reads that support that
|
|
10
|
+
variant's ALT allele — in one call, no boilerplate:
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from nonhuman_screen import classify_variant_alt_reads
|
|
14
|
+
|
|
15
|
+
v = classify_variant_alt_reads(
|
|
16
|
+
"sample.bam", "kraken2_db", "chr1", 12345, "A", "T", ref_fasta="ref.fa",
|
|
17
|
+
)
|
|
18
|
+
v.nonhuman_fraction # 0.0–1.0 over ALT-supporting reads
|
|
19
|
+
v.fractions.bacterial # per-domain breakdown
|
|
20
|
+
v.supporting_reads # denominator
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`nonhuman-screen` is **sample-agnostic**: it classifies whatever reads you hand
|
|
24
|
+
it and never asks whose they are. The same call works on a proband, a parent, a
|
|
25
|
+
tumour, or a plain FASTQ — so it can back higher-level analyses (e.g. flagging
|
|
26
|
+
inherited variant calls that are actually parental contamination) without
|
|
27
|
+
knowing anything about pedigrees.
|
|
28
|
+
|
|
29
|
+
> Extracted from [`kmer-denovo-filter`](https://github.com/jlanej/kmer_denovo_filter),
|
|
30
|
+
> where this methodology was originally developed, so it can be reused
|
|
31
|
+
> independently.
|
|
32
|
+
|
|
33
|
+
## How it works (in one paragraph)
|
|
34
|
+
|
|
35
|
+
Reads are written to a temporary FASTQ and classified by kraken2's LCA
|
|
36
|
+
algorithm. Each read's assigned taxid is mapped to a domain using the
|
|
37
|
+
database's `nodes.dmp` taxonomy. A read counts as **non-human** only if it is
|
|
38
|
+
classified and lies outside the human lineage, outside the human clade, and
|
|
39
|
+
outside UniVec-Core. Two conservative guards reduce false positives: a
|
|
40
|
+
**human-homology guard** (any k-mer voting for human, taxid 9606, drops the read
|
|
41
|
+
from every non-human numerator — important for integrating viruses such as
|
|
42
|
+
HBV/HPV/ERVs) and **UniVec-Core exclusion** (synthetic vector/adapter sequences,
|
|
43
|
+
taxid 81077, are tracked separately and never counted as contamination). See
|
|
44
|
+
[docs/methodology.md](docs/methodology.md).
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install nonhuman-screen # core engine (standard library only)
|
|
50
|
+
pip install 'nonhuman-screen[bam]' # + BAM/CRAM & allele-based NHF (pysam)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The **`bam` extra is required for the BAM/allele helpers and for the
|
|
54
|
+
`nonhuman-screen` CLI** (both classify modes import pysam). The core install
|
|
55
|
+
covers only `Kraken2Runner.classify_sequences` on in-memory reads.
|
|
56
|
+
|
|
57
|
+
You also need the `kraken2` binary on `PATH` and a kraken2 database — see
|
|
58
|
+
[docs/database.md](docs/database.md).
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Allele-based NHF for many variants (batched — one kraken2 call)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from nonhuman_screen import classify_variants_alt_reads
|
|
66
|
+
|
|
67
|
+
variants = [("chr1", 12345, "A", "T"), ("chr2", 999, "G", "GAC")] # pos 0-based
|
|
68
|
+
for v in classify_variants_alt_reads("sample.bam", "kraken2_db", variants,
|
|
69
|
+
ref_fasta="ref.fa"):
|
|
70
|
+
print(v.variant_key, v.nonhuman_fraction, v.supporting_reads)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The reads supporting all variants' ALT alleles are gathered and classified in a
|
|
74
|
+
**single** kraken2 invocation (its database load dominates runtime), then split
|
|
75
|
+
back per variant. You never re-implement fetch/allele-filter/classify yourself.
|
|
76
|
+
|
|
77
|
+
### Classify an arbitrary set of reads
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from nonhuman_screen import Kraken2Runner
|
|
81
|
+
|
|
82
|
+
result = Kraken2Runner("kraken2_db").classify_sequences(
|
|
83
|
+
{"read1": "ACGT...", "read2": "TTGCA..."}
|
|
84
|
+
)
|
|
85
|
+
result.nonhuman_fraction # 0.0–1.0
|
|
86
|
+
result.fractions() # TaxonomicFractions(nonhuman=…, bacterial=…, …)
|
|
87
|
+
result.taxonomy_available # False if nodes.dmp was missing (fractions unreliable)
|
|
88
|
+
result.nonhuman_read_names # set of read names
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Command line
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Per-variant allele NHF table (headline)
|
|
95
|
+
nonhuman-screen classify \
|
|
96
|
+
--bam sample.bam --kraken2-db kraken2_db --ref-fasta ref.fa \
|
|
97
|
+
--variants candidates.vcf.gz --out-prefix sample_contam
|
|
98
|
+
# -> sample_contam.variant_nhf.tsv, sample_contam.summary.json
|
|
99
|
+
|
|
100
|
+
# Whole-BAM summary
|
|
101
|
+
nonhuman-screen classify --bam sample.bam --kraken2-db kraken2_db
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
See [docs/cli.md](docs/cli.md).
|
|
105
|
+
|
|
106
|
+
## Coordinate convention
|
|
107
|
+
|
|
108
|
+
All `pos` arguments in the Python API are **0-based** (pysam reference
|
|
109
|
+
coordinates); variant keys are `"{chrom}:{pos}:{ref}:{alt}"`. The CLI reads a
|
|
110
|
+
VCF and handles the conversion for you.
|
|
111
|
+
|
|
112
|
+
## API summary
|
|
113
|
+
|
|
114
|
+
| Symbol | Needs `[bam]` | Purpose |
|
|
115
|
+
|---|:---:|---|
|
|
116
|
+
| `Kraken2Runner.classify_sequences(seqs)` | no | Classify `{name: seq}` → `ClassificationResult` |
|
|
117
|
+
| `ClassificationResult` | no | Per-domain read-name sets, counts, `fractions()`, `nonhuman_fraction`, `taxonomy_available` |
|
|
118
|
+
| `TaxonomicFractions` | no | Per-domain fractions; `from_result` / `over_reads` |
|
|
119
|
+
| `read_supports_alt(read, variant_pos, ref, alt)` | no | Does an aligned read carry the ALT allele? |
|
|
120
|
+
| `parse_kmer_votes(kmer_string)` | no | Parse kraken2 per-read k-mer detail into taxid votes |
|
|
121
|
+
| `VariantNHF` | no | Result of the allele-based helpers: `variant_key`, `nonhuman_fraction`, `supporting_reads`, `fractions`, `to_dict()` (pos 0-based) |
|
|
122
|
+
| `classify_variant_alt_reads(...)` | yes | Allele-based NHF for one variant → `VariantNHF` |
|
|
123
|
+
| `classify_variants_alt_reads(...)` | yes | Batched allele-based NHF for many variants |
|
|
124
|
+
| `classify_reads_from_bam(...)` | yes | Classify reads by name and/or locus |
|
|
125
|
+
| `reads_supporting_alt(...)` | yes | Read names supporting an ALT allele |
|
|
126
|
+
|
|
127
|
+
## Docker
|
|
128
|
+
|
|
129
|
+
The included [`Dockerfile`](Dockerfile) installs the pinned kraken2 (v2.17.1) and
|
|
130
|
+
the package with the `[bam]` extra:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
docker build -t nonhuman-screen .
|
|
134
|
+
|
|
135
|
+
docker run --rm -v "$PWD:/data" nonhuman-screen \
|
|
136
|
+
classify --bam /data/sample.bam --kraken2-db /data/kraken2_db \
|
|
137
|
+
--variants /data/calls.vcf.gz --out-prefix /data/contam
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The image entrypoint is `nonhuman-screen`; mount your BAM and kraken2 database in.
|
|
141
|
+
You still supply the kraken2 database yourself (see [docs/database.md](docs/database.md)).
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nonhuman-screen"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Classify sequencing reads by non-human taxonomic content (kraken2), including the allele-based non-human fraction of reads supporting a variant."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = [
|
|
13
|
+
"kraken2", "contamination", "metagenomics", "non-human",
|
|
14
|
+
"taxonomy", "variant", "bioinformatics", "sequencing",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
22
|
+
]
|
|
23
|
+
# The core engine (Kraken2Runner.classify_sequences) is standard-library only
|
|
24
|
+
# and shells out to the `kraken2` binary. No third-party runtime dependency.
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
# BAM/CRAM + allele-based NHF helpers.
|
|
29
|
+
bam = ["pysam>=0.21.0"]
|
|
30
|
+
dev = ["pytest>=7.0", "pysam>=0.21.0"]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
nonhuman-screen = "nonhuman_screen.cli:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/jlanej/nonhuman-screen"
|
|
37
|
+
Documentation = "https://github.com/jlanej/nonhuman-screen/tree/main/docs"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.package-data]
|
|
43
|
+
nonhuman_screen = ["py.typed"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""nonhuman-screen: classify sequencing reads by non-human taxonomic content.
|
|
2
|
+
|
|
3
|
+
Wraps kraken2's LCA classification and reduces per-read verdicts to a
|
|
4
|
+
non-human fraction (NHF) plus per-domain breakdowns, with a human-homology
|
|
5
|
+
guard and UniVec-Core exclusion. Sample-agnostic: it classifies whatever
|
|
6
|
+
named reads you hand it, so it works equally on a proband, a parent, a tumour,
|
|
7
|
+
or a plain FASTQ.
|
|
8
|
+
|
|
9
|
+
Two levels of API:
|
|
10
|
+
|
|
11
|
+
* Engine (stdlib only) — ``Kraken2Runner.classify_sequences({name: seq})``.
|
|
12
|
+
* BAM/allele helpers (needs the ``[bam]`` extra) — ``classify_variant_alt_reads``
|
|
13
|
+
and ``classify_variants_alt_reads`` compute the non-human fraction of the
|
|
14
|
+
reads supporting a variant's ALT allele.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from nonhuman_screen.engine import (
|
|
18
|
+
ClassificationResult,
|
|
19
|
+
Kraken2Runner,
|
|
20
|
+
_ARCHAEA_TAXID,
|
|
21
|
+
_BACTERIA_TAXID,
|
|
22
|
+
_EUKARYOTA_TAXID,
|
|
23
|
+
_FUNGI_TAXID,
|
|
24
|
+
_HUMAN_TAXID,
|
|
25
|
+
_METAZOA_TAXID,
|
|
26
|
+
_UNIVEC_CORE_TAXID,
|
|
27
|
+
_VIRIDIPLANTAE_TAXID,
|
|
28
|
+
_VIRUSES_TAXID,
|
|
29
|
+
)
|
|
30
|
+
from nonhuman_screen.alleles import read_supports_alt
|
|
31
|
+
from nonhuman_screen.result import TaxonomicFractions, VariantNHF
|
|
32
|
+
from nonhuman_screen.votes import parse_kmer_votes
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"Kraken2Runner",
|
|
38
|
+
"ClassificationResult",
|
|
39
|
+
"TaxonomicFractions",
|
|
40
|
+
"VariantNHF",
|
|
41
|
+
"read_supports_alt",
|
|
42
|
+
"parse_kmer_votes",
|
|
43
|
+
"__version__",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# BAM/allele helpers require pysam ([bam] extra). Expose them at the top level
|
|
47
|
+
# when available, but never make importing the core engine depend on pysam.
|
|
48
|
+
try:
|
|
49
|
+
from nonhuman_screen.bam import ( # noqa: F401
|
|
50
|
+
classify_reads_from_bam,
|
|
51
|
+
classify_variant_alt_reads,
|
|
52
|
+
classify_variants_alt_reads,
|
|
53
|
+
reads_supporting_alt,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__all__ += [
|
|
57
|
+
"classify_reads_from_bam",
|
|
58
|
+
"classify_variant_alt_reads",
|
|
59
|
+
"classify_variants_alt_reads",
|
|
60
|
+
"reads_supporting_alt",
|
|
61
|
+
]
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Allele-support determination for reads at a variant locus.
|
|
2
|
+
|
|
3
|
+
Pure-Python helpers that decide whether an aligned read carries a given
|
|
4
|
+
alternate allele. They operate on a pysam ``AlignedSegment`` passed in by
|
|
5
|
+
the caller but do **not** import pysam themselves, so this module has no
|
|
6
|
+
third-party dependency.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_symbolic(allele):
|
|
11
|
+
"""Return True if *allele* is a symbolic VCF allele with no literal sequence.
|
|
12
|
+
|
|
13
|
+
Symbolic alleles include ``<DEL>``, ``<INS>``, ``<DUP>``, breakend
|
|
14
|
+
notation containing ``[`` or ``]``, and the overlapping-deletion
|
|
15
|
+
marker ``*``.
|
|
16
|
+
"""
|
|
17
|
+
if not allele:
|
|
18
|
+
return True
|
|
19
|
+
return allele[0] == "<" or allele == "*" or "[" in allele or "]" in allele
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_supports_alt(
|
|
23
|
+
read, variant_pos, ref, alt, min_baseq=0, *,
|
|
24
|
+
aligned_pairs=None, seq=None, quals=None,
|
|
25
|
+
):
|
|
26
|
+
"""Return True if *read* carries the alternate allele at *variant_pos*.
|
|
27
|
+
|
|
28
|
+
Extracts the exact read sequence aligned to the reference span of the
|
|
29
|
+
variant and compares it strictly to the candidate alternate allele.
|
|
30
|
+
Handles SNPs, MNPs, insertions, deletions, and complex indels natively.
|
|
31
|
+
|
|
32
|
+
Returns ``False`` for symbolic alleles or when *alt* is ``None``.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
min_baseq: Minimum base quality threshold for bases considered as
|
|
36
|
+
alt support.
|
|
37
|
+
aligned_pairs: Optional pre-computed result of
|
|
38
|
+
``read.get_aligned_pairs(matches_only=False)``. Computed from
|
|
39
|
+
*read* when not provided.
|
|
40
|
+
seq: Optional pre-decoded ``read.query_sequence``. Decoded from
|
|
41
|
+
*read* when not provided.
|
|
42
|
+
quals: Optional pre-decoded ``read.query_qualities``. Decoded from
|
|
43
|
+
*read* only when ``min_baseq > 0`` and not provided.
|
|
44
|
+
"""
|
|
45
|
+
if alt is None or _is_symbolic(alt):
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
if seq is None:
|
|
49
|
+
seq = read.query_sequence
|
|
50
|
+
if seq is None:
|
|
51
|
+
return False
|
|
52
|
+
if min_baseq > 0 and quals is None:
|
|
53
|
+
quals = read.query_qualities
|
|
54
|
+
|
|
55
|
+
if aligned_pairs is None:
|
|
56
|
+
aligned_pairs = read.get_aligned_pairs(matches_only=False)
|
|
57
|
+
|
|
58
|
+
extracted_seq = []
|
|
59
|
+
in_variant_region = False
|
|
60
|
+
|
|
61
|
+
for qpos, rpos in aligned_pairs:
|
|
62
|
+
# Stop collecting once we reach or pass the end of the reference allele span
|
|
63
|
+
if rpos is not None and rpos >= variant_pos + len(ref):
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
# Start collecting when we hit the exact start of the variant
|
|
67
|
+
if rpos == variant_pos:
|
|
68
|
+
in_variant_region = True
|
|
69
|
+
|
|
70
|
+
if in_variant_region:
|
|
71
|
+
# qpos is None for deleted bases (skip), otherwise append the read base
|
|
72
|
+
if qpos is not None:
|
|
73
|
+
if (
|
|
74
|
+
min_baseq > 0 and quals is not None
|
|
75
|
+
and quals[qpos] < min_baseq
|
|
76
|
+
):
|
|
77
|
+
return False
|
|
78
|
+
extracted_seq.append(seq[qpos])
|
|
79
|
+
|
|
80
|
+
# If the variant region was skipped entirely due to read boundaries
|
|
81
|
+
if not in_variant_region:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
return "".join(extracted_seq).upper() == alt.upper()
|
|
85
|
+
|