Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion assets/acmg_sf.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
# report_if when a P/LP variant (ClinVar, or a rare HIGH-impact record) is a secondary finding:
# any one variant is enough
# biallelic two variants: a homozygote, or two different variants even when
# their phase is unknown (Table 1 footnote a)
# their phase is unknown (Table 1 footnote a); a called deletion
# of the gene (AnnotSV, step 35's copy number) counts as one of the
# two unless read-backed phase puts it on the variant's copy
# (bin/collect_summary.py two_hits); so it does for hemizygous
# hom_variant only a variant of the variants column, homozygous
# hemizygous a hemizygous or homozygous variant, or two heterozygous ones; a
# single heterozygous variant is not reported
Expand Down
6 changes: 4 additions & 2 deletions assets/carrier_genes.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
# severity: alleles of the gene that are not a full loss of function, separated by ";": each is
# hypomorph or pseudodeficiency, then chrom:pos:ref:alt (GRCh38), then a name; "." for none. Such
# an allele never makes a carrier finding on its own (the couple screen, step 39, and the
# carrier list of the sample report).
# carrier list of the sample report). With a called deletion of the gene it is a second hit (the
# two-hit lines of the sample report, bin/collect_summary.py two_hits). In the couple screen a
# hypomorph beside a full loss of function in the other partner's same gene is a shared gene.
# Rebuilt with: couple_report.py genes (docs/39-couple.md says how).
gene chrom start end inheritance lists condition severity
ISG15 chr1 1001138 1014540 AR Orphanet Mendelian susceptibility to mycobacterial diseases due to complete ISG15 deficiency .
Expand Down Expand Up @@ -186,7 +188,7 @@ HSD3B2 chr1 119413725 119453802 AR Orphanet Congenital adrenal hyperplasia due t
PHGDH chr1 119648411 119744227 AR Orphanet 3-phosphoglycerate dehydrogenase deficiency, infantile/juvenile form; and 1 other .
HMGCS2 chr1 119747979 119769092 AR Orphanet 3-hydroxy-3-methylglutaryl-CoA synthase deficiency .
PEX11B chr1 145911348 145918747 AR Orphanet Infantile Refsum disease; and 2 others .
RBM8A chr1 145920935 145928067 AR Orphanet Thrombocytopenia-absent radius syndrome .
RBM8A chr1 145920935 145928067 AR Orphanet Thrombocytopenia-absent radius syndrome hypomorph chr1:145927447:C:T c.-21G>A (rs139428292), a low-expression allele: TAR syndrome with a null allele such as the 1q21.1 deletion on the other copy; hypomorph chr1:145927328:C:G c.67+32G>C (rs201779890), the other low-expression allele of TAR syndrome
HJV chr1 146017468 146036746 AR Orphanet HJV or HAMP-related hemochromatosis .
GJA8 chr1 147902795 147909269 AR Orphanet Cataract-microcornea syndrome; and 3 others .
NOTCH2NLC chr1 149390534 149471833 AR Orphanet Oculopharyngodistal myopathy .
Expand Down
259 changes: 244 additions & 15 deletions bin/collect_summary.py

Large diffs are not rendered by default.

39 changes: 30 additions & 9 deletions bin/couple_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,12 @@ def common_match(benign, chrom, s0, e0):

# --- findings ----------------------------------------------------------------

def finding(gene, text, copies):
"""copies: how many of the person's copies the finding is on (1 or 2)."""
return {"gene": gene, "text": text, "copies": copies}
def finding(gene, text, copies, hypomorph=None):
"""copies: how many of the person's copies the finding is on (1 or 2).
hypomorph: the name of a low-function allele of the list's severity
column, which counts only beside a full loss of function in the partner
(screen_couple)."""
return {"gene": gene, "text": text, "copies": copies, "hypomorph": hypomorph}


def zygosity(gt, chrom, sex, par):
Expand All @@ -259,7 +262,11 @@ def stars_text(n):
def clinvar_findings(path, genes, sex, par, aside=None):
"""Step 06's hits in genes of the list, and the count of all hits. A hit
the list marks as a hypomorph or pseudodeficiency allele (its severity
column) is no carrier finding: it goes to aside as a text."""
column) is no carrier finding on its own: it goes to aside as a text. A
hypomorph is also returned as a finding marked hypomorph: beside a full
loss of function in the other partner's same gene it is the second hit of
the child (TAR syndrome is a null allele plus an RBM8A low-expression
allele); screen_couple drops it otherwise. total counts every hit."""
out, total = [], 0
for r in collect_summary.vcf_records(path):
if len(r) < 8:
Expand All @@ -276,6 +283,10 @@ def clinvar_findings(path, genes, sex, par, aside=None):
if aside is not None:
aside.append("%s:%d %s>%s %s in %s: %s, a %s allele" % (
h["chrom"], h["pos"], h["ref"], h["alt"], z, g, sev["name"], sev["kind"]))
if sev["kind"] == "hypomorph":
text = "%s:%d %s>%s %s, %s, a hypomorph allele" % (h["chrom"], h["pos"], h["ref"], h["alt"],
z, sev["name"])
out.append(finding(g, text, 1 if z == "het" else 2, sev["name"]))
continue
if g in genes:
ident = "" if r[2] in (".", "") else ", ClinVar %s" % r[2]
Expand Down Expand Up @@ -485,6 +496,11 @@ def ar_row(g, a, b, fa, fb):
more = [n for n, f in ((a, fa), (b, fb)) if len(f) > 1 and all(x["copies"] == 1 for x in f)]
why = ("Both partners carry a change in %s, linked to %s (autosomal recessive). A child who inherits "
"the change of each parent can have the condition: %s." % (g["gene"], g["condition"], risk))
low = [n for n, f in ((a, fa), (b, fb)) if f and all(x.get("hypomorph") for x in f)]
if low:
why += (" The change of %s is a low-function (hypomorph) allele, not a full loss of function: with the "
"other parent's loss-of-function change a child can have the condition, for some genes in a "
"milder form; two such alleles alone are set aside." % " and ".join(low))
if more:
why += (" %s %s two changes in the gene; when they sit on different copies, every child gets one, "
"and the chance is higher." % (" and ".join(more), "has" if len(more) == 1 else "have"))
Expand Down Expand Up @@ -520,10 +536,13 @@ def xl_lines(g, who, sex, f):
def screen_couple(a, b, sexes, found, genes):
"""Rows of the couple table, shared genes first, then X-linked, then the rest."""
rows = []
for name in sorted(set(x["gene"] for x in found[a] + found[b])):
# A hypomorph (finding marked hypomorph) counts only when the other
# partner has a full loss of function in the same gene.
full = {s: set(x["gene"] for x in found[s] if not x.get("hypomorph")) for s in (a, b)}
for name in sorted(full[a] | full[b]):
g = genes[name]
fa = [x for x in found[a] if x["gene"] == name]
fb = [x for x in found[b] if x["gene"] == name]
fa = [x for x in found[a] if x["gene"] == name and (not x.get("hypomorph") or name in full[b])]
fb = [x for x in found[b] if x["gene"] == name and (not x.get("hypomorph") or name in full[a])]
if g["inheritance"] == "XL":
parts = [xl_lines(g, w, sexes.get(w), f) for w, f in ((a, fa), (b, fb)) if f]
result, risk, why = "x_linked", "; ".join(p[0] for p in parts), " ".join(p[1] for p in parts)
Expand Down Expand Up @@ -686,7 +705,7 @@ def cmd_screen(a):
gaps.append("Carrier deletion alleles of %s: no interval table (step 35)." % s)
aside = []
f, total = clinvar_findings(clinvar[s], genes, sexes[s], par, aside)
info = {"clinvar": "%d / %d" % (len(f), total)}
info = {"clinvar": "%d / %d" % (sum(1 for x in f if not x["hypomorph"]), total)}
for note in aside:
gaps.append("Set aside for %s, not a carrier finding on its own: %s." % (s, note))
if s not in delly:
Expand Down Expand Up @@ -860,7 +879,9 @@ def place(g):
"# severity: alleles of the gene that are not a full loss of function, separated by \";\": each is",
"# hypomorph or pseudodeficiency, then chrom:pos:ref:alt (GRCh38), then a name; \".\" for none. Such",
"# an allele never makes a carrier finding on its own (the couple screen, step 39, and the",
"# carrier list of the sample report).",
"# carrier list of the sample report). With a called deletion of the gene it is a second hit (the",
"# two-hit lines of the sample report, bin/collect_summary.py two_hits). In the couple screen a",
"# hypomorph beside a full loss of function in the other partner's same gene is a shared gene.",
"# Rebuilt with: couple_report.py genes (docs/39-couple.md says how).",
]
with open(a.out, "w", encoding="utf-8") as f:
Expand Down
32 changes: 26 additions & 6 deletions bin/render_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -1635,7 +1635,7 @@ def sf_rows(sf):
the same in both reports: ClinVar hits, rare HIGH-impact records, then
class 4 and 5 SVs."""
def extra(x):
return "".join(f"; {v}" for v in (x.get("severity"), x.get("rule_note")) if v)
return "".join(f"; {v}" for v in (x.get("severity"), x.get("rule_note"), x.get("two_hit")) if v)
rows = [(x["gene"], x["variant"], x["genotype"],
"ClinVar " + x["significance"] + ("; low-confidence call: check the reads" if x.get("low_confidence") else "")
+ extra(x)) for x in sf["clinvar_hits"]]
Expand All @@ -1655,8 +1655,9 @@ def sf_apart_lines(sf):
no rule could be applied."""
if sf.get("unavailable"):
return [f"Secondary findings: not assessed, {sf['unavailable']}"]
return [f"{x['gene']} {x['variant']} {x.get('genotype') or '.'} ({x['source']}): not reported, {x['reason']}"
for x in sf.get("not_reported", [])]
return ([f"{x['gene']} {x['variant']} {x.get('genotype') or '.'} ({x['source']}): not reported, {x['reason']}"
for x in sf.get("not_reported", [])]
+ ([sf["two_hit_note"]] if sf.get("two_hit_note") else []))


def carrier_lines(c):
Expand All @@ -1665,12 +1666,31 @@ def carrier_lines(c):
return [f"Carrier list: not made, {c['unavailable']}"]
lines = [f"Carrier list ({c['genes_on_list']} genes): {len(c['carriers'])} finding(s)"
+ (f", {len(c['set_aside'])} set aside" if c["set_aside"] else "")]
def hemi(x):
return f" ({x['hemizygous']})" if x.get("hemizygous") else ""
for x in c["carriers"]:
lines.append(f"{x['gene']} ({x['condition']}; {x['inheritance']}): {x['variant']} {x['genotype']}, "
lines.append(f"{x['gene']} ({x['condition']}; {x['inheritance']}): {x['variant']} {x['genotype']}{hemi(x)}, "
f"ClinVar {x['significance']}")
for x in c["set_aside"]:
lines.append(f"Set aside: {x['gene']} {x['variant']} {x['genotype']}: {x['name']} is a {x['kind']} allele, "
"not a carrier finding on its own")
lines.append(f"Set aside: {x['gene']} {x['variant']} {x['genotype']}{hemi(x)}: {x['name']} is a {x['kind']} "
"allele, not a carrier finding on its own")
return lines + two_hit_lines(c.get("two_hits"))


def two_hit_lines(t):
"""The genes a called deletion and a ClinVar hit both reach (collect_summary.two_hits), and which deletion
callers were read; none for a summary written before the rule."""
if not t:
return []
lines = []
for x in t["genes"]:
allele = f" ({x['allele']})" if x.get("allele") else ""
lines.append(f"{x['verdict'].capitalize()}: {x['gene']} ({x['condition']}; AR), both copies may be hit: "
f"{x['deletion']} and {x['variant']} {x['genotype']}{allele}; {x['why']}")
lines.append("Two hits across callers (a deletion plus a variant on the other copy): "
+ (f"{len(t['genes'])} gene(s); " if t["genes"] else "none found; ")
+ "deletions read from " + ("; ".join(t["deletions_read"]) or "no caller")
+ (f"; not read: {'; '.join(t['deletions_not_read'])}" if t["deletions_not_read"] else ""))
return lines


Expand Down
10 changes: 9 additions & 1 deletion docs/interpreting-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,15 @@ The most common "pathogenic" finding in any genome is **heterozygous carrier sta
- BTD with p.Asp444His: reported as partial, not profound, biotinidase deficiency;
- AnnotSV class 4 and 5 structural variants (step 5) in BRCA1, BRCA2, MLH1, MSH2 and LDLR reach the same card, and so does a deletion of the 3' end of EPCAM (exons 8 and 9), which silences MSH2 (Kovacs et al., Hum Mutat 2009;30(2):197-203).

A deletion in trans with a small variant in a recessive gene is not counted as two variants yet. The TTN rule (truncating variants only) and the LMNA rule (missense only with cardiac evidence) are printed beside the finding, not applied. It is still a list to review with a clinician.
**Two hits across callers: a deletion plus a variant on the other copy.** Small-variant callers see one hit when the other copy of the gene is deleted. So the carrier list also joins the deletions the other engines call with the ClinVar hits, gene by gene (`bin/collect_summary.py`, `two_hits`). The deletions come from AnnotSV's class 4 and 5 deletions (step 5), step 35's interval table (the recurrent regions of `bin/cn_loci.tsv`, such as 1q21.1), its HBA1/HBA2 count and Parascopy's SMN1 count. A recessive gene that a deletion and a hit both reach gets one line:

- **biallelic** when the reads show the two hits on different copies. Either the hit is called homozygous inside the deletion, so it sits on the one copy left (the line and the carrier list call it hemizygous), or both calls carry read-backed phase (a `|` genotype with the same phase set, PS) on different copies;
- **possibly biallelic** otherwise: the phase is unknown. Most short-read calls carry no phase, so this is the usual line. Step 05's checked table keeps no genotype column yet, and step 35's counts have no phase, so in a pipeline run a deletion pairs as possibly biallelic unless the hit is called homozygous inside it. A parent's genome (step 38, [family analysis](multi-sample.md#family-analysis-step-38-opt-in)) or long reads can settle it;
- no line when read-backed phase puts both on the same copy (cis): the other copy is intact.

Examples: an exon deletion of MUTYH plus a pathogenic MUTYH variant; three alpha-globin genes plus an HBA2 point variant; one SMN1 copy plus an SMN1 variant (short reads place few SMN1 variants reliably, so check with a targeted test); and the 1q21.1 deletion plus one of the two RBM8A low-expression alleles (c.-21G>A, rs139428292, or c.67+32G>C, rs201779890), the cause of TAR syndrome (thrombocytopenia with absent radius; Albers et al., Nat Genet 2012;44(4):435-439, PMID 22366785). On its own an RBM8A low-expression allele stays set aside. The [couple screen](39-couple.md) (step 39) applies the same pairing between partners: a hypomorph allele of the list in one partner and a full loss of function in the same gene in the other (a ClinVar pathogenic variant, a Delly deletion) is a shared gene with its risk for a child; the allele in both partners, or in one alone, stays set aside. In the secondary-findings card a called deletion counts as one of the two variants a recessive gene needs, unless read-backed phase puts it on the variant's copy. The line under the carrier list names the deletion callers it read and the ones it could not read. A deletion AnnotSV ranks below class 4 is not counted.

The TTN rule (truncating variants only) and the LMNA rule (missense only with cardiac evidence) are printed beside the finding, not applied. It is still a list to review with a clinician.

---

Expand Down
Loading
Loading