use crate::abstain::AbstainReason;
use crate::hyp::Hyp;
use crate::operator::{Evidence, Operator};
use crate::outcome::Outcome;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OperatorMetadata {
pub name: String,
pub version: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SetOutcome {
pub result: Hyp,
pub abstentions: Vec<(String, AbstainReason)>,
}
pub struct OperatorSet {
operators_and_metadata: Vec<(Box<dyn Operator>, OperatorMetadata)>,
}
impl OperatorSet {
pub fn new() -> Self {
OperatorSet {
operators_and_metadata: Vec::new(),
}
}
pub fn register(mut self, op: Box<dyn Operator>, meta: OperatorMetadata) -> Self {
if self
.operators_and_metadata
.iter()
.any(|(_, m)| m.name == meta.name)
{
panic!(
"OperatorSet::register: operator '{}' already registered",
meta.name
);
}
self.operators_and_metadata.push((op, meta));
self
}
pub fn len(&self) -> usize {
self.operators_and_metadata.len()
}
pub fn is_empty(&self) -> bool {
self.operators_and_metadata.is_empty()
}
pub fn operator_names(&self) -> Vec<&str> {
self.operators_and_metadata
.iter()
.map(|(_, m)| m.name.as_str())
.collect()
}
pub fn apply_set(&self, h: &Hyp, e: &Evidence) -> SetOutcome {
let mut current_h = h.clone();
let mut abstentions = Vec::new();
for (op, meta) in self.operators_and_metadata.iter() {
match op.apply(¤t_h, e) {
Outcome::Refined(h_prime) => {
debug_assert!(
h_prime <= current_h,
"INV-PS-03 violated by {}: refinement not strictly refined",
meta.name
);
current_h = h_prime;
}
Outcome::Abstain(reason) => {
abstentions.push((meta.name.clone(), reason));
}
}
}
SetOutcome {
result: current_h,
abstentions,
}
}
}
impl Default for OperatorSet {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provenance::{Provenance, ProvenanceOrigin};
use crate::version::Ver;
use crate::{Atom, OntologySystem};
use chrono::Utc;
use std::collections::BTreeMap;
struct NoopOperatorFixture;
impl Operator for NoopOperatorFixture {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(_h.clone())
}
}
struct ConstRefineOperatorFixture {
atom: Atom,
}
impl Operator for ConstRefineOperatorFixture {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
let mut atoms = _h.atoms().to_vec();
atoms.push(self.atom.clone());
Outcome::Refined(Hyp::new(atoms))
}
}
struct AlwaysAbstainOperatorFixture;
impl Operator for AlwaysAbstainOperatorFixture {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Abstain(AbstainReason::InsufficientEvidence("fixture abstain"))
}
}
fn test_evidence() -> Evidence {
Evidence::new(
vec![],
Provenance::new(
ProvenanceOrigin::new("test", "test", "test"),
Utc::now(),
Ver::new("test", "test", "0.1.0"),
BTreeMap::new(),
),
)
}
#[test]
fn test_empty_set_construction() {
let set = OperatorSet::new();
assert!(set.is_empty());
assert_eq!(set.len(), 0);
assert_eq!(set.operator_names(), Vec::<&str>::new());
}
#[test]
fn test_register_single_operator() {
struct DummyOp;
impl Operator for DummyOp {
fn apply(&self, h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(h.clone())
}
}
let set = OperatorSet::new().register(
Box::new(DummyOp),
OperatorMetadata {
name: "TestOp".to_string(),
version: "v1.0.0".to_string(),
},
);
assert_eq!(set.len(), 1);
assert!(!set.is_empty());
assert_eq!(set.operator_names(), vec!["TestOp"]);
}
#[test]
fn test_register_multiple_operators() {
struct DummyOp1;
impl Operator for DummyOp1 {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(_h.clone())
}
}
struct DummyOp2;
impl Operator for DummyOp2 {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(_h.clone())
}
}
let set = OperatorSet::new()
.register(
Box::new(DummyOp1),
OperatorMetadata {
name: "Op1".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(DummyOp2),
OperatorMetadata {
name: "Op2".to_string(),
version: "v1.0.0".to_string(),
},
);
assert_eq!(set.len(), 2);
assert_eq!(set.operator_names(), vec!["Op1", "Op2"]);
}
#[test]
fn test_apply_set_empty_returns_input() {
let set = OperatorSet::new();
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.result, h);
assert!(outcome.abstentions.is_empty());
}
#[test]
fn test_apply_set_single_noop() {
struct NoopOp;
impl Operator for NoopOp {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(_h.clone())
}
}
let set = OperatorSet::new().register(
Box::new(NoopOp),
OperatorMetadata {
name: "Noop".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.result, h);
assert!(outcome.abstentions.is_empty());
}
#[test]
fn test_apply_set_single_abstain() {
struct AbstainOp;
impl Operator for AbstainOp {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Abstain(AbstainReason::InsufficientEvidence("test"))
}
}
let set = OperatorSet::new().register(
Box::new(AbstainOp),
OperatorMetadata {
name: "Abstain".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.result, h);
assert_eq!(outcome.abstentions.len(), 1);
assert_eq!(outcome.abstentions[0].0, "Abstain");
}
#[test]
fn test_apply_set_preserves_input_on_all_abstain() {
struct AbstainOp1;
impl Operator for AbstainOp1 {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Abstain(AbstainReason::InsufficientEvidence("test1"))
}
}
struct AbstainOp2;
impl Operator for AbstainOp2 {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Abstain(AbstainReason::OutOfDistribution("test2"))
}
}
let set = OperatorSet::new()
.register(
Box::new(AbstainOp1),
OperatorMetadata {
name: "Abstain1".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(AbstainOp2),
OperatorMetadata {
name: "Abstain2".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.result, h);
assert_eq!(outcome.abstentions.len(), 2);
}
#[test]
fn test_default_creates_empty_set() {
let set = OperatorSet::default();
assert_eq!(set.len(), 0);
assert!(set.is_empty());
}
#[test]
fn test_apply_set_abstain_then_refine() {
struct AbstainOp;
impl Operator for AbstainOp {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Abstain(AbstainReason::InsufficientEvidence("test"))
}
}
struct NoopOp;
impl Operator for NoopOp {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(_h.clone())
}
}
let set = OperatorSet::new()
.register(
Box::new(AbstainOp),
OperatorMetadata {
name: "Abstain".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(NoopOp),
OperatorMetadata {
name: "Noop".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.result, h);
assert_eq!(outcome.abstentions.len(), 1);
assert_eq!(outcome.abstentions[0].0, "Abstain");
}
#[test]
fn test_apply_set_both_noop() {
struct NoopOp1;
impl Operator for NoopOp1 {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(_h.clone())
}
}
struct NoopOp2;
impl Operator for NoopOp2 {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(_h.clone())
}
}
let set = OperatorSet::new()
.register(
Box::new(NoopOp1),
OperatorMetadata {
name: "Noop1".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(NoopOp2),
OperatorMetadata {
name: "Noop2".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.result, h);
assert!(outcome.abstentions.is_empty());
}
#[test]
fn test_apply_set_records_abstention_names_in_order() {
struct AbstainOp;
impl Operator for AbstainOp {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Abstain(AbstainReason::InsufficientEvidence("test"))
}
}
let set = OperatorSet::new()
.register(
Box::new(AbstainOp),
OperatorMetadata {
name: "First".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(AbstainOp),
OperatorMetadata {
name: "Second".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(AbstainOp),
OperatorMetadata {
name: "Third".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.abstentions.len(), 3);
assert_eq!(outcome.abstentions[0].0, "First");
assert_eq!(outcome.abstentions[1].0, "Second");
assert_eq!(outcome.abstentions[2].0, "Third");
}
#[test]
fn test_apply_set_monotonicity_const_refine() {
let atom = Atom {
system: OntologySystem::SNOMED,
code: "CONST-9999".to_string(),
preferred_term: "Test".to_string(),
version: "0.1.0".to_string(),
};
let set = OperatorSet::new().register(
Box::new(ConstRefineOperatorFixture { atom: atom.clone() }),
OperatorMetadata {
name: "ConstRefine".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert!(outcome.result <= h, "result should refine input");
}
#[test]
fn test_apply_set_abstention_propagates_forward() {
let atom = Atom {
system: OntologySystem::SNOMED,
code: "CONST-8888".to_string(),
preferred_term: "Test2".to_string(),
version: "0.1.0".to_string(),
};
let set = OperatorSet::new()
.register(
Box::new(AlwaysAbstainOperatorFixture),
OperatorMetadata {
name: "Abstain".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(ConstRefineOperatorFixture { atom }),
OperatorMetadata {
name: "ConstRefine".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert!(outcome.result <= h);
assert_eq!(outcome.abstentions.len(), 1);
assert_eq!(outcome.abstentions[0].0, "Abstain");
}
#[test]
fn test_apply_set_empty_set_identity() {
let set = OperatorSet::new();
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.result, h);
assert!(outcome.abstentions.is_empty());
}
#[test]
fn test_apply_set_all_abstain_preserves_input() {
let set = OperatorSet::new()
.register(
Box::new(AlwaysAbstainOperatorFixture),
OperatorMetadata {
name: "A1".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(AlwaysAbstainOperatorFixture),
OperatorMetadata {
name: "A2".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(AlwaysAbstainOperatorFixture),
OperatorMetadata {
name: "A3".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.result, h);
assert_eq!(outcome.abstentions.len(), 3);
}
#[test]
fn test_apply_set_noop_chain_identity() {
let set = OperatorSet::new()
.register(
Box::new(NoopOperatorFixture),
OperatorMetadata {
name: "N1".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(NoopOperatorFixture),
OperatorMetadata {
name: "N2".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.result, h);
assert!(outcome.abstentions.is_empty());
}
#[test]
fn test_apply_set_multiple_const_refine() {
let atom1 = Atom {
system: OntologySystem::SNOMED,
code: "CONST-7777".to_string(),
preferred_term: "T1".to_string(),
version: "0.1.0".to_string(),
};
let atom2 = Atom {
system: OntologySystem::SNOMED,
code: "CONST-6666".to_string(),
preferred_term: "T2".to_string(),
version: "0.1.0".to_string(),
};
let set = OperatorSet::new()
.register(
Box::new(ConstRefineOperatorFixture {
atom: atom1.clone(),
}),
OperatorMetadata {
name: "R1".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(ConstRefineOperatorFixture {
atom: atom2.clone(),
}),
OperatorMetadata {
name: "R2".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert!(outcome.result <= h);
let result_atoms = outcome.result.atoms();
assert!(result_atoms.contains(&atom1));
assert!(result_atoms.contains(&atom2));
}
#[test]
fn test_operator_names_preserves_registration_order() {
struct DummyOp;
impl Operator for DummyOp {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(_h.clone())
}
}
let set = OperatorSet::new()
.register(
Box::new(DummyOp),
OperatorMetadata {
name: "Zebra".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(DummyOp),
OperatorMetadata {
name: "Alpha".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(DummyOp),
OperatorMetadata {
name: "Mike".to_string(),
version: "v1.0.0".to_string(),
},
);
let names = set.operator_names();
assert_eq!(names, vec!["Zebra", "Alpha", "Mike"]);
}
#[test]
fn test_abstention_names_match_registration_order() {
struct AbstainOp;
impl Operator for AbstainOp {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Abstain(AbstainReason::InsufficientEvidence("test"))
}
}
let set = OperatorSet::new()
.register(
Box::new(AbstainOp),
OperatorMetadata {
name: "Zebra".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(AbstainOp),
OperatorMetadata {
name: "Alpha".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(AbstainOp),
OperatorMetadata {
name: "Mike".to_string(),
version: "v1.0.0".to_string(),
},
);
let h = Hyp::unknown();
let e = test_evidence();
let outcome = set.apply_set(&h, &e);
assert_eq!(outcome.abstentions.len(), 3);
assert_eq!(outcome.abstentions[0].0, "Zebra");
assert_eq!(outcome.abstentions[1].0, "Alpha");
assert_eq!(outcome.abstentions[2].0, "Mike");
}
#[test]
#[should_panic(expected = "already registered")]
fn test_register_duplicate_name_panics() {
struct DummyOp;
impl Operator for DummyOp {
fn apply(&self, _h: &Hyp, _e: &Evidence) -> Outcome<Hyp, AbstainReason> {
Outcome::Refined(_h.clone())
}
}
let _set = OperatorSet::new()
.register(
Box::new(DummyOp),
OperatorMetadata {
name: "Duplicate".to_string(),
version: "v1.0.0".to_string(),
},
)
.register(
Box::new(DummyOp),
OperatorMetadata {
name: "Duplicate".to_string(),
version: "v2.0.0".to_string(),
},
);
}
}