cdot 0.2.26__tar.gz → 0.2.28__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.
- cdot-0.2.28/PKG-INFO +180 -0
- cdot-0.2.28/README.md +141 -0
- cdot-0.2.28/analysis/benchmark_resolution.py +332 -0
- cdot-0.2.28/analysis/build_clinvar_pairs.py +101 -0
- cdot-0.2.28/analysis/clean_hgvs_search_csvs.py +116 -0
- cdot-0.2.28/analysis/combine_csv.py +11 -0
- cdot-0.2.28/analysis/compare_providers.py +35 -0
- cdot-0.2.28/analysis/compute_benchmark.py +125 -0
- cdot-0.2.28/analysis/compute_coverage.py +101 -0
- cdot-0.2.28/analysis/inject_and_clean.py +501 -0
- cdot-0.2.28/analysis/investigate_fails.py +59 -0
- {cdot-0.2.26 → cdot-0.2.28}/cdot/__init__.py +2 -2
- {cdot-0.2.26 → cdot-0.2.28}/cdot/data_release.py +33 -13
- cdot-0.2.28/cdot/hgvs/__init__.py +25 -0
- cdot-0.2.28/cdot/hgvs/clean.py +884 -0
- cdot-0.2.28/cdot/hgvs/dataproviders/__init__.py +6 -0
- cdot-0.2.28/cdot/hgvs/dataproviders/ensembl_tark_data_provider.py +486 -0
- {cdot-0.2.26 → cdot-0.2.28}/cdot/hgvs/dataproviders/fasta_seqfetcher.py +75 -51
- {cdot-0.2.26 → cdot-0.2.28}/cdot/hgvs/dataproviders/json_data_provider.py +299 -19
- cdot-0.2.28/cdot/hgvs/dataproviders/seqfetcher.py +147 -0
- cdot-0.2.28/cdot/hgvs/gene_hgvs.py +488 -0
- cdot-0.2.28/cdot/models.py +210 -0
- {cdot-0.2.26 → cdot-0.2.28}/cdot/pyhgvs/pyhgvs_transcript.py +4 -6
- cdot-0.2.28/cdot.egg-info/PKG-INFO +180 -0
- cdot-0.2.28/cdot.egg-info/SOURCES.txt +40 -0
- cdot-0.2.28/cdot.egg-info/requires.txt +28 -0
- cdot-0.2.28/cdot.egg-info/top_level.txt +8 -0
- cdot-0.2.28/pyproject.toml +68 -0
- cdot-0.2.28/setup.cfg +4 -0
- cdot-0.2.28/tests/test_clean_hgvs.py +440 -0
- cdot-0.2.28/tests/test_gene_hgvs.py +837 -0
- {cdot-0.2.26 → cdot-0.2.28}/tests/test_gff_parsers.py +73 -9
- cdot-0.2.28/tests/test_json_data_provider_ensembl.py +134 -0
- cdot-0.2.26/tests/test_json_data_provider.py → cdot-0.2.28/tests/test_json_data_provider_refseq.py +7 -1
- cdot-0.2.28/tests/test_models.py +214 -0
- cdot-0.2.28/tests/test_rest_data_provider_prefetch.py +269 -0
- cdot-0.2.28/tests/test_struct_dict_compat.py +90 -0
- cdot-0.2.26/PKG-INFO +0 -119
- cdot-0.2.26/README.md +0 -100
- cdot-0.2.26/cdot/hgvs/dataproviders/__init__.py +0 -2
- cdot-0.2.26/cdot/pyhgvs/__init__.py +0 -0
- cdot-0.2.26/cdot.egg-info/PKG-INFO +0 -119
- cdot-0.2.26/cdot.egg-info/SOURCES.txt +0 -21
- cdot-0.2.26/cdot.egg-info/requires.txt +0 -4
- cdot-0.2.26/cdot.egg-info/top_level.txt +0 -1
- cdot-0.2.26/pyproject.toml +0 -6
- cdot-0.2.26/setup.cfg +0 -36
- {cdot-0.2.26 → cdot-0.2.28}/LICENSE +0 -0
- {cdot-0.2.26/cdot/hgvs → cdot-0.2.28/cdot/pyhgvs}/__init__.py +0 -0
- {cdot-0.2.26 → cdot-0.2.28}/cdot.egg-info/dependency_links.txt +0 -0
- {cdot-0.2.26 → cdot-0.2.28}/tests/test_pyhgvs.py +0 -0
- {cdot-0.2.26 → cdot-0.2.28}/tests/test_uta_conversion.py +0 -0
cdot-0.2.28/PKG-INFO
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cdot
|
|
3
|
+
Version: 0.2.28
|
|
4
|
+
Summary: Transcripts for HGVS libraries
|
|
5
|
+
Author-email: Dave Lawrence <davmlaw@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SACGF/cdot
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/SACGF/cdot/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: intervaltree
|
|
17
|
+
Requires-Dist: more_itertools
|
|
18
|
+
Requires-Dist: bioutils>=0.5.8
|
|
19
|
+
Requires-Dist: lazy
|
|
20
|
+
Requires-Dist: hgvs
|
|
21
|
+
Requires-Dist: msgspec
|
|
22
|
+
Provides-Extra: hgvs
|
|
23
|
+
Requires-Dist: hgvs; extra == "hgvs"
|
|
24
|
+
Provides-Extra: fasta
|
|
25
|
+
Requires-Dist: pysam; extra == "fasta"
|
|
26
|
+
Provides-Extra: build
|
|
27
|
+
Requires-Dist: HTSeq>=2.1.2; extra == "build"
|
|
28
|
+
Requires-Dist: ijson; extra == "build"
|
|
29
|
+
Requires-Dist: biopython; extra == "build"
|
|
30
|
+
Requires-Dist: pyhgvs; extra == "build"
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest; extra == "test"
|
|
33
|
+
Requires-Dist: msgspec; extra == "test"
|
|
34
|
+
Requires-Dist: pysam; extra == "test"
|
|
35
|
+
Provides-Extra: paper
|
|
36
|
+
Requires-Dist: snakemake; extra == "paper"
|
|
37
|
+
Requires-Dist: vibepaper; extra == "paper"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# cdot
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/cdot/) [](https://pypi.org/project/cdot/) [](https://github.com/SACGF/cdot/actions/workflows/tests.yml) [](https://zenodo.org/doi/10.5281/zenodo.13324621)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
**cdot** provides the transcript data needed to map and validate
|
|
46
|
+
[HGVS](http://varnomen.hgvs.org/) variants - the gene/transcript coordinates, exon structure and
|
|
47
|
+
genome alignments - for the two most popular Python HGVS libraries:
|
|
48
|
+
[biocommons HGVS](https://github.com/biocommons/hgvs) and [PyHGVS](https://github.com/counsyl/hgvs).
|
|
49
|
+
|
|
50
|
+
To do HGVS work (e.g. convert `NM_001637.3:c.1582G>A` to genomic coordinates) those libraries need a
|
|
51
|
+
transcript data source. The usual source, [UTA](https://github.com/biocommons/uta), is a PostgreSQL
|
|
52
|
+
database that's slow and heavy to run. cdot instead **converts the official RefSeq/Ensembl annotation
|
|
53
|
+
files (GTF/GFF3) into compact JSON** and ships fast loaders for the HGVS libraries. You can use it via:
|
|
54
|
+
|
|
55
|
+
* **Local `JSON.gz` files** - [download a release](https://github.com/SACGF/cdot/releases) and load it into RAM (fastest).
|
|
56
|
+
* **REST API** - query a [cdot_rest](https://github.com/SACGF/cdot_rest) server (no local data needed).
|
|
57
|
+
|
|
58
|
+
Because it reads the released annotation files directly, cdot covers **1.58 million transcript/genome
|
|
59
|
+
alignments**, including historical transcript versions - vs ~141k in UTA (v.20210129) - which matters
|
|
60
|
+
when resolving legacy HGVS. See [cdot vs UTA](docs/cdot_vs_uta.md) for the trade-offs.
|
|
61
|
+
|
|
62
|
+
Recent changes are in the [changelog](https://github.com/SACGF/cdot/blob/main/CHANGELOG.md).
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
pip install cdot
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Optional extras:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
pip install cdot[fasta] # local genome FASTA sequence fetching (pysam) - needed for the PyHGVS example below
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
(`hgvs` is a core dependency, so the biocommons HGVS examples work out of the box.)
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
|
|
80
|
+
[Biocommons HGVS](https://github.com/biocommons/hgvs) example:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
import hgvs
|
|
84
|
+
from hgvs.assemblymapper import AssemblyMapper
|
|
85
|
+
from cdot.hgvs.dataproviders import JSONDataProvider, RESTDataProvider
|
|
86
|
+
|
|
87
|
+
hdp = RESTDataProvider() # Uses API server at cdotlib.org
|
|
88
|
+
# hdp = JSONDataProvider(["./cdot-0.2.32.refseq.grch37.json.gz"]) # Uses local JSON file
|
|
89
|
+
|
|
90
|
+
am = AssemblyMapper(hdp,
|
|
91
|
+
assembly_name='GRCh37',
|
|
92
|
+
alt_aln_method='splign', replace_reference=True)
|
|
93
|
+
|
|
94
|
+
hp = hgvs.parser.Parser()
|
|
95
|
+
var_c = hp.parse_hgvs_variant('NM_001637.3:c.1582G>A')
|
|
96
|
+
am.c_to_g(var_c)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
[more Biocommons examples](docs/examples_biocommons.md):
|
|
100
|
+
|
|
101
|
+
> **Tip:** cdot provides many transcripts that aren't in SeqRepo, so the default sequence fetcher will
|
|
102
|
+
> raise `HGVSDataNotAvailableError` for them. You almost always want to supply a
|
|
103
|
+
> [`FastaSeqFetcher`](docs/fasta_seqfetcher.md) (chained after SeqRepo) so every cdot transcript
|
|
104
|
+
> resolves against a local genome FASTA.
|
|
105
|
+
|
|
106
|
+
For fixing messy HGVS input and fast bulk processing, see [Advanced usage](docs/advanced_usage.md).
|
|
107
|
+
|
|
108
|
+
[PyHGVS](https://github.com/counsyl/hgvs) example (needs `pip install cdot[fasta]` for pysam):
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
import pyhgvs
|
|
112
|
+
from pysam.libcfaidx import FastaFile
|
|
113
|
+
from cdot.pyhgvs.pyhgvs_transcript import JSONPyHGVSTranscriptFactory, RESTPyHGVSTranscriptFactory
|
|
114
|
+
|
|
115
|
+
genome = FastaFile("/data/annotation/fasta/GCF_000001405.25_GRCh37.p13_genomic.fna.gz")
|
|
116
|
+
factory = RESTPyHGVSTranscriptFactory()
|
|
117
|
+
# factory = JSONPyHGVSTranscriptFactory(["./cdot-0.2.32.refseq.grch37.json.gz"]) # Uses local JSON file
|
|
118
|
+
pyhgvs.parse_hgvs_name('NM_001637.3:c.1582G>A', genome, get_transcript=factory.get_transcript_grch37)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
[more PyHGVS examples](docs/examples_pyhgvs.md):
|
|
122
|
+
|
|
123
|
+
## Documentation
|
|
124
|
+
|
|
125
|
+
See [docs/](docs/) for reference and how-to guides:
|
|
126
|
+
|
|
127
|
+
* [JSON data format](docs/json_data_format.md) - every field in a cdot JSON(.gz) file
|
|
128
|
+
* [Coordinates & exon alignments](docs/coordinates_and_exons.md) - how exon coordinates and gap strings work
|
|
129
|
+
* [Advanced usage](docs/advanced_usage.md) - fixing messy HGVS input, and bulk read-ahead retrieval
|
|
130
|
+
|
|
131
|
+
See the [docs index](docs/README.md) for the full list (examples, FastaSeqFetcher, creating data, cdot vs UTA, …).
|
|
132
|
+
|
|
133
|
+
## Q. What's the performance like?
|
|
134
|
+
|
|
135
|
+
Resolving real ClinVar c.HGVS to genomic coordinates (GRCh38, biocommons HGVS, local sequence fetching):
|
|
136
|
+
|
|
137
|
+
* UTA public DB: ~1-1.5 seconds / transcript
|
|
138
|
+
* cdot REST service: ~30 HGVS/second sequential, **~500 HGVS/second** with [`prefetch()`](docs/advanced_usage.md) batch cache-warming
|
|
139
|
+
* cdot JSON.gz (local): 500-1k/second
|
|
140
|
+
|
|
141
|
+
`prefetch()` warms every transcript in one batch round-trip up front, so bulk resolution over the REST
|
|
142
|
+
service runs almost entirely from cache - closing most of the gap to local JSON.gz (~16x faster end-to-end
|
|
143
|
+
on 500 variants). Reproduce with `analysis/benchmark_resolution.py`.
|
|
144
|
+
|
|
145
|
+
## Q. Where can I download the JSON.gz files?
|
|
146
|
+
|
|
147
|
+
[Download from GitHub releases](https://github.com/SACGF/cdot/releases) - RefSeq (37/38) - 72M, Ensembl (37/38) 61M
|
|
148
|
+
|
|
149
|
+
Details on what the files contain [here](docs/release_files.md)
|
|
150
|
+
|
|
151
|
+
## Q. How does this compare to Universal Transcript Archive?
|
|
152
|
+
|
|
153
|
+
Both projects have similar goals of providing transcripts for loading HGVS, but they approach it from different ways
|
|
154
|
+
|
|
155
|
+
* UTA aligns sequences, then stores coordinates in an SQL database.
|
|
156
|
+
* cdot convert existing Ensembl/RefSeq GTFs into JSON
|
|
157
|
+
|
|
158
|
+
See [cdot vs UTA](docs/cdot_vs_uta.md) for more details
|
|
159
|
+
|
|
160
|
+
## Q. How do you store transcripts in JSON?
|
|
161
|
+
|
|
162
|
+
See the **[JSON data format reference](docs/json_data_format.md)** for a full description of every field, with a machine-readable [JSON Schema](docs/cdot-json-schema.json) alongside it. [Coordinates & exon alignments](docs/coordinates_and_exons.md) explains how exon coordinates and the alignment gap strings work. See also [design notes](docs/design_notes.md) on why the format looks the way it does.
|
|
163
|
+
|
|
164
|
+
You can also read the data with typed Python objects (no extra install required):
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from cdot import models
|
|
168
|
+
|
|
169
|
+
data = models.load("cdot-0.2.32.refseq.GRCh38.json.gz")
|
|
170
|
+
tx = data.transcripts["NM_001637.3"]
|
|
171
|
+
print(tx.gene_name, tx.protein)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
We think a standard for JSON gene/transcript information would be a great thing, and am keen to collaborate to make it happen!
|
|
175
|
+
|
|
176
|
+
## Q. What does cdot stand for?
|
|
177
|
+
|
|
178
|
+
cdot, pronounced "see dot", is a play on the HGVS coding-sequence prefix ```:c.```
|
|
179
|
+
|
|
180
|
+
This was developed for the [Australian Genomics](https://www.australiangenomics.org.au/) [Shariant](https://shariant.org.au/) project, due to the need to load historical HGVS from lab archives.
|
cdot-0.2.28/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# cdot
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/cdot/) [](https://pypi.org/project/cdot/) [](https://github.com/SACGF/cdot/actions/workflows/tests.yml) [](https://zenodo.org/doi/10.5281/zenodo.13324621)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
**cdot** provides the transcript data needed to map and validate
|
|
7
|
+
[HGVS](http://varnomen.hgvs.org/) variants - the gene/transcript coordinates, exon structure and
|
|
8
|
+
genome alignments - for the two most popular Python HGVS libraries:
|
|
9
|
+
[biocommons HGVS](https://github.com/biocommons/hgvs) and [PyHGVS](https://github.com/counsyl/hgvs).
|
|
10
|
+
|
|
11
|
+
To do HGVS work (e.g. convert `NM_001637.3:c.1582G>A` to genomic coordinates) those libraries need a
|
|
12
|
+
transcript data source. The usual source, [UTA](https://github.com/biocommons/uta), is a PostgreSQL
|
|
13
|
+
database that's slow and heavy to run. cdot instead **converts the official RefSeq/Ensembl annotation
|
|
14
|
+
files (GTF/GFF3) into compact JSON** and ships fast loaders for the HGVS libraries. You can use it via:
|
|
15
|
+
|
|
16
|
+
* **Local `JSON.gz` files** - [download a release](https://github.com/SACGF/cdot/releases) and load it into RAM (fastest).
|
|
17
|
+
* **REST API** - query a [cdot_rest](https://github.com/SACGF/cdot_rest) server (no local data needed).
|
|
18
|
+
|
|
19
|
+
Because it reads the released annotation files directly, cdot covers **1.58 million transcript/genome
|
|
20
|
+
alignments**, including historical transcript versions - vs ~141k in UTA (v.20210129) - which matters
|
|
21
|
+
when resolving legacy HGVS. See [cdot vs UTA](docs/cdot_vs_uta.md) for the trade-offs.
|
|
22
|
+
|
|
23
|
+
Recent changes are in the [changelog](https://github.com/SACGF/cdot/blob/main/CHANGELOG.md).
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
pip install cdot
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Optional extras:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install cdot[fasta] # local genome FASTA sequence fetching (pysam) - needed for the PyHGVS example below
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
(`hgvs` is a core dependency, so the biocommons HGVS examples work out of the box.)
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
[Biocommons HGVS](https://github.com/biocommons/hgvs) example:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
import hgvs
|
|
45
|
+
from hgvs.assemblymapper import AssemblyMapper
|
|
46
|
+
from cdot.hgvs.dataproviders import JSONDataProvider, RESTDataProvider
|
|
47
|
+
|
|
48
|
+
hdp = RESTDataProvider() # Uses API server at cdotlib.org
|
|
49
|
+
# hdp = JSONDataProvider(["./cdot-0.2.32.refseq.grch37.json.gz"]) # Uses local JSON file
|
|
50
|
+
|
|
51
|
+
am = AssemblyMapper(hdp,
|
|
52
|
+
assembly_name='GRCh37',
|
|
53
|
+
alt_aln_method='splign', replace_reference=True)
|
|
54
|
+
|
|
55
|
+
hp = hgvs.parser.Parser()
|
|
56
|
+
var_c = hp.parse_hgvs_variant('NM_001637.3:c.1582G>A')
|
|
57
|
+
am.c_to_g(var_c)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
[more Biocommons examples](docs/examples_biocommons.md):
|
|
61
|
+
|
|
62
|
+
> **Tip:** cdot provides many transcripts that aren't in SeqRepo, so the default sequence fetcher will
|
|
63
|
+
> raise `HGVSDataNotAvailableError` for them. You almost always want to supply a
|
|
64
|
+
> [`FastaSeqFetcher`](docs/fasta_seqfetcher.md) (chained after SeqRepo) so every cdot transcript
|
|
65
|
+
> resolves against a local genome FASTA.
|
|
66
|
+
|
|
67
|
+
For fixing messy HGVS input and fast bulk processing, see [Advanced usage](docs/advanced_usage.md).
|
|
68
|
+
|
|
69
|
+
[PyHGVS](https://github.com/counsyl/hgvs) example (needs `pip install cdot[fasta]` for pysam):
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
import pyhgvs
|
|
73
|
+
from pysam.libcfaidx import FastaFile
|
|
74
|
+
from cdot.pyhgvs.pyhgvs_transcript import JSONPyHGVSTranscriptFactory, RESTPyHGVSTranscriptFactory
|
|
75
|
+
|
|
76
|
+
genome = FastaFile("/data/annotation/fasta/GCF_000001405.25_GRCh37.p13_genomic.fna.gz")
|
|
77
|
+
factory = RESTPyHGVSTranscriptFactory()
|
|
78
|
+
# factory = JSONPyHGVSTranscriptFactory(["./cdot-0.2.32.refseq.grch37.json.gz"]) # Uses local JSON file
|
|
79
|
+
pyhgvs.parse_hgvs_name('NM_001637.3:c.1582G>A', genome, get_transcript=factory.get_transcript_grch37)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
[more PyHGVS examples](docs/examples_pyhgvs.md):
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
See [docs/](docs/) for reference and how-to guides:
|
|
87
|
+
|
|
88
|
+
* [JSON data format](docs/json_data_format.md) - every field in a cdot JSON(.gz) file
|
|
89
|
+
* [Coordinates & exon alignments](docs/coordinates_and_exons.md) - how exon coordinates and gap strings work
|
|
90
|
+
* [Advanced usage](docs/advanced_usage.md) - fixing messy HGVS input, and bulk read-ahead retrieval
|
|
91
|
+
|
|
92
|
+
See the [docs index](docs/README.md) for the full list (examples, FastaSeqFetcher, creating data, cdot vs UTA, …).
|
|
93
|
+
|
|
94
|
+
## Q. What's the performance like?
|
|
95
|
+
|
|
96
|
+
Resolving real ClinVar c.HGVS to genomic coordinates (GRCh38, biocommons HGVS, local sequence fetching):
|
|
97
|
+
|
|
98
|
+
* UTA public DB: ~1-1.5 seconds / transcript
|
|
99
|
+
* cdot REST service: ~30 HGVS/second sequential, **~500 HGVS/second** with [`prefetch()`](docs/advanced_usage.md) batch cache-warming
|
|
100
|
+
* cdot JSON.gz (local): 500-1k/second
|
|
101
|
+
|
|
102
|
+
`prefetch()` warms every transcript in one batch round-trip up front, so bulk resolution over the REST
|
|
103
|
+
service runs almost entirely from cache - closing most of the gap to local JSON.gz (~16x faster end-to-end
|
|
104
|
+
on 500 variants). Reproduce with `analysis/benchmark_resolution.py`.
|
|
105
|
+
|
|
106
|
+
## Q. Where can I download the JSON.gz files?
|
|
107
|
+
|
|
108
|
+
[Download from GitHub releases](https://github.com/SACGF/cdot/releases) - RefSeq (37/38) - 72M, Ensembl (37/38) 61M
|
|
109
|
+
|
|
110
|
+
Details on what the files contain [here](docs/release_files.md)
|
|
111
|
+
|
|
112
|
+
## Q. How does this compare to Universal Transcript Archive?
|
|
113
|
+
|
|
114
|
+
Both projects have similar goals of providing transcripts for loading HGVS, but they approach it from different ways
|
|
115
|
+
|
|
116
|
+
* UTA aligns sequences, then stores coordinates in an SQL database.
|
|
117
|
+
* cdot convert existing Ensembl/RefSeq GTFs into JSON
|
|
118
|
+
|
|
119
|
+
See [cdot vs UTA](docs/cdot_vs_uta.md) for more details
|
|
120
|
+
|
|
121
|
+
## Q. How do you store transcripts in JSON?
|
|
122
|
+
|
|
123
|
+
See the **[JSON data format reference](docs/json_data_format.md)** for a full description of every field, with a machine-readable [JSON Schema](docs/cdot-json-schema.json) alongside it. [Coordinates & exon alignments](docs/coordinates_and_exons.md) explains how exon coordinates and the alignment gap strings work. See also [design notes](docs/design_notes.md) on why the format looks the way it does.
|
|
124
|
+
|
|
125
|
+
You can also read the data with typed Python objects (no extra install required):
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from cdot import models
|
|
129
|
+
|
|
130
|
+
data = models.load("cdot-0.2.32.refseq.GRCh38.json.gz")
|
|
131
|
+
tx = data.transcripts["NM_001637.3"]
|
|
132
|
+
print(tx.gene_name, tx.protein)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
We think a standard for JSON gene/transcript information would be a great thing, and am keen to collaborate to make it happen!
|
|
136
|
+
|
|
137
|
+
## Q. What does cdot stand for?
|
|
138
|
+
|
|
139
|
+
cdot, pronounced "see dot", is a play on the HGVS coding-sequence prefix ```:c.```
|
|
140
|
+
|
|
141
|
+
This was developed for the [Australian Genomics](https://www.australiangenomics.org.au/) [Shariant](https://shariant.org.au/) project, due to the need to load historical HGVS from lab archives.
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Resolution / recovery / speed benchmark for cdot.
|
|
3
|
+
|
|
4
|
+
Answers the three paper questions over real ClinVar (g.HGVS, c.HGVS) pairs:
|
|
5
|
+
|
|
6
|
+
1. RESOLUTION - of real c.HGVS, how many convert to the correct genomic coord?
|
|
7
|
+
2. RECOVERY - how many failing strings does fix_hgvs() rescue, and via which mode?
|
|
8
|
+
3. SPEED - throughput (HGVS/s) and provider load time.
|
|
9
|
+
|
|
10
|
+
Ground truth: the g.HGVS column (from ClinVar CLNHGVS). A c->g conversion is "correct"
|
|
11
|
+
if it reproduces that g.HGVS.
|
|
12
|
+
|
|
13
|
+
Recovery (--recovery) routes every string through the single combined entry point
|
|
14
|
+
fix_hgvs() (clean_hgvs -> resolve_gene_hgvs -> version-bump fallback) and attributes
|
|
15
|
+
each rescue to a failure mode by reading the HGVSFix codes it returns. Two passes:
|
|
16
|
+
* as-is - run fix_hgvs() on the pairs unchanged; rescues + false-rescues + a
|
|
17
|
+
regression guard. This is the production-mode measurement: on clean
|
|
18
|
+
input nothing fires, so feed messy data to see real per-mode numbers.
|
|
19
|
+
* degraded - degrade each clean pair per mode (inject a formatting error / request
|
|
20
|
+
an absent transcript version), then fix_hgvs() -> resolve and score
|
|
21
|
+
against ground truth, with a false-rescue count. Exercises the rescue
|
|
22
|
+
path on the (already-clean) committed test data.
|
|
23
|
+
Note: the "more transcripts" (coverage) mode is NOT a string repair - it needs a
|
|
24
|
+
provider swap (run with --uta vs cdot and compare no_data), not fix_hgvs().
|
|
25
|
+
|
|
26
|
+
Provider is pluggable so we isolate the data-layer contribution (the biocommons engine
|
|
27
|
+
is held constant). Defaults to cdot REST (cdotlib.org) so it runs with no local data.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
# quick: REST over the committed test pairs
|
|
31
|
+
python analysis/benchmark_resolution.py tests/test_data/clinvar_hgvs/clinvar_hgvs_100.tsv --rest
|
|
32
|
+
|
|
33
|
+
# local JSON (after downloading a release json.gz)
|
|
34
|
+
python analysis/benchmark_resolution.py pairs.tsv --json cdot-X.refseq.grch38.json.gz
|
|
35
|
+
|
|
36
|
+
# add the unified fix_hgvs() recovery passes (as-is + degraded)
|
|
37
|
+
python analysis/benchmark_resolution.py pairs.tsv --rest --recovery
|
|
38
|
+
|
|
39
|
+
Input file: TSV with two columns g.HGVS <TAB> c.HGVS (one variant per line).
|
|
40
|
+
"""
|
|
41
|
+
import argparse
|
|
42
|
+
import json
|
|
43
|
+
import logging
|
|
44
|
+
import re
|
|
45
|
+
import time
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
import hgvs.dataproviders.uta
|
|
49
|
+
import hgvs.parser
|
|
50
|
+
from hgvs.assemblymapper import AssemblyMapper
|
|
51
|
+
from hgvs.exceptions import HGVSDataNotAvailableError, HGVSInvalidVariantError
|
|
52
|
+
|
|
53
|
+
from cdot.hgvs.dataproviders import JSONDataProvider, RESTDataProvider, FastaSeqFetcher
|
|
54
|
+
from cdot.hgvs.clean import HGVSFixCode, HGVSFixSeverity, VersionStrategy
|
|
55
|
+
from cdot.hgvs.gene_hgvs import fix_hgvs
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_provider(args):
|
|
59
|
+
"""Return (provider, label). replace_reference is handled by the caller."""
|
|
60
|
+
# Local FASTA seqfetcher avoids remote NCBI sequence fetching (the real speed bottleneck).
|
|
61
|
+
seqfetcher = FastaSeqFetcher(*args.fasta) if args.fasta else None
|
|
62
|
+
seq_label = "fasta" if args.fasta else "ncbi"
|
|
63
|
+
if args.uta:
|
|
64
|
+
# NOTE: do NOT attach cdot's --fasta FastaSeqFetcher to UTA. That fetcher
|
|
65
|
+
# reconstructs transcript (NM_) sequence from the genome using *cdot's*
|
|
66
|
+
# exon data; bare UTA has none, so every transcript-sequence request
|
|
67
|
+
# raises HGVSDataNotAvailableError and is mis-counted as no_data (it made
|
|
68
|
+
# a clean UTA run look like 85% no_data — a pure artifact). Stock UTA must
|
|
69
|
+
# use its own SeqRepo/remote seqfetcher. Set HGVS_SEQREPO_DIR for a fast,
|
|
70
|
+
# offline, transcript-capable baseline; otherwise it falls back to the
|
|
71
|
+
# (slow) remote bioutils seqfetcher.
|
|
72
|
+
if args.fasta:
|
|
73
|
+
print(" (--fasta ignored for --uta: cdot's genome fetcher can't serve "
|
|
74
|
+
"transcript sequence; UTA uses SeqRepo/remote)")
|
|
75
|
+
provider = hgvs.dataproviders.uta.connect()
|
|
76
|
+
import os
|
|
77
|
+
seq_src = "seqrepo" if os.environ.get("HGVS_SEQREPO_DIR") else "remote"
|
|
78
|
+
uta_url = os.environ.get("UTA_DB_URL", "")
|
|
79
|
+
loc = "local" if ("localhost" in uta_url or "127.0.0.1" in uta_url) else "remote"
|
|
80
|
+
return provider, f"uta_{loc}/{seq_src}"
|
|
81
|
+
if args.json:
|
|
82
|
+
return JSONDataProvider([args.json], seqfetcher=seqfetcher), f"cdot_json({Path(args.json).name})/{seq_label}"
|
|
83
|
+
# default: REST
|
|
84
|
+
return RESTDataProvider(secure=not args.rest_insecure, seqfetcher=seqfetcher), f"cdot_rest(cdotlib.org)/{seq_label}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_pairs(path):
|
|
88
|
+
pairs = []
|
|
89
|
+
with open(path) as fh:
|
|
90
|
+
for line in fh:
|
|
91
|
+
line = line.strip()
|
|
92
|
+
if not line:
|
|
93
|
+
continue
|
|
94
|
+
parts = line.split("\t") if "\t" in line else line.split()
|
|
95
|
+
if len(parts) >= 2:
|
|
96
|
+
pairs.append((parts[0], parts[1]))
|
|
97
|
+
return pairs
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def classify(am, hp, g_hgvs, c_hgvs):
|
|
101
|
+
"""Return (bucket, converted_or_None). buckets: correct/incorrect/no_data/error."""
|
|
102
|
+
try:
|
|
103
|
+
var_c = hp.parse_hgvs_variant(c_hgvs)
|
|
104
|
+
if ":c." in c_hgvs:
|
|
105
|
+
converted = str(am.c_to_g(var_c))
|
|
106
|
+
else:
|
|
107
|
+
converted = str(am.n_to_g(var_c))
|
|
108
|
+
except HGVSDataNotAvailableError:
|
|
109
|
+
return "no_data", None
|
|
110
|
+
except HGVSInvalidVariantError:
|
|
111
|
+
return "error", None
|
|
112
|
+
except Exception as e: # noqa: BLE001 - benchmark wants to keep going
|
|
113
|
+
logging.debug("error on %s: %s", c_hgvs, e)
|
|
114
|
+
return "error", None
|
|
115
|
+
return ("correct" if converted == g_hgvs else "incorrect"), converted
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def run_resolution(am, hp, pairs, show_incorrect=False):
|
|
119
|
+
counts = {"correct": 0, "incorrect": 0, "no_data": 0, "error": 0}
|
|
120
|
+
times = []
|
|
121
|
+
t0 = time.perf_counter()
|
|
122
|
+
for g_hgvs, c_hgvs in pairs:
|
|
123
|
+
s = time.perf_counter()
|
|
124
|
+
bucket, converted = classify(am, hp, g_hgvs, c_hgvs)
|
|
125
|
+
times.append(time.perf_counter() - s)
|
|
126
|
+
counts[bucket] += 1
|
|
127
|
+
if show_incorrect and bucket == "incorrect":
|
|
128
|
+
print(f" INCORRECT {c_hgvs}: expected {g_hgvs} got {converted}")
|
|
129
|
+
if bucket == "no_data":
|
|
130
|
+
logging.debug("no_data: %s", c_hgvs)
|
|
131
|
+
wall = time.perf_counter() - t0
|
|
132
|
+
return counts, times, wall
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---- unified recovery via fix_hgvs ------------------------------------------
|
|
136
|
+
#
|
|
137
|
+
# fix_hgvs() is the single combined entry point: clean_hgvs -> resolve_gene_hgvs
|
|
138
|
+
# -> resolve_transcript_version (version bump). It returns the repaired string
|
|
139
|
+
# plus the list of HGVSFix applied, so we attribute each rescue to a failure
|
|
140
|
+
# mode just by reading the fix codes returned — no separate per-mode pipeline.
|
|
141
|
+
|
|
142
|
+
# WARNING-level fix codes grouped by the failure mode they address.
|
|
143
|
+
_VERSION_CODES = {HGVSFixCode.USED_ADJACENT_VERSION}
|
|
144
|
+
_GENE_CODES = {HGVSFixCode.RESOLVED_GENE_TO_TRANSCRIPT, HGVSFixCode.UPPERCASED_GENE_SYMBOL}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _mode_of(code):
|
|
148
|
+
if code in _VERSION_CODES:
|
|
149
|
+
return "version_bump"
|
|
150
|
+
if code in _GENE_CODES:
|
|
151
|
+
return "gene_resolution"
|
|
152
|
+
return "cleaning" # every other WARNING code is a clean_hgvs() string fix
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _fix_and_classify(am, hp, provider, build, g_hgvs, c_input, version_fallback):
|
|
156
|
+
"""Run fix_hgvs() then resolve. Returns (bucket, fixed_string, modes_fired)."""
|
|
157
|
+
try:
|
|
158
|
+
fixed, fixes = fix_hgvs(c_input, provider, build, version_fallback=version_fallback)
|
|
159
|
+
except Exception as e: # noqa: BLE001 - benchmark keeps going
|
|
160
|
+
logging.debug("fix_hgvs error on %s: %s", c_input, e)
|
|
161
|
+
return "error", c_input, set()
|
|
162
|
+
modes = {_mode_of(f.code) for f in fixes if f.severity == HGVSFixSeverity.WARNING}
|
|
163
|
+
bucket, _converted = classify(am, hp, g_hgvs, fixed)
|
|
164
|
+
return bucket, fixed, modes
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def run_asis(am, hp, provider, build, pairs, version_fallback):
|
|
168
|
+
"""Production-mode pass: run fix_hgvs() on each pair as-is and read what the
|
|
169
|
+
returned fixes rescued. Also guards against regressions (a string that
|
|
170
|
+
resolved correctly before fix_hgvs but not after)."""
|
|
171
|
+
agg = {"n": len(pairs), "baseline_correct": 0, "fixed_correct": 0,
|
|
172
|
+
"rescued": 0, "regressed": 0, "false_rescue": 0,
|
|
173
|
+
"by_mode": {}}
|
|
174
|
+
for g_hgvs, c_hgvs in pairs:
|
|
175
|
+
base_bucket, _ = classify(am, hp, g_hgvs, c_hgvs)
|
|
176
|
+
fbucket, _fixed, modes = _fix_and_classify(
|
|
177
|
+
am, hp, provider, build, g_hgvs, c_hgvs, version_fallback)
|
|
178
|
+
if base_bucket == "correct":
|
|
179
|
+
agg["baseline_correct"] += 1
|
|
180
|
+
if fbucket == "correct":
|
|
181
|
+
agg["fixed_correct"] += 1
|
|
182
|
+
for m in modes:
|
|
183
|
+
agg["by_mode"].setdefault(m, {"fired": 0, "rescued": 0, "false_rescue": 0})["fired"] += 1
|
|
184
|
+
if base_bucket != "correct" and fbucket == "correct":
|
|
185
|
+
agg["rescued"] += 1
|
|
186
|
+
for m in modes:
|
|
187
|
+
agg["by_mode"][m]["rescued"] += 1
|
|
188
|
+
if base_bucket == "correct" and fbucket != "correct":
|
|
189
|
+
agg["regressed"] += 1
|
|
190
|
+
if fbucket == "incorrect":
|
|
191
|
+
agg["false_rescue"] += 1
|
|
192
|
+
for m in modes:
|
|
193
|
+
agg["by_mode"][m]["false_rescue"] += 1
|
|
194
|
+
return agg
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _degrade(c_hgvs):
|
|
198
|
+
"""Yield (mode, category, degraded_string) — only degradations that keep the
|
|
199
|
+
SAME transcript, so the ground-truth g.HGVS still applies after repair."""
|
|
200
|
+
m = re.match(r"([A-Za-z]+_?\d+)\.(\d+):(c\..+)", c_hgvs)
|
|
201
|
+
if not m:
|
|
202
|
+
return
|
|
203
|
+
base, ver, rest = m.group(1), m.group(2), m.group(3)
|
|
204
|
+
tx = f"{base}.{ver}"
|
|
205
|
+
# cleaning — string-formatting damage clean_hgvs() should fully reverse
|
|
206
|
+
yield "cleaning", "whitespace", f"{tx} {rest}"
|
|
207
|
+
yield "cleaning", "lowercase_tx", f"{tx.lower()}:{rest}"
|
|
208
|
+
yield "cleaning", "missing_kind", f"{tx}:{rest[2:]}" # drop the "c."
|
|
209
|
+
yield "cleaning", "double_colon", f"{tx}::{rest}"
|
|
210
|
+
# version bump — request a version that is (almost certainly) absent, forcing
|
|
211
|
+
# the adjacent-version fallback; correct iff the chosen version reproduces g.
|
|
212
|
+
yield "version_bump", "absent_version", f"{base}.99:{rest}"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def run_degraded(am, hp, provider, build, pairs, version_fallback):
|
|
216
|
+
"""Exercise the fix_hgvs() rescue path on (clean) test data by degrading each
|
|
217
|
+
pair per mode, then scoring the repaired string against the ground-truth g."""
|
|
218
|
+
cats = {}
|
|
219
|
+
for g_hgvs, c_hgvs in pairs:
|
|
220
|
+
for mode, cat, broken in _degrade(c_hgvs):
|
|
221
|
+
d = cats.setdefault((mode, cat),
|
|
222
|
+
{"n": 0, "resolved": 0, "correct": 0, "false_rescue": 0})
|
|
223
|
+
d["n"] += 1
|
|
224
|
+
bucket, _fixed, _modes = _fix_and_classify(
|
|
225
|
+
am, hp, provider, build, g_hgvs, broken, version_fallback)
|
|
226
|
+
if bucket in ("correct", "incorrect"):
|
|
227
|
+
d["resolved"] += 1
|
|
228
|
+
if bucket == "correct":
|
|
229
|
+
d["correct"] += 1
|
|
230
|
+
elif bucket == "incorrect":
|
|
231
|
+
d["false_rescue"] += 1
|
|
232
|
+
return cats
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def pct(n, d):
|
|
236
|
+
return f"{100 * n / d:.1f}%" if d else "n/a"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main():
|
|
240
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
241
|
+
ap.add_argument("pairs_file", help="TSV: g.HGVS <TAB> c.HGVS")
|
|
242
|
+
g = ap.add_mutually_exclusive_group()
|
|
243
|
+
g.add_argument("--rest", action="store_true", help="cdot REST (default)")
|
|
244
|
+
g.add_argument("--rest-insecure", action="store_true")
|
|
245
|
+
g.add_argument("--json", help="cdot JSON.gz file")
|
|
246
|
+
g.add_argument("--uta", action="store_true")
|
|
247
|
+
ap.add_argument("--build", default="GRCh38")
|
|
248
|
+
ap.add_argument("--fasta", nargs="+", help="genome FASTA(s) for local sequence fetching (avoids remote NCBI)")
|
|
249
|
+
ap.add_argument("--replace-reference", action="store_true",
|
|
250
|
+
help="validate ref base (needs a seqfetcher); off = pure coordinate projection")
|
|
251
|
+
ap.add_argument("--prefetch", action="store_true", help="(REST) warm the transcript cache concurrently before resolving")
|
|
252
|
+
ap.add_argument("--prefetch-workers", type=int, default=10)
|
|
253
|
+
ap.add_argument("--recovery", action="store_true", help="also run cleaning + version-bump experiments")
|
|
254
|
+
ap.add_argument("--show-incorrect", action="store_true")
|
|
255
|
+
ap.add_argument("--out", help="write JSON results here")
|
|
256
|
+
ap.add_argument("--debug", action="store_true")
|
|
257
|
+
args = ap.parse_args()
|
|
258
|
+
logging.basicConfig(level=logging.DEBUG if args.debug else logging.WARNING)
|
|
259
|
+
|
|
260
|
+
pairs = load_pairs(args.pairs_file)
|
|
261
|
+
|
|
262
|
+
t0 = time.perf_counter()
|
|
263
|
+
provider, label = build_provider(args)
|
|
264
|
+
load_time = time.perf_counter() - t0
|
|
265
|
+
|
|
266
|
+
am = AssemblyMapper(provider, assembly_name=args.build, alt_aln_method="splign",
|
|
267
|
+
replace_reference=args.replace_reference)
|
|
268
|
+
hp = hgvs.parser.Parser()
|
|
269
|
+
|
|
270
|
+
prefetch_time = None
|
|
271
|
+
if args.prefetch:
|
|
272
|
+
if not hasattr(provider, "prefetch"):
|
|
273
|
+
print(f" (provider {label} has no prefetch(); ignoring --prefetch)")
|
|
274
|
+
else:
|
|
275
|
+
tx_acs = sorted({c.split(":", 1)[0] for _g, c in pairs if ":" in c})
|
|
276
|
+
t0 = time.perf_counter()
|
|
277
|
+
n = provider.prefetch(tx_acs, max_workers=args.prefetch_workers)
|
|
278
|
+
prefetch_time = time.perf_counter() - t0
|
|
279
|
+
print(f" prefetched {n}/{len(tx_acs)} transcripts in {prefetch_time:.2f}s "
|
|
280
|
+
f"({args.prefetch_workers} workers)")
|
|
281
|
+
|
|
282
|
+
print(f"== Resolution: {label} | {len(pairs)} pairs | {args.build} "
|
|
283
|
+
f"| replace_reference={args.replace_reference} ==")
|
|
284
|
+
counts, times, wall = run_resolution(am, hp, pairs, args.show_incorrect)
|
|
285
|
+
resolved = counts["correct"] + counts["incorrect"]
|
|
286
|
+
total = len(pairs)
|
|
287
|
+
print(f" load_time : {load_time:.2f}s")
|
|
288
|
+
print(f" correct : {counts['correct']:>5} ({pct(counts['correct'], total)})")
|
|
289
|
+
print(f" incorrect : {counts['incorrect']:>5} ({pct(counts['incorrect'], total)})")
|
|
290
|
+
print(f" no_data (missing) : {counts['no_data']:>5} ({pct(counts['no_data'], total)})")
|
|
291
|
+
print(f" error : {counts['error']:>5} ({pct(counts['error'], total)})")
|
|
292
|
+
print(f" resolved (any g.) : {resolved:>5} ({pct(resolved, total)})")
|
|
293
|
+
print(f" throughput : {total / wall:.1f} HGVS/s (wall {wall:.2f}s)")
|
|
294
|
+
|
|
295
|
+
results = {"provider": label, "build": args.build, "n": total,
|
|
296
|
+
"replace_reference": args.replace_reference,
|
|
297
|
+
"load_time_s": round(load_time, 2), "counts": counts,
|
|
298
|
+
"prefetch_s": round(prefetch_time, 2) if prefetch_time is not None else None,
|
|
299
|
+
"throughput_hgvs_per_s": round(total / wall, 1), "wall_s": round(wall, 2)}
|
|
300
|
+
|
|
301
|
+
if args.recovery:
|
|
302
|
+
vf = VersionStrategy.UP_THEN_DOWN
|
|
303
|
+
|
|
304
|
+
print("\n== Recovery (as-is): fix_hgvs() on each pair, attributed by the fixes returned ==")
|
|
305
|
+
asis = run_asis(am, hp, provider, args.build, pairs, vf)
|
|
306
|
+
print(f" baseline correct : {asis['baseline_correct']:>5} ({pct(asis['baseline_correct'], asis['n'])})")
|
|
307
|
+
print(f" after fix_hgvs : {asis['fixed_correct']:>5} ({pct(asis['fixed_correct'], asis['n'])})")
|
|
308
|
+
print(f" rescued by fix : {asis['rescued']:>5} ({pct(asis['rescued'], asis['n'])})")
|
|
309
|
+
print(f" false-rescue : {asis['false_rescue']:>5} (resolved, but to the wrong g.)")
|
|
310
|
+
print(f" regressed : {asis['regressed']:>5} (was correct, broken by fix_hgvs)")
|
|
311
|
+
for mode, d in sorted(asis["by_mode"].items()):
|
|
312
|
+
print(f" mode {mode:<16} fired={d['fired']:<4} rescued={d['rescued']:<4} false_rescue={d['false_rescue']}")
|
|
313
|
+
if not asis["by_mode"]:
|
|
314
|
+
print(" (no fixes fired — test pairs are already clean; see the degraded pass below)")
|
|
315
|
+
results["recovery_asis"] = asis
|
|
316
|
+
|
|
317
|
+
print("\n== Recovery (degraded): degrade per mode -> fix_hgvs() -> resolve vs ground truth ==")
|
|
318
|
+
degraded = run_degraded(am, hp, provider, args.build, pairs, vf)
|
|
319
|
+
for (mode, cat), d in sorted(degraded.items()):
|
|
320
|
+
print(f" {mode:<14} {cat:<16} n={d['n']:<4} "
|
|
321
|
+
f"correct={pct(d['correct'], d['n']):>6} "
|
|
322
|
+
f"false_rescue={pct(d['false_rescue'], d['n']):>6}")
|
|
323
|
+
results["recovery_degraded"] = {f"{m}/{c}": d for (m, c), d in degraded.items()}
|
|
324
|
+
|
|
325
|
+
if args.out:
|
|
326
|
+
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
|
|
327
|
+
Path(args.out).write_text(json.dumps(results, indent=2))
|
|
328
|
+
print(f"\nWritten: {args.out}")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|