use crate::liftover::assembly_report::AssemblyReport;
use crate::reference::transcript::GenomeBuild;
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct ContigAliases {
to_refseq: HashMap<(String, GenomeBuild), String>,
refseq_to_ucsc: HashMap<String, String>,
refseq_to_ensembl: HashMap<String, String>,
}
impl ContigAliases {
pub fn new() -> Self {
Self::default()
}
pub fn default_human() -> Self {
let mut aliases = Self::new();
for i in 1..=22 {
let ucsc = format!("chr{}", i);
let ensembl = format!("{}", i);
let refseq_37 = format!("NC_{:06}.{}", i, Self::grch37_version(i));
let refseq_38 = format!("NC_{:06}.{}", i, Self::grch38_version(i));
aliases.add_alias(&ucsc, GenomeBuild::GRCh37, &refseq_37);
aliases.add_alias(&ensembl, GenomeBuild::GRCh37, &refseq_37);
aliases.add_alias(&refseq_37, GenomeBuild::GRCh37, &refseq_37);
aliases.add_alias(&ucsc, GenomeBuild::GRCh38, &refseq_38);
aliases.add_alias(&ensembl, GenomeBuild::GRCh38, &refseq_38);
aliases.add_alias(&refseq_38, GenomeBuild::GRCh38, &refseq_38);
aliases
.refseq_to_ucsc
.insert(refseq_37.clone(), ucsc.clone());
aliases
.refseq_to_ucsc
.insert(refseq_38.clone(), ucsc.clone());
aliases.refseq_to_ensembl.insert(refseq_37, ensembl.clone());
aliases.refseq_to_ensembl.insert(refseq_38, ensembl);
}
aliases.add_alias("chrX", GenomeBuild::GRCh37, "NC_000023.10");
aliases.add_alias("X", GenomeBuild::GRCh37, "NC_000023.10");
aliases.add_alias("NC_000023.10", GenomeBuild::GRCh37, "NC_000023.10");
aliases.add_alias("chrX", GenomeBuild::GRCh38, "NC_000023.11");
aliases.add_alias("X", GenomeBuild::GRCh38, "NC_000023.11");
aliases.add_alias("NC_000023.11", GenomeBuild::GRCh38, "NC_000023.11");
aliases
.refseq_to_ucsc
.insert("NC_000023.10".to_string(), "chrX".to_string());
aliases
.refseq_to_ucsc
.insert("NC_000023.11".to_string(), "chrX".to_string());
aliases
.refseq_to_ensembl
.insert("NC_000023.10".to_string(), "X".to_string());
aliases
.refseq_to_ensembl
.insert("NC_000023.11".to_string(), "X".to_string());
aliases.add_alias("chrY", GenomeBuild::GRCh37, "NC_000024.9");
aliases.add_alias("Y", GenomeBuild::GRCh37, "NC_000024.9");
aliases.add_alias("NC_000024.9", GenomeBuild::GRCh37, "NC_000024.9");
aliases.add_alias("chrY", GenomeBuild::GRCh38, "NC_000024.10");
aliases.add_alias("Y", GenomeBuild::GRCh38, "NC_000024.10");
aliases.add_alias("NC_000024.10", GenomeBuild::GRCh38, "NC_000024.10");
aliases
.refseq_to_ucsc
.insert("NC_000024.9".to_string(), "chrY".to_string());
aliases
.refseq_to_ucsc
.insert("NC_000024.10".to_string(), "chrY".to_string());
aliases
.refseq_to_ensembl
.insert("NC_000024.9".to_string(), "Y".to_string());
aliases
.refseq_to_ensembl
.insert("NC_000024.10".to_string(), "Y".to_string());
aliases.add_alias("chrM", GenomeBuild::GRCh37, "NC_012920.1");
aliases.add_alias("MT", GenomeBuild::GRCh37, "NC_012920.1");
aliases.add_alias("NC_012920.1", GenomeBuild::GRCh37, "NC_012920.1");
aliases.add_alias("chrM", GenomeBuild::GRCh38, "NC_012920.1");
aliases.add_alias("MT", GenomeBuild::GRCh38, "NC_012920.1");
aliases.add_alias("NC_012920.1", GenomeBuild::GRCh38, "NC_012920.1");
aliases
.refseq_to_ucsc
.insert("NC_012920.1".to_string(), "chrM".to_string());
aliases
.refseq_to_ensembl
.insert("NC_012920.1".to_string(), "MT".to_string());
aliases
}
pub fn from_assembly_reports(reports: &[(GenomeBuild, &AssemblyReport)]) -> Self {
let mut aliases = Self::new();
for (build, report) in reports {
for entry in report.assembled_molecules_with_refseq() {
let refseq = entry.refseq_accession.as_str();
let ensembl = entry.assigned_molecule.as_str();
let ucsc = entry.ucsc_name.as_str();
aliases.add_alias(refseq, *build, refseq);
if !ensembl.is_empty() && ensembl != "na" {
aliases.add_alias(ensembl, *build, refseq);
aliases
.refseq_to_ensembl
.insert(refseq.to_string(), ensembl.to_string());
}
if !ucsc.is_empty() && ucsc != "na" {
aliases.add_alias(ucsc, *build, refseq);
aliases
.refseq_to_ucsc
.insert(refseq.to_string(), ucsc.to_string());
}
}
}
aliases
}
fn grch37_version(chr: i32) -> i32 {
match chr {
1 => 10,
2 => 11,
3 => 11,
4 => 11,
5 => 9,
6 => 11,
7 => 13,
8 => 10,
9 => 11,
10 => 10,
11 => 9,
12 => 11,
13 => 10,
14 => 8,
15 => 9,
16 => 9,
17 => 10,
18 => 9,
19 => 9,
20 => 10,
21 => 8,
22 => 10,
_ => 1,
}
}
fn grch38_version(chr: i32) -> i32 {
match chr {
1 => 11,
2 => 12,
3 => 12,
4 => 12,
5 => 10,
6 => 12,
7 => 14,
8 => 11,
9 => 12,
10 => 11,
11 => 10,
12 => 12,
13 => 11,
14 => 9,
15 => 10,
16 => 10,
17 => 11,
18 => 10,
19 => 10,
20 => 11,
21 => 9,
22 => 11,
_ => 1,
}
}
pub fn is_empty(&self) -> bool {
self.to_refseq.is_empty()
}
pub fn add_alias(&mut self, name: &str, build: GenomeBuild, refseq: &str) {
self.to_refseq
.insert((name.to_string(), build), refseq.to_string());
}
pub fn resolve_to_refseq(&self, name: &str, build: GenomeBuild) -> Option<&str> {
self.to_refseq
.get(&(name.to_string(), build))
.map(|s| s.as_str())
}
pub fn refseq_to_ucsc(&self, refseq: &str) -> Option<&str> {
self.refseq_to_ucsc.get(refseq).map(|s| s.as_str())
}
pub fn refseq_to_ensembl(&self, refseq: &str) -> Option<&str> {
self.refseq_to_ensembl.get(refseq).map(|s| s.as_str())
}
pub fn are_equivalent(&self, name1: &str, name2: &str, build: GenomeBuild) -> bool {
let refseq1 = self.resolve_to_refseq(name1, build);
let refseq2 = self.resolve_to_refseq(name2, build);
match (refseq1, refseq2) {
(Some(r1), Some(r2)) => r1 == r2,
_ => name1 == name2,
}
}
pub fn normalize(&self, name: &str, build: GenomeBuild) -> String {
self.resolve_to_refseq(name, build)
.map(|s| s.to_string())
.unwrap_or_else(|| name.to_string())
}
}
pub(crate) fn default_human_aliases() -> &'static ContigAliases {
use std::sync::OnceLock;
static ALIASES: OnceLock<ContigAliases> = OnceLock::new();
ALIASES.get_or_init(ContigAliases::default_human)
}
pub fn infer_genome_build_from_accession(
accession: &crate::hgvs::variant::Accession,
) -> Option<&'static str> {
infer_genome_build_from_accession_with(default_human_aliases(), accession)
}
pub fn infer_genome_build_from_accession_with(
aliases: &ContigAliases,
accession: &crate::hgvs::variant::Accession,
) -> Option<&'static str> {
if let Some(asm) = accession.assembly.as_deref() {
match asm {
"GRCh37" => return Some("GRCh37"),
"GRCh38" => return Some("GRCh38"),
_ => {}
}
}
if &*accession.prefix != "NC" {
return None;
}
let acc_str = accession.full();
if aliases
.resolve_to_refseq(&acc_str, GenomeBuild::GRCh38)
.is_some()
{
return Some("GRCh38");
}
if aliases
.resolve_to_refseq(&acc_str, GenomeBuild::GRCh37)
.is_some()
{
return Some("GRCh37");
}
None
}
pub fn infer_genome_build_layered(
table: Option<&ContigAliases>,
accession: &crate::hgvs::variant::Accession,
) -> Option<&'static str> {
if let Some(table) = table {
if let Some(build) = infer_genome_build_from_accession_with(table, accession) {
return Some(build);
}
}
infer_genome_build_from_accession(accession)
}
pub fn normalize_assembly_name(name: &str) -> Option<&'static str> {
match name.trim().to_ascii_lowercase().as_str() {
"grch37" | "hg19" | "b37" | "37" => Some("GRCh37"),
"grch38" | "hg38" | "b38" | "38" => Some("GRCh38"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_assembly_name() {
for name in ["GRCh37", "grch37", "hg19", "HG19", "b37", "37", " grch37 "] {
assert_eq!(normalize_assembly_name(name), Some("GRCh37"), "{name:?}");
}
for name in ["GRCh38", "grch38", "hg38", "b38", "38"] {
assert_eq!(normalize_assembly_name(name), Some("GRCh38"), "{name:?}");
}
for name in ["", "hg20", "GRCh39", "chm13", "t2t"] {
assert_eq!(normalize_assembly_name(name), None, "{name:?}");
}
}
#[test]
fn test_default_human_autosomes() {
let aliases = ContigAliases::default_human();
assert_eq!(
aliases.resolve_to_refseq("chr1", GenomeBuild::GRCh37),
Some("NC_000001.10")
);
assert_eq!(
aliases.resolve_to_refseq("1", GenomeBuild::GRCh37),
Some("NC_000001.10")
);
assert_eq!(
aliases.resolve_to_refseq("chr1", GenomeBuild::GRCh38),
Some("NC_000001.11")
);
assert_eq!(
aliases.resolve_to_refseq("1", GenomeBuild::GRCh38),
Some("NC_000001.11")
);
}
#[test]
fn test_sex_chromosomes() {
let aliases = ContigAliases::default_human();
assert_eq!(
aliases.resolve_to_refseq("chrX", GenomeBuild::GRCh37),
Some("NC_000023.10")
);
assert_eq!(
aliases.resolve_to_refseq("X", GenomeBuild::GRCh38),
Some("NC_000023.11")
);
assert_eq!(
aliases.resolve_to_refseq("chrY", GenomeBuild::GRCh37),
Some("NC_000024.9")
);
}
#[test]
fn test_mitochondrial() {
let aliases = ContigAliases::default_human();
assert_eq!(
aliases.resolve_to_refseq("chrM", GenomeBuild::GRCh37),
Some("NC_012920.1")
);
assert_eq!(
aliases.resolve_to_refseq("MT", GenomeBuild::GRCh38),
Some("NC_012920.1")
);
}
#[test]
fn test_reverse_lookup() {
let aliases = ContigAliases::default_human();
assert_eq!(aliases.refseq_to_ucsc("NC_000001.10"), Some("chr1"));
assert_eq!(aliases.refseq_to_ensembl("NC_000001.10"), Some("1"));
}
#[test]
fn test_equivalence() {
let aliases = ContigAliases::default_human();
assert!(aliases.are_equivalent("chr1", "1", GenomeBuild::GRCh37));
assert!(aliases.are_equivalent("chr1", "NC_000001.10", GenomeBuild::GRCh37));
assert!(!aliases.are_equivalent("chr1", "chr2", GenomeBuild::GRCh37));
}
#[test]
fn test_normalize() {
let aliases = ContigAliases::default_human();
assert_eq!(
aliases.normalize("chr1", GenomeBuild::GRCh37),
"NC_000001.10"
);
assert_eq!(aliases.normalize("unknown", GenomeBuild::GRCh37), "unknown");
}
use crate::hgvs::variant::Accession;
use crate::liftover::assembly_report::parse_assembly_report;
fn synthetic_report(name: &str, rows: &[(&str, &str, &str)]) -> String {
let mut s = format!("# Assembly name: {name}\n");
for (ucsc, ensembl, refseq) in rows {
s.push_str(&format!(
"{ensembl}\tassembled-molecule\t{ensembl}\tChromosome\tCM000000.1\t=\t{refseq}\tPrimary Assembly\t1000\t{ucsc}\n"
));
}
s
}
#[test]
fn from_assembly_reports_builds_resolvable_aliases() {
let report = parse_assembly_report(&synthetic_report(
"GRCh38.p14",
&[("chr1", "1", "NC_000001.11"), ("chrX", "X", "NC_000023.11")],
));
let aliases = ContigAliases::from_assembly_reports(&[(GenomeBuild::GRCh38, &report)]);
assert_eq!(
aliases.resolve_to_refseq("chr1", GenomeBuild::GRCh38),
Some("NC_000001.11")
);
assert_eq!(
aliases.resolve_to_refseq("1", GenomeBuild::GRCh38),
Some("NC_000001.11")
);
assert_eq!(
aliases.resolve_to_refseq("NC_000023.11", GenomeBuild::GRCh38),
Some("NC_000023.11")
);
assert_eq!(aliases.refseq_to_ucsc("NC_000001.11"), Some("chr1"));
assert_eq!(aliases.refseq_to_ensembl("NC_000023.11"), Some("X"));
}
#[test]
fn infer_with_classifies_from_merged_reports() {
let g38 = parse_assembly_report(&synthetic_report(
"GRCh38.p14",
&[("chr1", "1", "NC_000001.11")],
));
let g37 = parse_assembly_report(&synthetic_report(
"GRCh37.p13",
&[("chr1", "1", "NC_000001.10")],
));
let aliases = ContigAliases::from_assembly_reports(&[
(GenomeBuild::GRCh38, &g38),
(GenomeBuild::GRCh37, &g37),
]);
assert_eq!(
infer_genome_build_from_accession_with(
&aliases,
&Accession::new("NC", "000001", Some(11))
),
Some("GRCh38")
);
assert_eq!(
infer_genome_build_from_accession_with(
&aliases,
&Accession::new("NC", "000001", Some(10))
),
Some("GRCh37")
);
}
#[test]
fn infer_with_hardens_beyond_the_hardcoded_table() {
let unknown_to_default = Accession::new("NC", "000001", Some(12));
assert_eq!(
infer_genome_build_from_accession(&unknown_to_default),
None,
"the bundled version table must not know NC_000001.12"
);
let report = parse_assembly_report(&synthetic_report(
"GRCh38.p15",
&[("chr1", "1", "NC_000001.12")],
));
let aliases = ContigAliases::from_assembly_reports(&[(GenomeBuild::GRCh38, &report)]);
assert_eq!(
infer_genome_build_from_accession_with(&aliases, &unknown_to_default),
Some("GRCh38"),
"report-derived aliases must classify accessions absent from the bundled table"
);
}
#[test]
fn infer_default_path_is_unchanged() {
assert_eq!(
infer_genome_build_from_accession(&Accession::new("NC", "000017", Some(11))),
Some("GRCh38")
);
assert_eq!(
infer_genome_build_from_accession(&Accession::new("NC", "000017", Some(10))),
Some("GRCh37")
);
assert_eq!(
infer_genome_build_from_accession(&Accession::new("NC", "012920", Some(1))),
Some("GRCh38")
);
assert_eq!(
infer_genome_build_from_accession(&Accession::new("NG", "012337", Some(1))),
None
);
}
const GRCH38_FUTURE_SAMPLE: &str = "\
# Assembly name: GRCh38.future
# Sequence-Name\tSequence-Role\tAssigned-Molecule\tAssigned-Molecule-Location/Type\tGenBank-Accn\tRelationship\tRefSeq-Accn\tAssembly-Unit\tSequence-Length\tUCSC-style-name
17\tassembled-molecule\t17\tChromosome\tCM000679.9\t=\tNC_000017.99\tPrimary Assembly\t83257441\tchr17
";
#[test]
fn layered_classifies_report_only_version() {
use crate::hgvs::variant::Accession;
let report = parse_assembly_report(GRCH38_FUTURE_SAMPLE);
let table = ContigAliases::from_assembly_reports(&[(GenomeBuild::GRCh38, &report)]);
let future = Accession::new("NC", "000017", Some(99));
assert_eq!(infer_genome_build_from_accession(&future), None);
assert_eq!(
infer_genome_build_layered(Some(&table), &future),
Some("GRCh38")
);
}
#[test]
fn layered_falls_back_to_hardcoded_when_absent_from_report() {
use crate::hgvs::variant::Accession;
let report = parse_assembly_report(GRCH38_FUTURE_SAMPLE);
let table = ContigAliases::from_assembly_reports(&[(GenomeBuild::GRCh38, &report)]);
let grch37_chr1 = Accession::new("NC", "000001", Some(10));
assert_eq!(
infer_genome_build_layered(Some(&table), &grch37_chr1),
Some("GRCh37")
);
}
#[test]
fn layered_with_no_table_matches_hardcoded() {
use crate::hgvs::variant::Accession;
let grch38_chr17 = Accession::new("NC", "000017", Some(11));
assert_eq!(
infer_genome_build_layered(None, &grch38_chr17),
infer_genome_build_from_accession(&grch38_chr17)
);
}
}