use std::collections::HashSet;
use crate::core::contig::Contig;
use crate::core::header::QueryHeader;
use crate::core::reference::KnownReference;
use crate::core::types::Confidence;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContigMatchType {
Exact,
NameLengthNoMd5,
Md5Conflict,
Unmatched,
}
#[inline]
fn count_to_f64(count: usize) -> f64 {
#[allow(clippy::cast_precision_loss)]
{
count as f64
}
}
#[derive(Debug, Clone)]
pub struct MatchScore {
pub composite: f64,
pub confidence: Confidence,
pub exact_matches: usize,
pub name_length_matches: usize,
pub md5_conflicts: usize,
pub unmatched: usize,
pub match_quality: f64,
pub coverage_score: f64,
pub order_score: f64,
pub order_preserved: bool,
pub md5_jaccard: f64,
pub name_length_jaccard: f64,
pub md5_query_coverage: f64,
pub name_length_query_coverage: f64,
}
impl MatchScore {
#[must_use]
pub fn calculate(query: &QueryHeader, reference: &KnownReference) -> Self {
let mut exact_matches = 0usize;
let mut name_length_matches = 0usize; let mut md5_conflicts = 0usize;
let mut unmatched = 0usize;
for contig in &query.contigs {
match classify_contig_match(contig, reference) {
ContigMatchType::Exact => exact_matches += 1,
ContigMatchType::NameLengthNoMd5 => name_length_matches += 1,
ContigMatchType::Md5Conflict => md5_conflicts += 1,
ContigMatchType::Unmatched => unmatched += 1,
}
}
let total_query = query.contigs.len();
let weighted_matches = count_to_f64(exact_matches)
+ count_to_f64(name_length_matches)
+ (count_to_f64(md5_conflicts) * 0.1);
let match_quality = if total_query > 0 {
weighted_matches / count_to_f64(total_query)
} else {
0.0
};
let good_matches = exact_matches + name_length_matches;
let coverage_score = if reference.contigs.is_empty() {
0.0
} else {
(count_to_f64(good_matches) / count_to_f64(reference.contigs.len())).min(1.0)
};
let (order_preserved, order_score) = analyze_order(query, reference);
let composite = ((0.70 * match_quality) + (0.20 * coverage_score) + (0.10 * order_score))
.clamp(0.0, 1.0);
let confidence = Confidence::from_score(composite);
let md5_jaccard = jaccard_similarity(&query.md5_set, &reference.md5_set);
let (name_length_jaccard, name_length_query_coverage) =
calculate_name_length_similarity_with_aliases(query, reference);
let md5_query_coverage = if query.md5_set.is_empty() {
0.0
} else {
count_to_f64(query.md5_set.intersection(&reference.md5_set).count())
/ count_to_f64(query.md5_set.len())
};
Self {
composite,
confidence,
exact_matches,
name_length_matches,
md5_conflicts,
unmatched,
match_quality,
coverage_score,
order_score,
order_preserved,
md5_jaccard,
name_length_jaccard,
md5_query_coverage,
name_length_query_coverage,
}
}
#[must_use]
pub fn calculate_with_weights(
query: &QueryHeader,
reference: &KnownReference,
weights: &crate::matching::engine::ScoringWeights,
) -> Self {
let mut exact_matches = 0usize;
let mut name_length_matches = 0usize;
let mut md5_conflicts = 0usize;
let mut unmatched = 0usize;
for contig in &query.contigs {
match classify_contig_match(contig, reference) {
ContigMatchType::Exact => exact_matches += 1,
ContigMatchType::NameLengthNoMd5 => name_length_matches += 1,
ContigMatchType::Md5Conflict => md5_conflicts += 1,
ContigMatchType::Unmatched => unmatched += 1,
}
}
let total_query = query.contigs.len();
let normalized_weights = weights.normalized();
let weighted_matches = count_to_f64(exact_matches)
+ count_to_f64(name_length_matches)
+ (count_to_f64(md5_conflicts) * normalized_weights.conflict_penalty);
let match_quality = if total_query > 0 {
weighted_matches / count_to_f64(total_query)
} else {
0.0
};
let good_matches = exact_matches + name_length_matches;
let coverage_score = if reference.contigs.is_empty() {
0.0
} else {
(count_to_f64(good_matches) / count_to_f64(reference.contigs.len())).min(1.0)
};
let (order_preserved, order_score) = analyze_order(query, reference);
let composite = ((normalized_weights.contig_match * match_quality)
+ (normalized_weights.coverage * coverage_score)
+ (normalized_weights.order * order_score))
.clamp(0.0, 1.0);
let confidence = Confidence::from_score(composite);
let md5_jaccard = jaccard_similarity(&query.md5_set, &reference.md5_set);
let (name_length_jaccard, name_length_query_coverage) =
calculate_name_length_similarity_with_aliases(query, reference);
let md5_query_coverage = if query.md5_set.is_empty() {
0.0
} else {
count_to_f64(query.md5_set.intersection(&reference.md5_set).count())
/ count_to_f64(query.md5_set.len())
};
Self {
composite,
confidence,
exact_matches,
name_length_matches,
md5_conflicts,
unmatched,
match_quality,
coverage_score,
order_score,
order_preserved,
md5_jaccard,
name_length_jaccard,
md5_query_coverage,
name_length_query_coverage,
}
}
}
fn jaccard_similarity<T: Eq + std::hash::Hash>(a: &HashSet<T>, b: &HashSet<T>) -> f64 {
let intersection = a.intersection(b).count();
let union = a.union(b).count();
if union == 0 {
0.0
} else {
count_to_f64(intersection) / count_to_f64(union)
}
}
fn calculate_name_length_similarity_with_aliases(
query: &QueryHeader,
reference: &KnownReference,
) -> (f64, f64) {
let mut matched_query_contigs = 0usize;
let mut matched_ref_keys: HashSet<(String, u64)> = HashSet::new();
for contig in &query.contigs {
let name_key = (contig.name.clone(), contig.length);
if reference.name_length_set.contains(&name_key) {
matched_query_contigs += 1;
matched_ref_keys.insert(name_key);
continue;
}
for alias in &contig.aliases {
let alias_key = (alias.clone(), contig.length);
if reference.name_length_set.contains(&alias_key) {
matched_query_contigs += 1;
matched_ref_keys.insert(alias_key);
break;
}
}
}
let query_count = query.contigs.len();
let ref_count = reference.contigs.len();
let union_size = query_count + ref_count - matched_ref_keys.len();
let jaccard = if union_size == 0 {
0.0
} else {
count_to_f64(matched_ref_keys.len()) / count_to_f64(union_size)
};
let coverage = if query_count == 0 {
0.0
} else {
count_to_f64(matched_query_contigs) / count_to_f64(query_count)
};
(jaccard, coverage)
}
fn analyze_order(query: &QueryHeader, reference: &KnownReference) -> (bool, f64) {
use std::collections::HashMap;
let mut ref_positions: HashMap<(&str, u64), usize> = HashMap::new();
for (i, contig) in reference.contigs.iter().enumerate() {
ref_positions.insert((contig.name.as_str(), contig.length), i);
for alias in &contig.aliases {
ref_positions.insert((alias.as_str(), contig.length), i);
}
}
let mut positions: Vec<usize> = Vec::new();
for contig in &query.contigs {
let key = (contig.name.as_str(), contig.length);
if let Some(&pos) = ref_positions.get(&key) {
positions.push(pos);
continue;
}
for alias in &contig.aliases {
let alias_key = (alias.as_str(), contig.length);
if let Some(&pos) = ref_positions.get(&alias_key) {
positions.push(pos);
break;
}
}
}
if positions.len() < 2 {
return (true, 0.0);
}
let is_sorted = positions.windows(2).all(|w| w[0] < w[1]);
let lis_len = longest_increasing_subsequence(&positions);
let order_score = count_to_f64(lis_len) / count_to_f64(positions.len());
(is_sorted, order_score)
}
fn longest_increasing_subsequence(arr: &[usize]) -> usize {
if arr.is_empty() {
return 0;
}
let mut dp = vec![1usize; arr.len()];
for i in 1..arr.len() {
for j in 0..i {
if arr[j] < arr[i] {
dp[i] = dp[i].max(dp[j] + 1);
}
}
}
dp.into_iter().max().expect("dp is non-empty")
}
fn classify_contig_match(query_contig: &Contig, reference: &KnownReference) -> ContigMatchType {
let matched_ref_contig = find_matching_reference_contig(query_contig, reference);
match matched_ref_contig {
None => ContigMatchType::Unmatched,
Some(ref_contig) => {
match (&query_contig.md5, &ref_contig.md5) {
(Some(q_md5), Some(r_md5)) => {
if q_md5.eq_ignore_ascii_case(r_md5) {
ContigMatchType::Exact
} else {
ContigMatchType::Md5Conflict
}
}
_ => ContigMatchType::NameLengthNoMd5,
}
}
}
}
fn find_matching_reference_contig<'a>(
query_contig: &Contig,
reference: &'a KnownReference,
) -> Option<&'a Contig> {
let query_key = (query_contig.name.clone(), query_contig.length);
if reference.name_length_set.contains(&query_key) {
for ref_contig in &reference.contigs {
if ref_contig.name == query_contig.name && ref_contig.length == query_contig.length {
return Some(ref_contig);
}
for alias in &ref_contig.aliases {
if alias == &query_contig.name && ref_contig.length == query_contig.length {
return Some(ref_contig);
}
}
}
}
for query_alias in &query_contig.aliases {
let alias_key = (query_alias.clone(), query_contig.length);
if reference.name_length_set.contains(&alias_key) {
for ref_contig in &reference.contigs {
if ref_contig.name == *query_alias && ref_contig.length == query_contig.length {
return Some(ref_contig);
}
for ref_alias in &ref_contig.aliases {
if ref_alias == query_alias && ref_contig.length == query_contig.length {
return Some(ref_contig);
}
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jaccard_similarity() {
let a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [2, 3, 4].into_iter().collect();
let similarity = jaccard_similarity(&a, &b);
assert!((similarity - 0.5).abs() < 0.001);
let empty1: HashSet<i32> = HashSet::new();
let empty2: HashSet<i32> = HashSet::new();
assert!((jaccard_similarity(&empty1, &empty2) - 0.0).abs() < 0.001);
assert!((jaccard_similarity(&a, &empty1) - 0.0).abs() < 0.001);
let c: HashSet<i32> = [1, 2, 3].into_iter().collect();
assert!((jaccard_similarity(&a, &c) - 1.0).abs() < 0.001);
}
#[test]
fn test_longest_increasing_subsequence() {
assert_eq!(longest_increasing_subsequence(&[1, 2, 3, 4, 5]), 5);
assert_eq!(longest_increasing_subsequence(&[5, 4, 3, 2, 1]), 1);
assert_eq!(longest_increasing_subsequence(&[1, 3, 2, 4, 5]), 4);
assert_eq!(longest_increasing_subsequence(&[]), 0);
}
#[test]
fn test_composite_score_never_exceeds_one() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![
Contig::new("chr1", 1000),
Contig::new("chr2", 2000),
Contig::new("chr3", 3000),
];
let reference = KnownReference::new(
"test_ref",
"Test Reference",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query_no_md5 = QueryHeader::new(vec![
Contig::new("chr1", 1000),
Contig::new("chr2", 2000),
Contig::new("chr3", 3000),
]);
let score_no_md5 = MatchScore::calculate(&query_no_md5, &reference);
assert!(
score_no_md5.composite <= 1.0,
"Composite score without MD5 should not exceed 1.0, got {}",
score_no_md5.composite
);
assert!(
score_no_md5.composite >= 0.0,
"Composite score should not be negative, got {}",
score_no_md5.composite
);
let query_with_md5 = QueryHeader::new(vec![
Contig::new("chr1", 1000).with_md5("abc123"),
Contig::new("chr2", 2000).with_md5("def456"),
Contig::new("chr3", 3000).with_md5("ghi789"),
]);
let score_with_md5 = MatchScore::calculate(&query_with_md5, &reference);
assert!(
score_with_md5.composite <= 1.0,
"Composite score with MD5 should not exceed 1.0, got {}",
score_with_md5.composite
);
assert!(
score_with_md5.composite >= 0.0,
"Composite score should not be negative, got {}",
score_with_md5.composite
);
}
#[test]
fn test_composite_score_with_custom_weights_never_exceeds_one() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
use crate::matching::engine::ScoringWeights;
let ref_contigs = vec![Contig::new("chr1", 1000), Contig::new("chr2", 2000)];
let reference = KnownReference::new(
"test_ref",
"Test Reference",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query = QueryHeader::new(vec![Contig::new("chr1", 1000), Contig::new("chr2", 2000)]);
let weight_configs = vec![
ScoringWeights {
contig_match: 0.7,
coverage: 0.2,
order: 0.1,
conflict_penalty: 0.1,
},
ScoringWeights {
contig_match: 0.5,
coverage: 0.3,
order: 0.2,
conflict_penalty: 0.0,
},
ScoringWeights {
contig_match: 1.0,
coverage: 1.0,
order: 1.0,
conflict_penalty: 0.5,
},
];
for weights in weight_configs {
let score = MatchScore::calculate_with_weights(&query, &reference, &weights);
assert!(
score.composite <= 1.0,
"Composite score should not exceed 1.0 with weights {:?}, got {}",
weights,
score.composite
);
assert!(
score.composite >= 0.0,
"Composite score should not be negative with weights {:?}, got {}",
weights,
score.composite
);
}
}
#[test]
fn test_alias_match_query_name_to_ref_name() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![
Contig::new("chr1", 248_956_422),
Contig::new("chr2", 242_193_529),
];
let reference = KnownReference::new(
"test_ref",
"Test Reference",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query = QueryHeader::new(vec![
Contig::new("chr1", 248_956_422),
Contig::new("chr2", 242_193_529),
]);
let score = MatchScore::calculate(&query, &reference);
assert!(
(score.name_length_jaccard - 1.0).abs() < 0.001,
"Direct name match should have jaccard = 1.0, got {}",
score.name_length_jaccard
);
}
#[test]
fn test_alias_match_query_name_to_ref_alias() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![
Contig::new("NC_000001.11", 248_956_422)
.with_aliases(vec!["chr1".to_string(), "1".to_string()]),
Contig::new("NC_000002.12", 242_193_529)
.with_aliases(vec!["chr2".to_string(), "2".to_string()]),
];
let reference = KnownReference::new(
"test_ncbi_ref",
"Test NCBI Reference",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query = QueryHeader::new(vec![
Contig::new("chr1", 248_956_422),
Contig::new("chr2", 242_193_529),
]);
let score = MatchScore::calculate(&query, &reference);
assert!(
(score.name_length_jaccard - 1.0).abs() < 0.001,
"Query name matching ref alias should have jaccard = 1.0, got {}",
score.name_length_jaccard
);
}
#[test]
fn test_alias_match_query_alias_to_ref_name() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![
Contig::new("chr1", 248_956_422),
Contig::new("chr2", 242_193_529),
];
let reference = KnownReference::new(
"test_ucsc_ref",
"Test UCSC Reference",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query = QueryHeader::new(vec![
Contig::new("NC_000001.11", 248_956_422)
.with_aliases(vec!["chr1".to_string(), "1".to_string()]),
Contig::new("NC_000002.12", 242_193_529)
.with_aliases(vec!["chr2".to_string(), "2".to_string()]),
]);
let score = MatchScore::calculate(&query, &reference);
assert!(
(score.name_length_jaccard - 1.0).abs() < 0.001,
"Query alias matching ref name should have jaccard = 1.0, got {}",
score.name_length_jaccard
);
}
#[test]
fn test_alias_match_query_alias_to_ref_alias() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![
Contig::new("NC_000001.11", 248_956_422)
.with_aliases(vec!["chr1".to_string(), "CM000663.2".to_string()]),
Contig::new("NC_000002.12", 242_193_529)
.with_aliases(vec!["chr2".to_string(), "CM000664.2".to_string()]),
];
let reference = KnownReference::new(
"test_ncbi_ref_1",
"Test NCBI Reference 1",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query = QueryHeader::new(vec![
Contig::new("CM000663.2", 248_956_422)
.with_aliases(vec!["chr1".to_string(), "NC_000001.11".to_string()]),
Contig::new("CM000664.2", 242_193_529)
.with_aliases(vec!["chr2".to_string(), "NC_000002.12".to_string()]),
]);
let score = MatchScore::calculate(&query, &reference);
assert!(
(score.name_length_jaccard - 1.0).abs() < 0.001,
"Query alias matching ref alias should have jaccard = 1.0, got {}",
score.name_length_jaccard
);
}
#[test]
fn test_perfect_name_length_match_no_md5_gets_high_score() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![
Contig::new("chr1", 248_956_422),
Contig::new("chr2", 242_193_529),
Contig::new("chr3", 198_295_559),
];
let reference = KnownReference::new(
"test_ref",
"Test Reference",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query = QueryHeader::new(vec![
Contig::new("chr1", 248_956_422),
Contig::new("chr2", 242_193_529),
Contig::new("chr3", 198_295_559),
]);
let score = MatchScore::calculate(&query, &reference);
assert_eq!(
score.exact_matches, 0,
"No exact matches (no MD5 available)"
);
assert_eq!(
score.name_length_matches, 3,
"All 3 contigs should be name+length matches"
);
assert_eq!(score.md5_conflicts, 0, "No conflicts");
assert_eq!(score.unmatched, 0, "No unmatched");
assert!(
(score.match_quality - 1.0).abs() < 0.001,
"Contig match score should be 1.0, got {}",
score.match_quality
);
assert!(
(score.coverage_score - 1.0).abs() < 0.001,
"Coverage score should be 1.0, got {}",
score.coverage_score
);
assert!(
score.composite > 0.9,
"100% name+length match with no MD5 should score > 90%, got {:.1}%",
score.composite * 100.0
);
}
#[test]
fn test_md5_conflict_gets_heavy_penalty() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![
Contig::new("chr1", 248_956_422).with_md5("abc123"),
Contig::new("chr2", 242_193_529).with_md5("def456"),
];
let reference = KnownReference::new(
"test_ref",
"Test Reference",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query = QueryHeader::new(vec![
Contig::new("chr1", 248_956_422).with_md5("DIFFERENT1"),
Contig::new("chr2", 242_193_529).with_md5("DIFFERENT2"),
]);
let score = MatchScore::calculate(&query, &reference);
assert_eq!(score.exact_matches, 0, "No exact matches");
assert_eq!(score.name_length_matches, 0, "No name+length-only matches");
assert_eq!(score.md5_conflicts, 2, "Both contigs have MD5 conflicts");
assert_eq!(score.unmatched, 0, "No unmatched");
assert!(
(score.match_quality - 0.1).abs() < 0.001,
"Contig match score for conflicts should be 0.1, got {}",
score.match_quality
);
assert!(
score.composite < 0.2,
"MD5 conflicts should result in low score, got {:.1}%",
score.composite * 100.0
);
}
#[test]
fn test_mixed_match_types_weighted_correctly() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![
Contig::new("chr1", 248_956_422).with_md5("correct_md5_1"),
Contig::new("chr2", 242_193_529).with_md5("correct_md5_2"),
Contig::new("chr3", 198_295_559), ];
let reference = KnownReference::new(
"test_ref",
"Test Reference",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query = QueryHeader::new(vec![
Contig::new("chr1", 248_956_422).with_md5("correct_md5_1"),
Contig::new("chr2", 242_193_529).with_md5("WRONG_MD5"),
Contig::new("chr3", 198_295_559),
Contig::new("chr4", 100_000), ]);
let score = MatchScore::calculate(&query, &reference);
assert_eq!(score.exact_matches, 1, "1 exact match (chr1)");
assert_eq!(score.name_length_matches, 1, "1 name+length match (chr3)");
assert_eq!(score.md5_conflicts, 1, "1 conflict (chr2)");
assert_eq!(score.unmatched, 1, "1 unmatched (chr4)");
let expected_match_score = (1.0 + 1.0 + 0.1) / 4.0;
assert!(
(score.match_quality - expected_match_score).abs() < 0.001,
"Expected match_quality = {}, got {}",
expected_match_score,
score.match_quality
);
}
#[test]
fn test_classify_contig_match_exact() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![Contig::new("chr1", 1000).with_md5("abc123")];
let reference = KnownReference::new(
"test",
"Test",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query_contig = Contig::new("chr1", 1000).with_md5("abc123");
assert_eq!(
classify_contig_match(&query_contig, &reference),
ContigMatchType::Exact
);
}
#[test]
fn test_classify_contig_match_no_md5() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![Contig::new("chr1", 1000)]; let reference = KnownReference::new(
"test",
"Test",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query_contig = Contig::new("chr1", 1000); assert_eq!(
classify_contig_match(&query_contig, &reference),
ContigMatchType::NameLengthNoMd5
);
}
#[test]
fn test_classify_contig_match_md5_conflict() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![Contig::new("chr1", 1000).with_md5("abc123")];
let reference = KnownReference::new(
"test",
"Test",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query_contig = Contig::new("chr1", 1000).with_md5("DIFFERENT");
assert_eq!(
classify_contig_match(&query_contig, &reference),
ContigMatchType::Md5Conflict
);
}
#[test]
fn test_classify_contig_match_unmatched() {
use crate::core::contig::Contig;
use crate::core::reference::KnownReference;
use crate::core::types::{Assembly, ReferenceSource};
let ref_contigs = vec![Contig::new("chr1", 1000)];
let reference = KnownReference::new(
"test",
"Test",
Assembly::Grch38,
ReferenceSource::Custom("test".to_string()),
)
.with_contigs(ref_contigs);
let query_contig = Contig::new("chr2", 1000);
assert_eq!(
classify_contig_match(&query_contig, &reference),
ContigMatchType::Unmatched
);
let query_contig2 = Contig::new("chr1", 2000);
assert_eq!(
classify_contig_match(&query_contig2, &reference),
ContigMatchType::Unmatched
);
}
}