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.
Files changed (52) hide show
  1. cdot-0.2.28/PKG-INFO +180 -0
  2. cdot-0.2.28/README.md +141 -0
  3. cdot-0.2.28/analysis/benchmark_resolution.py +332 -0
  4. cdot-0.2.28/analysis/build_clinvar_pairs.py +101 -0
  5. cdot-0.2.28/analysis/clean_hgvs_search_csvs.py +116 -0
  6. cdot-0.2.28/analysis/combine_csv.py +11 -0
  7. cdot-0.2.28/analysis/compare_providers.py +35 -0
  8. cdot-0.2.28/analysis/compute_benchmark.py +125 -0
  9. cdot-0.2.28/analysis/compute_coverage.py +101 -0
  10. cdot-0.2.28/analysis/inject_and_clean.py +501 -0
  11. cdot-0.2.28/analysis/investigate_fails.py +59 -0
  12. {cdot-0.2.26 → cdot-0.2.28}/cdot/__init__.py +2 -2
  13. {cdot-0.2.26 → cdot-0.2.28}/cdot/data_release.py +33 -13
  14. cdot-0.2.28/cdot/hgvs/__init__.py +25 -0
  15. cdot-0.2.28/cdot/hgvs/clean.py +884 -0
  16. cdot-0.2.28/cdot/hgvs/dataproviders/__init__.py +6 -0
  17. cdot-0.2.28/cdot/hgvs/dataproviders/ensembl_tark_data_provider.py +486 -0
  18. {cdot-0.2.26 → cdot-0.2.28}/cdot/hgvs/dataproviders/fasta_seqfetcher.py +75 -51
  19. {cdot-0.2.26 → cdot-0.2.28}/cdot/hgvs/dataproviders/json_data_provider.py +299 -19
  20. cdot-0.2.28/cdot/hgvs/dataproviders/seqfetcher.py +147 -0
  21. cdot-0.2.28/cdot/hgvs/gene_hgvs.py +488 -0
  22. cdot-0.2.28/cdot/models.py +210 -0
  23. {cdot-0.2.26 → cdot-0.2.28}/cdot/pyhgvs/pyhgvs_transcript.py +4 -6
  24. cdot-0.2.28/cdot.egg-info/PKG-INFO +180 -0
  25. cdot-0.2.28/cdot.egg-info/SOURCES.txt +40 -0
  26. cdot-0.2.28/cdot.egg-info/requires.txt +28 -0
  27. cdot-0.2.28/cdot.egg-info/top_level.txt +8 -0
  28. cdot-0.2.28/pyproject.toml +68 -0
  29. cdot-0.2.28/setup.cfg +4 -0
  30. cdot-0.2.28/tests/test_clean_hgvs.py +440 -0
  31. cdot-0.2.28/tests/test_gene_hgvs.py +837 -0
  32. {cdot-0.2.26 → cdot-0.2.28}/tests/test_gff_parsers.py +73 -9
  33. cdot-0.2.28/tests/test_json_data_provider_ensembl.py +134 -0
  34. cdot-0.2.26/tests/test_json_data_provider.py → cdot-0.2.28/tests/test_json_data_provider_refseq.py +7 -1
  35. cdot-0.2.28/tests/test_models.py +214 -0
  36. cdot-0.2.28/tests/test_rest_data_provider_prefetch.py +269 -0
  37. cdot-0.2.28/tests/test_struct_dict_compat.py +90 -0
  38. cdot-0.2.26/PKG-INFO +0 -119
  39. cdot-0.2.26/README.md +0 -100
  40. cdot-0.2.26/cdot/hgvs/dataproviders/__init__.py +0 -2
  41. cdot-0.2.26/cdot/pyhgvs/__init__.py +0 -0
  42. cdot-0.2.26/cdot.egg-info/PKG-INFO +0 -119
  43. cdot-0.2.26/cdot.egg-info/SOURCES.txt +0 -21
  44. cdot-0.2.26/cdot.egg-info/requires.txt +0 -4
  45. cdot-0.2.26/cdot.egg-info/top_level.txt +0 -1
  46. cdot-0.2.26/pyproject.toml +0 -6
  47. cdot-0.2.26/setup.cfg +0 -36
  48. {cdot-0.2.26 → cdot-0.2.28}/LICENSE +0 -0
  49. {cdot-0.2.26/cdot/hgvs → cdot-0.2.28/cdot/pyhgvs}/__init__.py +0 -0
  50. {cdot-0.2.26 → cdot-0.2.28}/cdot.egg-info/dependency_links.txt +0 -0
  51. {cdot-0.2.26 → cdot-0.2.28}/tests/test_pyhgvs.py +0 -0
  52. {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
+ [![PyPi version](https://img.shields.io/pypi/v/cdot.svg)](https://pypi.org/project/cdot/) [![Python versions](https://img.shields.io/pypi/pyversions/cdot.svg)](https://pypi.org/project/cdot/) [![tests](https://github.com/SACGF/cdot/actions/workflows/tests.yml/badge.svg)](https://github.com/SACGF/cdot/actions/workflows/tests.yml) [![DOI](https://zenodo.org/badge/448753921.svg)](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
+ [![PyPi version](https://img.shields.io/pypi/v/cdot.svg)](https://pypi.org/project/cdot/) [![Python versions](https://img.shields.io/pypi/pyversions/cdot.svg)](https://pypi.org/project/cdot/) [![tests](https://github.com/SACGF/cdot/actions/workflows/tests.yml/badge.svg)](https://github.com/SACGF/cdot/actions/workflows/tests.yml) [![DOI](https://zenodo.org/badge/448753921.svg)](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()