codonyat 1.2.0__tar.gz → 1.3.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.
- {codonyat-1.2.0 → codonyat-1.3.0}/PKG-INFO +36 -6
- {codonyat-1.2.0 → codonyat-1.3.0}/README.md +35 -5
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/__init__.py +3 -0
- codonyat-1.3.0/aa_caller/aavf.py +163 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/app.py +10 -1
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/cli.py +54 -8
- codonyat-1.3.0/aa_caller/cli_options.py +52 -0
- codonyat-1.3.0/aa_caller/constants.py +13 -0
- codonyat-1.3.0/aa_caller/container.py +451 -0
- codonyat-1.3.0/aa_caller/metrics.py +198 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/reference.py +75 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/runner.py +85 -11
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/sam.py +11 -0
- codonyat-1.3.0/aa_caller/stats_writer.py +192 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/validators.py +13 -9
- {codonyat-1.2.0 → codonyat-1.3.0}/codonyat.egg-info/PKG-INFO +36 -6
- {codonyat-1.2.0 → codonyat-1.3.0}/codonyat.egg-info/SOURCES.txt +8 -1
- {codonyat-1.2.0 → codonyat-1.3.0}/pyproject.toml +1 -1
- codonyat-1.3.0/tests/test_metrics.py +154 -0
- codonyat-1.3.0/tests/test_protein_selection.py +259 -0
- codonyat-1.3.0/tests/test_v1_3_0_features.py +290 -0
- codonyat-1.2.0/aa_caller/constants.py +0 -6
- codonyat-1.2.0/aa_caller/container.py +0 -263
- {codonyat-1.2.0 → codonyat-1.3.0}/LICENSE +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/__main__.py +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/genetic_code.py +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/aa_caller/models.py +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/codonyat.egg-info/dependency_links.txt +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/codonyat.egg-info/entry_points.txt +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/codonyat.egg-info/requires.txt +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/codonyat.egg-info/top_level.txt +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/setup.cfg +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/tests/test_app.py +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/tests/test_integration.py +0 -0
- {codonyat-1.2.0 → codonyat-1.3.0}/tests/test_v1_2_0_features.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codonyat
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: codon-yat — A codon-aware (Yet Another) amino acid variant typer from SAM alignments of viral NGS data.
|
|
5
5
|
Author-email: Marc Noguera Julian <info@treetopunder.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,13 +37,14 @@ A Python package for codon-level amino acid variant calling from SAM alignments
|
|
|
37
37
|
- CIGAR-aware codon extraction that correctly handles insertions, deletions, and soft clips.
|
|
38
38
|
- Ratio balancing, entropy tracking, and TSV/XML exporters mirroring the legacy Perl outputs.
|
|
39
39
|
- Configurable CLI flags for strand ratio bounds, entropy sensitivity, and target protein.
|
|
40
|
+
- Opt-in research statistics (Wilson CI, strand-bias test, diversity indices) and AAVF v1.0. Research use only.
|
|
40
41
|
|
|
41
42
|
## Highlights
|
|
42
43
|
|
|
43
44
|
- Produces consistent TSV/XML diagnostics while exposing Python objects (`SamContainer`, `FullReference`, etc.) that downstream tooling can import.
|
|
44
45
|
- Validates every input file before parsing to surface malformed SAM/FASTA/amplicon data early.
|
|
45
46
|
- Logs per-position entropy and strand balance for easier debugging in CI or local runs.
|
|
46
|
-
- Supports any protein target via `--protein` (defaults to RT).
|
|
47
|
+
- Supports any protein target via `--protein` (one name, a comma-separated list, or `all`; defaults to RT).
|
|
47
48
|
|
|
48
49
|
## Installation
|
|
49
50
|
|
|
@@ -79,10 +80,16 @@ codonyat sample.sam reference.fasta amplicons.tsv --annotation annotations.tsv -
|
|
|
79
80
|
| Flag | Default | Description |
|
|
80
81
|
|------|---------|-------------|
|
|
81
82
|
| `--annotation` | None | Optional TSV annotation file (record, protein, description, intervals, strand) |
|
|
82
|
-
| `--protein` | `RT` |
|
|
83
|
+
| `--protein` | `RT` | Protein(s) to analyse: one name, a comma-separated list (e.g. `PR,RT,INT`, counted in a single pass) or `all` (every protein annotated in the reference header or TSV). Names must match the FASTA header annotations |
|
|
83
84
|
| `--ratio-upper` | `3.162` | Upper bound for strand-ratio balancing |
|
|
84
85
|
| `--ratio-lower` | `0.316` | Lower bound for strand-ratio balancing |
|
|
85
86
|
| `--entropy-threshold` | `0.0` | Minimum Shannon entropy to mark a position as diverse |
|
|
87
|
+
| `--min-baseq` | off | Count a codon only if all 3 bases have Phred-like quality ≥ this value |
|
|
88
|
+
| `--stats` | off | Optional stats TSV (CI, strand-bias, flags, diversity) |
|
|
89
|
+
| `--aavf` | off | Optional AAVF v1.0 path (no CAT/SRVL) |
|
|
90
|
+
| `--min-freq` | `0.01` | Frequency floor (fraction) for `lowfreq` / AAVF FILTER |
|
|
91
|
+
| `--min-coverage` | `10` | Coverage floor for `lowcov` / AAVF FILTER |
|
|
92
|
+
| `--sb-alpha` | `0.05` | Fisher p-value cutoff for the `sb` flag |
|
|
86
93
|
|
|
87
94
|
### Output
|
|
88
95
|
|
|
@@ -94,6 +101,25 @@ The CLI writes two files alongside the input SAM:
|
|
|
94
101
|
- Rows are ordered by genomic position (codon index) along the protein's intervals
|
|
95
102
|
- For multi-interval proteins, this follows the biological reading order through all segments
|
|
96
103
|
- `[sam-file].xml` — per-position `<Depth>`, `<FwCover>`, `<RvCover>`, `<Variants>`
|
|
104
|
+
- With several proteins, each protein's positions are wrapped in `<Protein name="…">`; single-protein XML is unchanged
|
|
105
|
+
|
|
106
|
+
Default TSV/XML are byte-identical to 1.2.1 when the opt-in flags are omitted.
|
|
107
|
+
|
|
108
|
+
### Research statistics (opt-in)
|
|
109
|
+
|
|
110
|
+
`--stats PATH` writes a sidecar TSV with `LEVEL` = `variant`, `codon`, or `gene`:
|
|
111
|
+
|
|
112
|
+
- **variant**: frequency as a fraction, Wilson 95% CI, Fisher strand-bias p, flags (`lowfreq`, `sb`, `lowcov`)
|
|
113
|
+
- **codon / gene**: Shannon entropy (raw and normalised), Gini–Simpson, nucleotide π, amino-acid π
|
|
114
|
+
|
|
115
|
+
`--aavf PATH` writes AAVF v1.0. FILTER is taken from the flags above. The file
|
|
116
|
+
does not contain resistance or surveillance fields. Formulas and citations:
|
|
117
|
+
[docs/methods.md](docs/methods.md).
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
codonyat-runner reads.sam reference.fasta amps.tsv \
|
|
121
|
+
--protein all --stats sample.stats.tsv --aavf sample.aavf
|
|
122
|
+
```
|
|
97
123
|
|
|
98
124
|
### Reference FASTA format
|
|
99
125
|
|
|
@@ -152,7 +178,7 @@ result = call_variants(
|
|
|
152
178
|
sam_path=Path("reads.sam"),
|
|
153
179
|
reference_path=Path("reference.fasta"),
|
|
154
180
|
amplicons_path=Path("amps.tsv"),
|
|
155
|
-
protein="RT",
|
|
181
|
+
protein="PR,RT,INT", # or "all", "RT", or ["PR", "RT"]
|
|
156
182
|
ratio_upper=3.2,
|
|
157
183
|
)
|
|
158
184
|
|
|
@@ -181,7 +207,7 @@ call_variants_from_args(args)
|
|
|
181
207
|
A lightweight runner at `codonyat-runner` exposes the same arguments as `call_variants_from_args`:
|
|
182
208
|
|
|
183
209
|
```bash
|
|
184
|
-
codonyat-runner reads.sam reference.fasta amps.tsv --protein RT --ratio-upper 3.1 --csv-path results.tsv
|
|
210
|
+
codonyat-runner reads.sam reference.fasta amps.tsv --protein PR,RT,INT --ratio-upper 3.1 --csv-path results.tsv
|
|
185
211
|
```
|
|
186
212
|
|
|
187
213
|
## Development
|
|
@@ -214,9 +240,13 @@ codonyat/
|
|
|
214
240
|
│ ├── models.py # Protein, Amplicon, Variant, Qual dataclasses
|
|
215
241
|
│ ├── validators.py # Input file validation
|
|
216
242
|
│ ├── genetic_code.py # Codon translation table
|
|
217
|
-
│
|
|
243
|
+
│ ├── constants.py # Default thresholds
|
|
244
|
+
│ ├── metrics.py # Wilson CI, Fisher, diversity indices
|
|
245
|
+
│ ├── stats_writer.py # --stats TSV
|
|
246
|
+
│ └── aavf.py # AAVF v1.0 writer
|
|
218
247
|
├── tests/ # pytest suite (unit + integration)
|
|
219
248
|
├── docs/index.html # Project page (GitHub Pages)
|
|
249
|
+
├── docs/methods.md # Formulas and citations for 1.3.0 outputs
|
|
220
250
|
└── pyproject.toml # Package metadata
|
|
221
251
|
```
|
|
222
252
|
|
|
@@ -8,13 +8,14 @@ A Python package for codon-level amino acid variant calling from SAM alignments
|
|
|
8
8
|
- CIGAR-aware codon extraction that correctly handles insertions, deletions, and soft clips.
|
|
9
9
|
- Ratio balancing, entropy tracking, and TSV/XML exporters mirroring the legacy Perl outputs.
|
|
10
10
|
- Configurable CLI flags for strand ratio bounds, entropy sensitivity, and target protein.
|
|
11
|
+
- Opt-in research statistics (Wilson CI, strand-bias test, diversity indices) and AAVF v1.0. Research use only.
|
|
11
12
|
|
|
12
13
|
## Highlights
|
|
13
14
|
|
|
14
15
|
- Produces consistent TSV/XML diagnostics while exposing Python objects (`SamContainer`, `FullReference`, etc.) that downstream tooling can import.
|
|
15
16
|
- Validates every input file before parsing to surface malformed SAM/FASTA/amplicon data early.
|
|
16
17
|
- Logs per-position entropy and strand balance for easier debugging in CI or local runs.
|
|
17
|
-
- Supports any protein target via `--protein` (defaults to RT).
|
|
18
|
+
- Supports any protein target via `--protein` (one name, a comma-separated list, or `all`; defaults to RT).
|
|
18
19
|
|
|
19
20
|
## Installation
|
|
20
21
|
|
|
@@ -50,10 +51,16 @@ codonyat sample.sam reference.fasta amplicons.tsv --annotation annotations.tsv -
|
|
|
50
51
|
| Flag | Default | Description |
|
|
51
52
|
|------|---------|-------------|
|
|
52
53
|
| `--annotation` | None | Optional TSV annotation file (record, protein, description, intervals, strand) |
|
|
53
|
-
| `--protein` | `RT` |
|
|
54
|
+
| `--protein` | `RT` | Protein(s) to analyse: one name, a comma-separated list (e.g. `PR,RT,INT`, counted in a single pass) or `all` (every protein annotated in the reference header or TSV). Names must match the FASTA header annotations |
|
|
54
55
|
| `--ratio-upper` | `3.162` | Upper bound for strand-ratio balancing |
|
|
55
56
|
| `--ratio-lower` | `0.316` | Lower bound for strand-ratio balancing |
|
|
56
57
|
| `--entropy-threshold` | `0.0` | Minimum Shannon entropy to mark a position as diverse |
|
|
58
|
+
| `--min-baseq` | off | Count a codon only if all 3 bases have Phred-like quality ≥ this value |
|
|
59
|
+
| `--stats` | off | Optional stats TSV (CI, strand-bias, flags, diversity) |
|
|
60
|
+
| `--aavf` | off | Optional AAVF v1.0 path (no CAT/SRVL) |
|
|
61
|
+
| `--min-freq` | `0.01` | Frequency floor (fraction) for `lowfreq` / AAVF FILTER |
|
|
62
|
+
| `--min-coverage` | `10` | Coverage floor for `lowcov` / AAVF FILTER |
|
|
63
|
+
| `--sb-alpha` | `0.05` | Fisher p-value cutoff for the `sb` flag |
|
|
57
64
|
|
|
58
65
|
### Output
|
|
59
66
|
|
|
@@ -65,6 +72,25 @@ The CLI writes two files alongside the input SAM:
|
|
|
65
72
|
- Rows are ordered by genomic position (codon index) along the protein's intervals
|
|
66
73
|
- For multi-interval proteins, this follows the biological reading order through all segments
|
|
67
74
|
- `[sam-file].xml` — per-position `<Depth>`, `<FwCover>`, `<RvCover>`, `<Variants>`
|
|
75
|
+
- With several proteins, each protein's positions are wrapped in `<Protein name="…">`; single-protein XML is unchanged
|
|
76
|
+
|
|
77
|
+
Default TSV/XML are byte-identical to 1.2.1 when the opt-in flags are omitted.
|
|
78
|
+
|
|
79
|
+
### Research statistics (opt-in)
|
|
80
|
+
|
|
81
|
+
`--stats PATH` writes a sidecar TSV with `LEVEL` = `variant`, `codon`, or `gene`:
|
|
82
|
+
|
|
83
|
+
- **variant**: frequency as a fraction, Wilson 95% CI, Fisher strand-bias p, flags (`lowfreq`, `sb`, `lowcov`)
|
|
84
|
+
- **codon / gene**: Shannon entropy (raw and normalised), Gini–Simpson, nucleotide π, amino-acid π
|
|
85
|
+
|
|
86
|
+
`--aavf PATH` writes AAVF v1.0. FILTER is taken from the flags above. The file
|
|
87
|
+
does not contain resistance or surveillance fields. Formulas and citations:
|
|
88
|
+
[docs/methods.md](docs/methods.md).
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
codonyat-runner reads.sam reference.fasta amps.tsv \
|
|
92
|
+
--protein all --stats sample.stats.tsv --aavf sample.aavf
|
|
93
|
+
```
|
|
68
94
|
|
|
69
95
|
### Reference FASTA format
|
|
70
96
|
|
|
@@ -123,7 +149,7 @@ result = call_variants(
|
|
|
123
149
|
sam_path=Path("reads.sam"),
|
|
124
150
|
reference_path=Path("reference.fasta"),
|
|
125
151
|
amplicons_path=Path("amps.tsv"),
|
|
126
|
-
protein="RT",
|
|
152
|
+
protein="PR,RT,INT", # or "all", "RT", or ["PR", "RT"]
|
|
127
153
|
ratio_upper=3.2,
|
|
128
154
|
)
|
|
129
155
|
|
|
@@ -152,7 +178,7 @@ call_variants_from_args(args)
|
|
|
152
178
|
A lightweight runner at `codonyat-runner` exposes the same arguments as `call_variants_from_args`:
|
|
153
179
|
|
|
154
180
|
```bash
|
|
155
|
-
codonyat-runner reads.sam reference.fasta amps.tsv --protein RT --ratio-upper 3.1 --csv-path results.tsv
|
|
181
|
+
codonyat-runner reads.sam reference.fasta amps.tsv --protein PR,RT,INT --ratio-upper 3.1 --csv-path results.tsv
|
|
156
182
|
```
|
|
157
183
|
|
|
158
184
|
## Development
|
|
@@ -185,9 +211,13 @@ codonyat/
|
|
|
185
211
|
│ ├── models.py # Protein, Amplicon, Variant, Qual dataclasses
|
|
186
212
|
│ ├── validators.py # Input file validation
|
|
187
213
|
│ ├── genetic_code.py # Codon translation table
|
|
188
|
-
│
|
|
214
|
+
│ ├── constants.py # Default thresholds
|
|
215
|
+
│ ├── metrics.py # Wilson CI, Fisher, diversity indices
|
|
216
|
+
│ ├── stats_writer.py # --stats TSV
|
|
217
|
+
│ └── aavf.py # AAVF v1.0 writer
|
|
189
218
|
├── tests/ # pytest suite (unit + integration)
|
|
190
219
|
├── docs/index.html # Project page (GitHub Pages)
|
|
220
|
+
├── docs/methods.md # Formulas and citations for 1.3.0 outputs
|
|
191
221
|
└── pyproject.toml # Package metadata
|
|
192
222
|
```
|
|
193
223
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Light wrapper to expose the RT variant caller as a package."""
|
|
2
2
|
|
|
3
3
|
from .cli import main
|
|
4
|
+
from .constants import PACKAGE_VERSION
|
|
4
5
|
from .container import SamContainer
|
|
5
6
|
from .reference import FullReference
|
|
6
7
|
from .runner import (
|
|
@@ -23,3 +24,5 @@ __all__ = [
|
|
|
23
24
|
"SamEntry",
|
|
24
25
|
"parse_amplicons",
|
|
25
26
|
]
|
|
27
|
+
|
|
28
|
+
__version__ = PACKAGE_VERSION
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""AAVF v1.0 writer. Research use only. Never emits CAT or SRVL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from datetime import date
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .constants import PACKAGE_VERSION
|
|
11
|
+
from .genetic_code import codon_to_aminoacid
|
|
12
|
+
from .metrics import classify_flags, is_acgt_codon
|
|
13
|
+
from .models import Variant
|
|
14
|
+
from .reference import FullReference
|
|
15
|
+
|
|
16
|
+
_AAVF_AA = frozenset("ACDEFGHIKLMNPQRSTVWYX*")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def aavf_amino_acid(codon: str) -> str:
|
|
20
|
+
"""Map a codon to an AAVF amino-acid letter (``*`` = stop, ``X`` = other)."""
|
|
21
|
+
aa = codon_to_aminoacid(codon)
|
|
22
|
+
if aa == "_":
|
|
23
|
+
return "*"
|
|
24
|
+
if aa in _AAVF_AA:
|
|
25
|
+
return aa
|
|
26
|
+
return "X"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def mark_changed_bases(ref_codon: str, alt_codon: str) -> str:
|
|
30
|
+
"""Lowercase matches to the reference codon; uppercase substitutions.
|
|
31
|
+
|
|
32
|
+
Matches the AAVF v1.0 examples (``RC=aaa;AC=aaC``).
|
|
33
|
+
"""
|
|
34
|
+
ref = ref_codon.lower()
|
|
35
|
+
alt = alt_codon.lower()
|
|
36
|
+
marked = []
|
|
37
|
+
for i, base in enumerate(alt):
|
|
38
|
+
if i < len(ref) and base == ref[i]:
|
|
39
|
+
marked.append(base)
|
|
40
|
+
else:
|
|
41
|
+
marked.append(base.upper())
|
|
42
|
+
return "".join(marked)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _fmt_float(value: float) -> str:
|
|
46
|
+
return f"{value:.6g}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def write_aavf(
|
|
50
|
+
sample_name: str,
|
|
51
|
+
reference: FullReference,
|
|
52
|
+
output_path: Path,
|
|
53
|
+
protein_pairs: Sequence[tuple[str, dict[int, dict]]],
|
|
54
|
+
*,
|
|
55
|
+
min_freq: float,
|
|
56
|
+
min_coverage: int,
|
|
57
|
+
sb_alpha: float,
|
|
58
|
+
file_date: str | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Write an AAVF v1.0 file for the counted proteins.
|
|
61
|
+
|
|
62
|
+
FILTER codes are the item-1 flags ``lowfreq``, ``sb`` and ``lowcov``.
|
|
63
|
+
CAT and SRVL are never written. Partial / gapped codons map to ``X``.
|
|
64
|
+
True amino-acid indels are not called.
|
|
65
|
+
"""
|
|
66
|
+
dated = file_date or date.today().strftime("%Y%m%d")
|
|
67
|
+
fasta_name = Path(reference.path).name
|
|
68
|
+
contig_lines = [
|
|
69
|
+
f"##contig=<ID={record_id.replace(':', '|')},length={len(reference.sequences[record_id])}>"
|
|
70
|
+
for record_id in reference.record_ids
|
|
71
|
+
]
|
|
72
|
+
lines = [
|
|
73
|
+
"##fileformat=AAVFv1.0",
|
|
74
|
+
f"##fileDate={dated}",
|
|
75
|
+
f"##source=codonyat-{PACKAGE_VERSION} (research use only)",
|
|
76
|
+
f"##reference={fasta_name}",
|
|
77
|
+
*contig_lines,
|
|
78
|
+
'##INFO=<ID=RC,Number=1,Type=String,Description="Reference codon">',
|
|
79
|
+
'##INFO=<ID=AC,Number=.,Type=String,Description="Alternate codon(s) encoding ALT">',
|
|
80
|
+
'##INFO=<ID=ACC,Number=.,Type=Integer,Description="Alternate codon counts, same order as AC">',
|
|
81
|
+
'##INFO=<ID=ACF,Number=.,Type=Float,Description="Alternate codon frequencies, same order as AC">',
|
|
82
|
+
'##INFO=<ID=FWC,Number=1,Type=Integer,Description="Forward-strand coverage at the codon">',
|
|
83
|
+
'##INFO=<ID=RVC,Number=1,Type=Integer,Description="Reverse-strand coverage at the codon">',
|
|
84
|
+
'##INFO=<ID=SR,Number=1,Type=Float,Description="Forward/reverse coverage ratio at the codon">',
|
|
85
|
+
'##FILTER=<ID=lowfreq,Description="ALT frequency below --min-freq">',
|
|
86
|
+
'##FILTER=<ID=sb,Description="Strand-bias Fisher p-value below --sb-alpha">',
|
|
87
|
+
'##FILTER=<ID=lowcov,Description="Codon coverage below --min-coverage">',
|
|
88
|
+
"#CHROM\tGENE\tPOS\tREF\tALT\tFILTER\tALT_FREQ\tCOVERAGE\tINFO",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
for protein_name, entries in protein_pairs:
|
|
92
|
+
# aa_pos then genomic position; keeps GENE blocks contiguous.
|
|
93
|
+
ordered = sorted(
|
|
94
|
+
entries.items(),
|
|
95
|
+
key=lambda item: (item[1].get("aa_pos", item[0]), item[0]),
|
|
96
|
+
)
|
|
97
|
+
for pos, entry in ordered:
|
|
98
|
+
depth = int(entry["depth"])
|
|
99
|
+
if depth <= 0:
|
|
100
|
+
continue
|
|
101
|
+
record_id = str(entry.get("record_id", reference.id)).replace(":", "|")
|
|
102
|
+
gene = protein_name.replace(":", "|")
|
|
103
|
+
aa_pos = int(entry.get("aa_pos", 0)) or "."
|
|
104
|
+
ref_codon = reference.get_seq_at(pos, 3, entry.get("record_id"))
|
|
105
|
+
ref_aa = aavf_amino_acid(ref_codon) if ref_codon else "X"
|
|
106
|
+
fw_depth = int(entry["fw_depth"])
|
|
107
|
+
rv_depth = int(entry["rv_depth"])
|
|
108
|
+
sr = (fw_depth / rv_depth) if rv_depth else 0.0
|
|
109
|
+
|
|
110
|
+
by_aa: dict[str, list[Variant]] = defaultdict(list)
|
|
111
|
+
for variant in entry["variants"].values():
|
|
112
|
+
by_aa[aavf_amino_acid(variant.codon)].append(variant)
|
|
113
|
+
|
|
114
|
+
for alt_aa, variants in by_aa.items():
|
|
115
|
+
alt_count = sum(v.count for v in variants)
|
|
116
|
+
alt_fw = sum(v.fw_reads for v in variants)
|
|
117
|
+
alt_rv = sum(v.rv_reads for v in variants)
|
|
118
|
+
alt_freq = (alt_count / depth) if depth else 0.0
|
|
119
|
+
flags, _p = classify_flags(
|
|
120
|
+
freq=alt_freq,
|
|
121
|
+
coverage=depth,
|
|
122
|
+
fw=alt_fw,
|
|
123
|
+
rv=alt_rv,
|
|
124
|
+
other_fw=fw_depth - alt_fw,
|
|
125
|
+
other_rv=rv_depth - alt_rv,
|
|
126
|
+
min_freq=min_freq,
|
|
127
|
+
min_coverage=min_coverage,
|
|
128
|
+
sb_alpha=sb_alpha,
|
|
129
|
+
)
|
|
130
|
+
filter_field = ";".join(flags) if flags else "PASS"
|
|
131
|
+
ac_parts = []
|
|
132
|
+
acc_parts = []
|
|
133
|
+
acf_parts = []
|
|
134
|
+
for variant in variants:
|
|
135
|
+
codon = variant.codon
|
|
136
|
+
labelled = mark_changed_bases(ref_codon, codon) if is_acgt_codon(codon) else codon.lower()
|
|
137
|
+
ac_parts.append(labelled)
|
|
138
|
+
acc_parts.append(str(int(variant.count)))
|
|
139
|
+
acf_parts.append(_fmt_float(variant.count / depth if depth else 0.0))
|
|
140
|
+
info = (
|
|
141
|
+
f"RC={ref_codon.lower()};"
|
|
142
|
+
f"AC={','.join(ac_parts)};"
|
|
143
|
+
f"ACC={','.join(acc_parts)};"
|
|
144
|
+
f"ACF={','.join(acf_parts)};"
|
|
145
|
+
f"FWC={fw_depth};RVC={rv_depth};SR={_fmt_float(sr)}"
|
|
146
|
+
)
|
|
147
|
+
lines.append(
|
|
148
|
+
"\t".join(
|
|
149
|
+
[
|
|
150
|
+
record_id,
|
|
151
|
+
gene,
|
|
152
|
+
str(aa_pos),
|
|
153
|
+
ref_aa,
|
|
154
|
+
alt_aa,
|
|
155
|
+
filter_field,
|
|
156
|
+
_fmt_float(alt_freq),
|
|
157
|
+
str(depth),
|
|
158
|
+
info,
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
output_path.write_text("\n".join(lines) + "\n")
|
|
@@ -6,14 +6,23 @@ Existing code that does ``from aa_caller.app import X`` continues to work.
|
|
|
6
6
|
from .cli import main # noqa: F401
|
|
7
7
|
from .constants import ( # noqa: F401
|
|
8
8
|
DEFAULT_ENTROPY_THRESHOLD,
|
|
9
|
+
DEFAULT_MIN_COVERAGE,
|
|
10
|
+
DEFAULT_MIN_FREQ,
|
|
9
11
|
DEFAULT_PROTEIN,
|
|
10
12
|
DEFAULT_RATIO_LOWER,
|
|
11
13
|
DEFAULT_RATIO_UPPER,
|
|
14
|
+
DEFAULT_SB_ALPHA,
|
|
15
|
+
PACKAGE_VERSION,
|
|
12
16
|
)
|
|
13
17
|
from .container import SamContainer # noqa: F401
|
|
14
18
|
from .genetic_code import GENETIC_CODE, codon_to_aminoacid # noqa: F401
|
|
15
19
|
from .models import Amplicon, Protein, Qual, Variant # noqa: F401
|
|
16
|
-
from .reference import
|
|
20
|
+
from .reference import ( # noqa: F401
|
|
21
|
+
FullReference,
|
|
22
|
+
parse_protein_spec,
|
|
23
|
+
resolve_protein_targets,
|
|
24
|
+
resolve_proteins,
|
|
25
|
+
)
|
|
17
26
|
from .sam import SamEntry # noqa: F401
|
|
18
27
|
from .validators import ( # noqa: F401
|
|
19
28
|
parse_amplicons,
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
import logging
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
+
from .cli_options import add_research_output_args
|
|
7
8
|
from .constants import (
|
|
8
9
|
DEFAULT_ENTROPY_THRESHOLD,
|
|
9
10
|
DEFAULT_PROTEIN,
|
|
@@ -11,7 +12,7 @@ from .constants import (
|
|
|
11
12
|
DEFAULT_RATIO_UPPER,
|
|
12
13
|
)
|
|
13
14
|
from .container import SamContainer
|
|
14
|
-
from .reference import FullReference
|
|
15
|
+
from .reference import FullReference, parse_protein_spec, resolve_protein_targets
|
|
15
16
|
from .validators import (
|
|
16
17
|
parse_amplicons,
|
|
17
18
|
validate_amplicon_file,
|
|
@@ -58,8 +59,13 @@ def main() -> None:
|
|
|
58
59
|
"--protein",
|
|
59
60
|
type=str,
|
|
60
61
|
default=DEFAULT_PROTEIN,
|
|
61
|
-
help=
|
|
62
|
+
help=(
|
|
63
|
+
"Protein(s) to analyze: one name (default: RT), a comma-separated list "
|
|
64
|
+
"(e.g. PR,RT,INT; all counted in one pass over the SAM) or 'all' for "
|
|
65
|
+
"every protein annotated in the reference header."
|
|
66
|
+
),
|
|
62
67
|
)
|
|
68
|
+
add_research_output_args(parser)
|
|
63
69
|
args = parser.parse_args()
|
|
64
70
|
|
|
65
71
|
if not args.sam_file.exists():
|
|
@@ -73,7 +79,13 @@ def main() -> None:
|
|
|
73
79
|
|
|
74
80
|
logger.info("Validating input formats before parsing")
|
|
75
81
|
validate_sam_file(args.sam_file)
|
|
76
|
-
|
|
82
|
+
try:
|
|
83
|
+
parse_protein_spec(args.protein)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
parser.error(str(exc))
|
|
86
|
+
validate_reference_file(
|
|
87
|
+
args.reference_file, protein_name=args.protein, annotation_path=args.annotation
|
|
88
|
+
)
|
|
77
89
|
validate_amplicon_file(args.amplicons_file)
|
|
78
90
|
|
|
79
91
|
logger.info("Reading amplicon configuration")
|
|
@@ -81,6 +93,11 @@ def main() -> None:
|
|
|
81
93
|
|
|
82
94
|
logger.info("Loading reference")
|
|
83
95
|
reference = FullReference(args.reference_file, annotation_path=args.annotation)
|
|
96
|
+
try:
|
|
97
|
+
proteins = resolve_protein_targets(args.protein, reference)
|
|
98
|
+
except ValueError as exc:
|
|
99
|
+
parser.error(str(exc))
|
|
100
|
+
protein_names = [protein.name for protein in proteins]
|
|
84
101
|
|
|
85
102
|
logger.info("Parsing SAM entries")
|
|
86
103
|
container = SamContainer(
|
|
@@ -88,15 +105,44 @@ def main() -> None:
|
|
|
88
105
|
ratio_upper=args.ratio_upper,
|
|
89
106
|
ratio_lower=args.ratio_lower,
|
|
90
107
|
entropy_threshold=args.entropy_threshold,
|
|
108
|
+
min_baseq=args.min_baseq,
|
|
91
109
|
)
|
|
92
110
|
container.load()
|
|
93
111
|
|
|
94
|
-
logger.info("Calculating %s variant frequencies",
|
|
95
|
-
container.
|
|
112
|
+
logger.info("Calculating %s variant frequencies", ",".join(protein_names))
|
|
113
|
+
container.calculate_for_proteins(proteins, reference)
|
|
96
114
|
|
|
97
115
|
csv_path = args.sam_file.with_suffix(args.sam_file.suffix + ".tsv")
|
|
98
116
|
xml_path = args.sam_file.with_suffix(args.sam_file.suffix + ".xml")
|
|
99
117
|
logger.info("Writing TSV results")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
if len(proteins) == 1:
|
|
119
|
+
container.write_csv(str(args.sam_file), reference, csv_path, protein_name=protein_names[0])
|
|
120
|
+
logger.info("Writing XML dump")
|
|
121
|
+
container.write_xml(str(args.sam_file), reference, xml_path)
|
|
122
|
+
else:
|
|
123
|
+
container.write_csv_proteins(str(args.sam_file), reference, csv_path, protein_names)
|
|
124
|
+
logger.info("Writing XML dump")
|
|
125
|
+
container.write_xml_proteins(str(args.sam_file), reference, xml_path, protein_names)
|
|
126
|
+
|
|
127
|
+
if args.stats:
|
|
128
|
+
logger.info("Writing stats TSV")
|
|
129
|
+
container.write_stats(
|
|
130
|
+
str(args.sam_file),
|
|
131
|
+
reference,
|
|
132
|
+
args.stats,
|
|
133
|
+
protein_names,
|
|
134
|
+
min_freq=args.min_freq,
|
|
135
|
+
min_coverage=args.min_coverage,
|
|
136
|
+
sb_alpha=args.sb_alpha,
|
|
137
|
+
)
|
|
138
|
+
if args.aavf:
|
|
139
|
+
logger.info("Writing AAVF")
|
|
140
|
+
container.write_aavf(
|
|
141
|
+
str(args.sam_file),
|
|
142
|
+
reference,
|
|
143
|
+
args.aavf,
|
|
144
|
+
protein_names,
|
|
145
|
+
min_freq=args.min_freq,
|
|
146
|
+
min_coverage=args.min_coverage,
|
|
147
|
+
sb_alpha=args.sb_alpha,
|
|
148
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Shared opt-in CLI flags for research statistics and AAVF."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from argparse import ArgumentParser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .constants import DEFAULT_MIN_COVERAGE, DEFAULT_MIN_FREQ, DEFAULT_SB_ALPHA
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_research_output_args(parser: ArgumentParser) -> None:
|
|
12
|
+
"""Attach opt-in 1.3.0 flags. Defaults leave TSV/XML unchanged."""
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"--min-baseq",
|
|
15
|
+
type=float,
|
|
16
|
+
default=None,
|
|
17
|
+
help=(
|
|
18
|
+
"If set, count a codon only when all three bases have Phred-like "
|
|
19
|
+
"quality ≥ this value (e.g. 20). Off by default so TSV/XML stay "
|
|
20
|
+
"byte-identical to 1.2.1."
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--stats",
|
|
25
|
+
type=Path,
|
|
26
|
+
default=None,
|
|
27
|
+
help="Optional stats TSV (Wilson 95%% CI, strand-bias p, flags, diversity).",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--aavf",
|
|
31
|
+
type=Path,
|
|
32
|
+
default=None,
|
|
33
|
+
help="Optional AAVF v1.0 path. Research use only; CAT/SRVL are omitted.",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--min-freq",
|
|
37
|
+
type=float,
|
|
38
|
+
default=DEFAULT_MIN_FREQ,
|
|
39
|
+
help="Frequency floor (fraction 0–1) for the lowfreq flag / AAVF FILTER (default: 0.01).",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--min-coverage",
|
|
43
|
+
type=int,
|
|
44
|
+
default=DEFAULT_MIN_COVERAGE,
|
|
45
|
+
help="Coverage floor for the lowcov flag / AAVF FILTER (default: 10).",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--sb-alpha",
|
|
49
|
+
type=float,
|
|
50
|
+
default=DEFAULT_SB_ALPHA,
|
|
51
|
+
help="Fisher exact p-value cutoff for the sb (strand-bias) flag (default: 0.05).",
|
|
52
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
DEFAULT_RATIO_UPPER = 3.16227766
|
|
4
|
+
DEFAULT_RATIO_LOWER = 0.316227766
|
|
5
|
+
DEFAULT_ENTROPY_THRESHOLD = 0.0
|
|
6
|
+
DEFAULT_PROTEIN = "RT"
|
|
7
|
+
|
|
8
|
+
# Opt-in research statistics (v1.3.0). Unused unless --stats / --aavf / --min-baseq is set.
|
|
9
|
+
PACKAGE_VERSION = "1.3.0"
|
|
10
|
+
WILSON_Z_95 = 1.959963984540054 # Φ^{-1}(0.975)
|
|
11
|
+
DEFAULT_MIN_FREQ = 0.01
|
|
12
|
+
DEFAULT_MIN_COVERAGE = 10
|
|
13
|
+
DEFAULT_SB_ALPHA = 0.05
|