Skip to content
This repository was archived by the owner on Jun 15, 2026. It is now read-only.

Commit e9ce281

Browse files
committed
v1.6.0: CADD v1.7 variant deleteriousness scores
PHRED-scaled deleteriousness enrichment from CADD v1.7. Two modes: cache (pre-built SQLite, ~5GB, ~120M keys) and full (81GB tabix via pysam, GRCh38 only). Enrichment-only annotator following the gnomAD/AlphaMissense pattern. Opt-in — disabled by default. Licensable-source gating: three-state permission model (ALLOW / BLOCK_FINAL / BLOCK_PURCHASABLE) with per-source license assertions. CADD is non-commercial by default but unlockable via commercial license; SNPedia is permanently blocked in commercial mode. Also: `config get` subcommand, strand normalization for array data, CADD cache build script with int64 packing and indel pass. 1215 tests, 92%+ coverage.
1 parent 6f018b4 commit e9ce281

36 files changed

Lines changed: 2927 additions & 75 deletions

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,33 @@
22

33
All notable changes are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
44

5+
## [1.6.0]
6+
7+
### Added
8+
- **CADD v1.7 variant deleteriousness scores (ADR-0032).** PHRED-scaled
9+
scores ranking how deleterious any single-nucleotide variant is, using
10+
100+ annotation tracks. Enrichment-only annotator following the
11+
gnomAD/AlphaMissense pattern. Two modes: cache (pre-built SQLite from
12+
HuggingFace, ~5 GB, ~120M variant keys) and full (81 GB tabix file via pysam, GRCh38
13+
only). CADD column appears in HTML, terminal, and JSON reports when
14+
scores are present.
15+
- **Non-commercial source opt-in pattern.** CADD is the first source
16+
with `commercial_ok=False`. Disabled by default (`sources.cadd =
17+
false`). Users opt in via `allelix config set sources.cadd true` or
18+
`allelix db update --cadd`. First download shows a license
19+
confirmation prompt.
20+
- **Strand normalization for array data.** `resolve_strand()` maps
21+
array-reported alleles to reference-forward orientation using gnomAD
22+
ref/alt as ground truth. Palindromic SNPs (A/T, C/G) return None
23+
rather than guessing.
24+
- **CADD cache build script.** `scripts/build_cadd_cache.py` filters
25+
the full CADD SNV and indel files to positions present in gnomAD,
26+
AlphaMissense, and ClinVar (GRCh38). Uses int64 packing for SNV
27+
keys to fit the ~120M position set (117M SNV + 3M indel).
28+
- **`options.cadd_full` config key.** Enables full CADD mode (tabix
29+
queries against the complete CADD file). Requires `pip install
30+
allelix[cadd]` for pysam.
31+
532
## [1.5.3]
633

734
### Fixed
@@ -1502,6 +1529,7 @@ All notable changes are documented here. Format follows [Keep a Changelog](https
15021529
- GitHub Actions CI matrix on Python 3.11 and 3.12.
15031530

15041531

1532+
[1.6.0]: https://github.com/dial481/allelix/compare/v1.5.3...v1.6.0
15051533
[1.5.3]: https://github.com/dial481/allelix/compare/v1.5.2...v1.5.3
15061534
[1.5.2]: https://github.com/dial481/allelix/compare/v1.5.1...v1.5.2
15071535
[1.5.1]: https://github.com/dial481/allelix/compare/v1.5.0...v1.5.1

CONTRIBUTING.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ from __future__ import annotations
235235
import sqlite3
236236
from typing import TYPE_CHECKING, ClassVar
237237

238-
from allelix.annotators.base import Annotator
238+
from allelix.annotators.base import Annotator, LicenseDescriptor
239239
from allelix.models import Annotation
240240

241241
if TYPE_CHECKING:
@@ -249,6 +249,13 @@ class MyDBAnnotator(Annotator):
249249
display_name: ClassVar[str] = "MyDB"
250250
attribution: ClassVar[str] = "MyDB"
251251
requires_download: ClassVar[bool] = True
252+
license: ClassVar[LicenseDescriptor] = LicenseDescriptor(
253+
spdx="CC-BY-4.0",
254+
license_url="https://example.com/mydb/license",
255+
attribution_text="MyDB variant data.",
256+
source_url="https://example.com/mydb",
257+
commercial_ok=True,
258+
)
252259

253260
def __init__(self, data_dir: Path) -> None:
254261
super().__init__(data_dir)

README.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
Open-source command-line toolkit for analyzing raw genotype files from consumer DNA testing services. Format-agnostic ingestion, database-agnostic annotation, offline-first.
44

55
> **Status:** Production — six parser formats, four annotators (ClinVar +
6-
> PharmGKB + GWAS Catalog + SNPedia), two enrichment sources (gnomAD
7-
> population frequencies + AlphaMissense pathogenicity), dual-build
8-
> ClinVar caches (GRCh37 + GRCh38), HTML/JSON/terminal reports,
9-
> methylation + pharmacogenomics focused commands, report diffing,
10-
> persistent config with commercial-mode safety switch. Build
11-
> auto-detection from position data (ADR-0021). No regex on prose
12-
> anywhere in production. Release notes: [`CHANGELOG.md`](CHANGELOG.md).
6+
> PharmGKB + GWAS Catalog + SNPedia), three enrichment sources (gnomAD
7+
> population frequencies + AlphaMissense pathogenicity + CADD
8+
> deleteriousness), licensable-source gating for commercial users,
9+
> dual-build ClinVar caches (GRCh37 + GRCh38),
10+
> HTML/JSON/terminal reports, methylation + pharmacogenomics focused
11+
> commands, report diffing, persistent config with commercial-mode
12+
> safety switch. Build auto-detection from position data (ADR-0021).
13+
> No regex on prose anywhere in production. Release notes:
14+
> [`CHANGELOG.md`](CHANGELOG.md).
1315
1416
## Quickstart
1517

@@ -31,6 +33,7 @@ allelix stats tests/fixtures/mock_myhappygenes.txt
3133
# Download reference databases. First run downloads all sources (~15GB
3234
# on disk with gnomAD + AlphaMissense). Use --no-gnomad / --no-alphamissense
3335
# to skip the large enrichment databases. Re-runs skip unchanged sources.
36+
# CADD is opt-in: allelix db update --cadd
3437
allelix db update
3538
allelix db status # see what's cached
3639

@@ -80,6 +83,7 @@ Adding a new format means adding one file to `allelix/parsers/` and registering
8083
| GWAS Catalog || Public domain (EBI/NHGRI). Trait–SNP associations with p-values and effect sizes. Carrier rule (ADR-0007) requires the user to carry the risk allele. P-value magnitude scoring (ADR-0024) maps continuous p-values to the 0–10 scale; unknown-risk-allele entries fire on rsID match alone but are capped at 3.0. |
8184
| gnomAD || ODbL v1.0. **Enrichment annotator** — adds population allele frequency context to existing annotations. Shows how common each variant is in the general population (~16M exome variants from 730K individuals). A pathogenic variant that 35% of people carry reads very differently from one seen in 0.001%. Pre-built cache downloaded via `db update` (~6GB on disk). Use `--no-gnomad` to skip. |
8285
| AlphaMissense || CC BY 4.0. **Enrichment annotator** — adds DeepMind's protein-structure-based pathogenicity predictions to existing annotations. Scores 71M missense variants on a 0–1 scale: <0.34 = likely benign, >0.564 = likely pathogenic. Complements ClinVar's expert classifications with computational predictions — especially valuable for variants ClinVar hasn't reviewed yet. Pre-built cache downloaded via `db update` (~8GB on disk). Use `--no-alphamissense` to skip. |
86+
| CADD | ✓ | LicenseRef-CADD (non-commercial). **Enrichment annotator** — adds PHRED-scaled deleteriousness scores from CADD v1.7. Ranks how deleterious any single-nucleotide variant is using 100+ annotation tracks (coding, non-coding, regulatory). PHRED 10 = top 10% most deleterious, 20 = top 1%, 30 = top 0.1%. **Opt-in** — disabled by default (`sources.cadd = false`). Enable via `allelix db update --cadd` or `allelix config set sources.cadd true`. Pre-built cache (~5 GB on disk, ~120M variant keys). Full mode available via pysam for GRCh38 data (`options.cadd_full = true`). Cache mode covers the large majority of variants present in gnomAD, AlphaMissense, and ClinVar — nearly every position allelix can annotate from its other databases. For genotyping chip data (23andMe, AncestryDNA, MyHappyGenes, etc.), cache and full mode produce effectively identical results because chip probes overwhelmingly target known, cataloged variants. Full mode adds coverage for novel or private variants that appear only in whole-genome or whole-exome sequencing data and are not in any pre-computed database. If your input is a genotyping chip file, cache mode is all you need. |
8387

8488
### Known PharmGKB limitation: reference-genotype rows where ClinVar and CPIC both lack data
8589

@@ -119,17 +123,24 @@ This is not a disclaimer afterthought. It is a design constraint that affects mo
119123
Allelix stores persistent configuration in `config.toml` (in the data directory, default `~/.local/share/allelix/`). A default config is created on first run.
120124

121125
```bash
122-
# View current config
126+
# View current config (annotated with license notes)
123127
allelix config show
124128

129+
# Read a single key
130+
allelix config get sources.cadd
131+
allelix config get license.commercial
132+
125133
# Disable a source permanently
126134
allelix config set sources.gnomad false
127135

128-
# Enable commercial mode (auto-disables non-commercial sources like SNPedia)
136+
# Enable commercial mode (auto-disables non-commercial sources)
129137
allelix config set license.commercial true
138+
139+
# Assert that you hold a commercial CADD license
140+
allelix config set license.cadd true
130141
```
131142

132-
CLI flags (`--no-gnomad`, `--no-alphamissense`, `--exclude-snpedia`) override the config for a single run. The config sets the baseline; flags override per-invocation.
143+
CLI flags (`--no-gnomad`, `--no-alphamissense`, `--exclude-snpedia`, `--cadd`) override the config for a single run. The config sets the baseline; flags override per-invocation.
133144

134145
### Database sizes and download times
135146

@@ -142,6 +153,7 @@ Not all databases are equal in size. `allelix db update` downloads them all by d
142153
| GWAS Catalog | ~200MB | 1–2 min | Trait-SNP associations from genome-wide studies. |
143154
| gnomAD | ~6GB | 5–15 min | Population allele frequencies (how common is this variant?). |
144155
| AlphaMissense | ~8GB | 5–15 min | Missense pathogenicity predictions (how likely to break protein function?). |
156+
| CADD (opt-in) | ~5GB | 5–15 min | Variant deleteriousness scores (how damaging is this variant?). Enable with `--cadd`. |
145157

146158
gnomAD and AlphaMissense are the largest but add the most interpretive context. gnomAD answers "is this variant rare or common?" — a pathogenic variant carried by 35% of the population reads very differently from one seen in 3 people. AlphaMissense answers "does this missense change likely damage the protein?" — especially valuable for the thousands of variants ClinVar hasn't reviewed yet.
147159

@@ -160,8 +172,9 @@ Allelix source code is licensed under the **GNU Affero General Public License v3
160172
| SNPedia | snpedia.com | CC BY-NC-SA 3.0 US | Attribution required, **non-commercial only**. Use `--exclude-snpedia` to omit. |
161173
| gnomAD | gnomad.broadinstitute.org | ODbL v1.0 | Attribution required. Population allele frequencies for context; not a clinical annotator. Use `--no-gnomad` to omit. |
162174
| AlphaMissense | zenodo.org/records/10813168 | CC BY 4.0 | Attribution required. Cheng et al., Science 2023. Missense variant pathogenicity predictions. Use `--no-alphamissense` to omit. |
175+
| CADD | cadd.gs.washington.edu | LicenseRef-CADD | Attribution required, **non-commercial by default**. Commercial licenses available from UW CoMotion. Opt-in via `allelix db update --cadd`. |
163176

164-
**Commercial users:** SNPedia content is non-commercial. Set `allelix config set license.commercial true` to permanently disable non-commercial sources, or pass `--exclude-snpedia` per-invocation. Either way, `analyze` runs using all other databases and omits SNPedia annotations automatically. All other databases (ClinVar, PharmGKB, GWAS Catalog, gnomAD, AlphaMissense) are compatible with commercial use.
177+
**Commercial users:** When `license.commercial = true`, non-commercial sources are gated by a three-state permission model. SNPedia is permanently blocked (no commercial license is available). CADD is blocked by default but can be unlocked — the University of Washington offers commercial licenses at `https://els2.comotion.uw.edu/product/cadd-scores`; after purchasing, assert your license with `allelix config set license.cadd true` to re-enable CADD in commercial mode. All other databases (ClinVar, PharmGKB, GWAS Catalog, gnomAD, AlphaMissense) are compatible with commercial use. `allelix config show` displays the permission state for each source.
165178

166179
### SNPedia data download
167180

SECURITY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Only the latest minor release receives security fixes.
66

77
| Version | Supported |
88
|---------|-----------|
9-
| 1.5.x ||
10-
| < 1.5 ||
9+
| 1.6.x ||
10+
| < 1.6 ||
1111

1212
## Reporting a vulnerability
1313

allelix/annotators/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from allelix.annotators.alphamissense import AlphaMissenseAnnotator
1010
from allelix.annotators.base import Annotator
11+
from allelix.annotators.cadd import CaddAnnotator
1112
from allelix.annotators.clinvar import CLINVAR_SUPPORTED_BUILDS, ClinVarAnnotator
1213
from allelix.annotators.gnomad import GnomadAnnotator
1314
from allelix.annotators.gwas import GWASCatalogAnnotator
@@ -24,6 +25,7 @@ def get_annotators(
2425
*,
2526
include_benign: bool = False,
2627
gwas_filter_traits: bool = True,
28+
cadd_full: bool = False,
2729
) -> list[Annotator]:
2830
"""Construct all registered annotators bound to the given data directory.
2931
@@ -37,6 +39,9 @@ def get_annotators(
3739
`gwas_filter_traits` passes through to GWASCatalogAnnotator. Default
3840
True excludes common-trait noise categories (ADR-0024 amendment).
3941
42+
`cadd_full` enables CADD full mode (tabix queries against the
43+
complete 81 GB CADD file). Requires ``pysam`` and a local copy.
44+
4045
ADR-0023: ClinVar's `reference_for(rsid, build)` is wired into
4146
PharmGKB and SNPedia as the primary hom-ref suppression filter — the
4247
REF allele lookup universally determines whether the user is
@@ -48,7 +53,8 @@ def get_annotators(
4853
snpedia = SNPediaAnnotator(data_dir, clinvar_ref_provider=clinvar.reference_for)
4954
gnomad = GnomadAnnotator(data_dir)
5055
alphamissense = AlphaMissenseAnnotator(data_dir)
51-
return [clinvar, pharmgkb, gwas, snpedia, gnomad, alphamissense]
56+
cadd = CaddAnnotator(data_dir, full_mode=cadd_full)
57+
return [clinvar, pharmgkb, gwas, snpedia, gnomad, alphamissense, cadd]
5258

5359

5460
_ANNOTATOR_CLASSES: dict[str, type[Annotator]] = {
@@ -60,6 +66,7 @@ def get_annotators(
6066
SNPediaAnnotator,
6167
GnomadAnnotator,
6268
AlphaMissenseAnnotator,
69+
CaddAnnotator,
6370
]
6471
}
6572

@@ -72,6 +79,7 @@ def get_annotator_class(name: str) -> type[Annotator] | None:
7279
__all__ = [
7380
"AlphaMissenseAnnotator",
7481
"Annotator",
82+
"CaddAnnotator",
7583
"ClinVarAnnotator",
7684
"GWASCatalogAnnotator",
7785
"GnomadAnnotator",

allelix/annotators/alphamissense.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class AlphaMissenseAnnotator(Annotator):
6565
),
6666
source_url="https://zenodo.org/records/10813168",
6767
citation="Cheng et al., Science 2023 (doi:10.1126/science.adg7492)",
68+
commercial_ok=True,
6869
)
6970

7071
def __init__(self, data_dir: Path) -> None:

allelix/annotators/base.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import contextlib
88
from abc import ABC, abstractmethod
99
from dataclasses import dataclass
10+
from enum import Enum, auto
1011
from typing import TYPE_CHECKING, ClassVar
1112

1213
if TYPE_CHECKING:
@@ -26,6 +27,9 @@ class LicenseDescriptor:
2627
attribution_text: str
2728
source_url: str | None = None
2829
citation: str | None = None
30+
commercial_ok: bool | None = None
31+
licensable: bool = False
32+
purchase_url: str | None = None
2933

3034

3135
_NON_COMMERCIAL_SPDX: frozenset[str] = frozenset(
@@ -37,9 +41,37 @@ class LicenseDescriptor:
3741
)
3842

3943

40-
def is_non_commercial(spdx: str) -> bool:
41-
"""Return True if the SPDX identifier prohibits commercial use."""
42-
return spdx in _NON_COMMERCIAL_SPDX
44+
def is_non_commercial(descriptor: LicenseDescriptor) -> bool:
45+
"""Return True if the license prohibits commercial use."""
46+
if descriptor.commercial_ok is not None:
47+
return not descriptor.commercial_ok
48+
return descriptor.spdx in _NON_COMMERCIAL_SPDX
49+
50+
51+
class Permission(Enum):
52+
"""Three-state permission result for a source in the current license context."""
53+
54+
ALLOW = auto()
55+
BLOCK_FINAL = auto()
56+
BLOCK_PURCHASABLE = auto()
57+
58+
59+
def permission(
60+
descriptor: LicenseDescriptor,
61+
*,
62+
commercial: bool,
63+
license_held: bool,
64+
) -> Permission:
65+
"""Determine whether a source is permitted under the current license context."""
66+
if not commercial:
67+
return Permission.ALLOW
68+
if not is_non_commercial(descriptor):
69+
return Permission.ALLOW
70+
if not descriptor.licensable:
71+
return Permission.BLOCK_FINAL
72+
if license_held:
73+
return Permission.ALLOW
74+
return Permission.BLOCK_PURCHASABLE
4375

4476

4577
def is_clinvar_homref(
@@ -88,6 +120,22 @@ def __init_subclass__(cls, **kwargs: object) -> None:
88120
if not is_abstract and not hasattr(cls, "license"):
89121
msg = f"{cls.__name__} must declare a 'license' ClassVar of type LicenseDescriptor"
90122
raise TypeError(msg)
123+
if hasattr(cls, "license"):
124+
desc = cls.license
125+
if (
126+
desc.spdx.startswith("LicenseRef-") or desc.spdx.startswith("custom-")
127+
) and desc.commercial_ok is None:
128+
msg = (
129+
f"{cls.__name__} uses custom SPDX '{desc.spdx}' but "
130+
f"does not declare commercial_ok (True or False)"
131+
)
132+
raise TypeError(msg)
133+
if desc.licensable and desc.purchase_url is None:
134+
msg = (
135+
f"{cls.__name__} declares licensable=True but "
136+
f"purchase_url is None — set it explicitly"
137+
)
138+
raise TypeError(msg)
91139

92140
def __init__(self, data_dir: Path) -> None:
93141
"""Bind the annotator to a data directory (created elsewhere)."""

0 commit comments

Comments
 (0)