use crate::ontology::Atom;
use std::cmp::Ordering;
use std::collections::HashSet;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Hyp(Vec<Atom>);
impl Hyp {
pub fn new(atoms: Vec<Atom>) -> Self {
let mut deduplicated: Vec<Atom> = atoms;
deduplicated.sort_by(|a, b| {
match a.system.cmp(&b.system) {
Ordering::Equal => match a.code.cmp(&b.code) {
Ordering::Equal => a.version.cmp(&b.version),
other => other,
},
other => other,
}
});
deduplicated.dedup();
Hyp(deduplicated)
}
pub fn unknown() -> Self {
Hyp(vec![])
}
pub fn compat(&self, other: &Hyp) -> bool {
if self.is_unknown() || other.is_unknown() {
return true;
}
let self_atoms: std::collections::HashSet<_> = self.atoms().iter().collect();
let other_atoms: std::collections::HashSet<_> = other.atoms().iter().collect();
!self_atoms.is_disjoint(&other_atoms)
}
pub fn meet(&self, other: &Hyp) -> Option<Hyp> {
if self == other {
return Some(self.clone());
}
if self.is_unknown() {
return Some(other.clone());
}
if other.is_unknown() {
return Some(self.clone());
}
None
}
fn is_unknown(&self) -> bool {
self.0.is_empty()
}
pub fn atoms(&self) -> &[Atom] {
&self.0
}
}
impl PartialOrd for Hyp {
fn partial_cmp(&self, other: &Hyp) -> Option<Ordering> {
let self_atoms: HashSet<&Atom> = self.atoms().iter().collect();
let other_atoms: HashSet<&Atom> = other.atoms().iter().collect();
let self_subset_of_other = self_atoms.is_subset(&other_atoms);
let other_subset_of_self = other_atoms.is_subset(&self_atoms);
match (self_subset_of_other, other_subset_of_self) {
(true, true) => Some(Ordering::Equal),
(false, true) => Some(Ordering::Less), (true, false) => Some(Ordering::Greater), (false, false) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ontology::OntologySystem;
fn atom_diagnosis_a() -> Atom {
Atom {
system: OntologySystem::SNOMED,
code: "67822003".to_string(),
preferred_term: "Hypoxemia".to_string(),
version: "2026-01-31".to_string(),
}
}
fn atom_diagnosis_b() -> Atom {
Atom {
system: OntologySystem::SNOMED,
code: "3723001".to_string(),
preferred_term: "Acute respiratory distress syndrome".to_string(),
version: "2026-01-31".to_string(),
}
}
fn atom_severity_high() -> Atom {
Atom {
system: OntologySystem::SNOMED,
code: "24484000".to_string(),
preferred_term: "Severe".to_string(),
version: "2026-01-31".to_string(),
}
}
#[test]
fn test_unknown_top_element() {
let unknown = Hyp::unknown();
let hyp = Hyp::new(vec![atom_diagnosis_a()]);
assert_eq!(hyp.partial_cmp(&unknown), Some(Ordering::Less));
assert_eq!(unknown.partial_cmp(&hyp), Some(Ordering::Greater));
}
#[test]
fn test_equal_hypotheses() {
let atom_a = atom_diagnosis_a();
let atom_s = atom_severity_high();
let h1 = Hyp::new(vec![atom_a.clone(), atom_s.clone()]);
let h2 = Hyp::new(vec![atom_a, atom_s]);
assert_eq!(h1.partial_cmp(&h2), Some(Ordering::Equal));
assert!(h1.compat(&h2));
}
#[test]
fn test_incomparable_hypotheses() {
let h1 = Hyp::new(vec![atom_diagnosis_a()]);
let h2 = Hyp::new(vec![atom_diagnosis_b()]);
assert_eq!(h1.partial_cmp(&h2), None);
assert!(!h1.compat(&h2));
}
#[test]
fn test_subset_refinement() {
let h_general = Hyp::new(vec![atom_diagnosis_a()]);
let h_specific = Hyp::new(vec![atom_diagnosis_a(), atom_severity_high()]);
assert_eq!(h_specific.partial_cmp(&h_general), Some(Ordering::Less));
assert_eq!(h_general.partial_cmp(&h_specific), Some(Ordering::Greater));
}
#[test]
fn test_compat_with_unknown() {
let unknown = Hyp::unknown();
let hyp = Hyp::new(vec![atom_diagnosis_a()]);
assert!(hyp.compat(&unknown));
assert!(unknown.compat(&hyp));
}
#[test]
fn test_meet_equal_hypotheses() {
let atom = atom_diagnosis_a();
let h1 = Hyp::new(vec![atom.clone()]);
let h2 = Hyp::new(vec![atom]);
assert_eq!(h1.meet(&h2), Some(h1.clone()));
}
#[test]
fn test_meet_with_unknown() {
let unknown = Hyp::unknown();
let hyp = Hyp::new(vec![atom_diagnosis_a()]);
assert_eq!(hyp.meet(&unknown), Some(hyp.clone()));
assert_eq!(unknown.meet(&hyp), Some(hyp.clone()));
}
#[test]
fn test_meet_incomparable_none() {
let h1 = Hyp::new(vec![atom_diagnosis_a()]);
let h2 = Hyp::new(vec![atom_diagnosis_b()]);
assert_eq!(h1.meet(&h2), None);
}
#[test]
fn test_hyp_duplicate_atoms_deduplicated() {
let atom = atom_diagnosis_a();
let h_with_dup = Hyp::new(vec![atom.clone(), atom.clone()]);
let h_deduped = Hyp::new(vec![atom]);
assert_eq!(h_with_dup, h_deduped);
assert_eq!(h_with_dup.partial_cmp(&h_deduped), Some(Ordering::Equal));
assert_eq!(h_with_dup.meet(&h_deduped), Some(h_deduped));
}
#[test]
fn test_hyp_atoms_sorted_deterministically() {
let atom_a = atom_diagnosis_a();
let atom_b = atom_diagnosis_b();
let atom_s = atom_severity_high();
let h1 = Hyp::new(vec![atom_s.clone(), atom_a.clone(), atom_b.clone()]);
let h2 = Hyp::new(vec![atom_b.clone(), atom_s.clone(), atom_a.clone()]);
assert_eq!(h1, h2);
let atoms = h1.atoms();
assert_eq!(atoms.len(), 3);
for i in 1..atoms.len() {
assert!(
(atoms[i - 1].system, &atoms[i - 1].code) <= (atoms[i].system, &atoms[i].code),
"atoms not sorted: {:?} > {:?}",
atoms[i - 1],
atoms[i]
);
}
}
#[test]
fn test_compat_closed_under_refinement_inv_ps_01() {
let atom_a = atom_diagnosis_a();
let atom_b = atom_diagnosis_b();
let atom_s = atom_severity_high();
let h1 = Hyp::new(vec![atom_a.clone(), atom_b.clone()]);
let h2_unknown = Hyp::unknown();
let h3 = Hyp::new(vec![atom_a.clone(), atom_s.clone()]);
assert!(h1 <= h2_unknown, "h₁ should refine h₂");
assert!(h2_unknown.compat(&h3), "h₂ should be compat with h₃");
assert!(
h1.compat(&h3),
"h₁ should be compat with h₃ (closure via shared atom_a)"
);
let h_disjoint1 = Hyp::new(vec![atom_a.clone()]);
let h_disjoint2 = Hyp::new(vec![atom_b.clone()]);
assert!(
!h_disjoint1.compat(&h_disjoint2),
"disjoint atom sets should not be compatible"
);
let h2_concrete = Hyp::new(vec![atom_a.clone()]);
let h3_extended = Hyp::new(vec![atom_a.clone(), atom_s.clone()]);
let h1_refined = Hyp::new(vec![atom_a.clone(), atom_b.clone()]);
assert!(h1_refined <= h2_concrete, "h₁ should refine h₂");
assert!(
h2_concrete.compat(&h3_extended),
"h₂ should be compat with h₃"
);
assert!(
h1_refined.compat(&h3_extended),
"h₁ should be compat with h₃"
);
}
}