1use std::collections::HashMap;
48
49use crate::ConceptPool;
50use crate::ir::{ClassId, ConceptExpr, ConceptId, IndividualId, Role};
51use crate::normalize::to_nnf;
52use crate::ontology::Axiom;
53
54#[derive(Debug, Default, Clone, Eq, PartialEq)]
68pub struct AbsorbedTBox {
69 pub concept_rules: Vec<ConceptRule>,
72 pub nominal_rules: Vec<NominalRule>,
75 pub role_rules: Vec<RoleRule>,
79 pub residual_gcis: Vec<ConceptId>,
82 pub deferred_or_residuals: Vec<ConceptId>,
90 pub concept_rules_by_trigger: HashMap<ClassId, Vec<ConceptId>>,
95 pub nominal_rules_by_individual: HashMap<IndividualId, Vec<ConceptId>>,
97 pub unguarded_role_rules: Vec<RoleRule>,
101 pub guarded_role_rules_by_guard: HashMap<ClassId, Vec<RoleRule>>,
104}
105
106impl AbsorbedTBox {
107 pub fn finalize(&mut self) {
111 self.concept_rules_by_trigger.clear();
112 self.concept_rules_by_trigger
113 .reserve(self.concept_rules.len());
114 for rule in &self.concept_rules {
115 self.concept_rules_by_trigger
116 .entry(rule.trigger)
117 .or_default()
118 .push(rule.conclusion);
119 }
120 self.nominal_rules_by_individual.clear();
121 self.nominal_rules_by_individual
122 .reserve(self.nominal_rules.len());
123 for rule in &self.nominal_rules {
124 self.nominal_rules_by_individual
125 .entry(rule.individual)
126 .or_default()
127 .push(rule.conclusion);
128 }
129 self.unguarded_role_rules.clear();
130 self.guarded_role_rules_by_guard.clear();
131 for rule in &self.role_rules {
132 match rule.guard {
133 None => self.unguarded_role_rules.push(*rule),
134 Some(g) => self
135 .guarded_role_rules_by_guard
136 .entry(g)
137 .or_default()
138 .push(*rule),
139 }
140 }
141 }
142}
143
144#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
145pub struct ConceptRule {
146 pub trigger: ClassId,
147 pub conclusion: ConceptId,
148}
149
150#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
151pub struct NominalRule {
152 pub individual: IndividualId,
153 pub conclusion: ConceptId,
154}
155
156#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
157pub struct RoleRule {
158 pub role: Role,
163 pub guard: Option<ClassId>,
164 pub target_label: ConceptId,
165}
166
167#[must_use]
174pub fn absorb(axioms_nnf: &[Axiom], pool: &mut ConceptPool) -> AbsorbedTBox {
175 let mut tbox = AbsorbedTBox::default();
176 for ax in axioms_nnf {
177 absorb_one(ax, pool, &mut tbox);
178 }
179 absorb_roles(&mut tbox, pool);
180 tbox
181}
182
183pub fn absorb_roles(tbox: &mut AbsorbedTBox, pool: &ConceptPool) {
192 let mut kept = Vec::with_capacity(tbox.concept_rules.len());
194 for rule in std::mem::take(&mut tbox.concept_rules) {
195 if let ConceptExpr::All(role, target) = pool.get(rule.conclusion) {
196 tbox.role_rules.push(RoleRule {
197 role: *role,
198 guard: Some(rule.trigger),
199 target_label: *target,
200 });
201 } else {
202 kept.push(rule);
203 }
204 }
205 tbox.concept_rules = kept;
206
207 let mut kept = Vec::with_capacity(tbox.residual_gcis.len());
209 for gci in std::mem::take(&mut tbox.residual_gcis) {
210 if let ConceptExpr::All(role, target) = pool.get(gci) {
211 tbox.role_rules.push(RoleRule {
212 role: *role,
213 guard: None,
214 target_label: *target,
215 });
216 } else {
217 kept.push(gci);
218 }
219 }
220 tbox.residual_gcis = kept;
221
222 let mut kept = Vec::with_capacity(tbox.nominal_rules.len());
226 for rule in std::mem::take(&mut tbox.nominal_rules) {
227 if let ConceptExpr::All(role, target) = pool.get(rule.conclusion) {
228 let _ = (role, target);
234 kept.push(rule);
235 } else {
236 kept.push(rule);
237 }
238 }
239 tbox.nominal_rules = kept;
240
241 tbox.deferred_or_residuals = tbox
248 .residual_gcis
249 .iter()
250 .copied()
251 .filter(|&g| matches!(pool.get(g), ConceptExpr::Or(_)))
252 .collect();
253 tbox.deferred_or_residuals
260 .sort_unstable_by_key(|c| c.index());
261
262 tbox.finalize();
264}
265
266fn absorb_one(ax: &Axiom, pool: &mut ConceptPool, tbox: &mut AbsorbedTBox) {
267 match ax {
268 Axiom::SubClassOf { sub, sup } => absorb_sub_sup(*sub, *sup, pool, tbox),
269 Axiom::EquivalentClasses(ids) => {
270 for i in 0..ids.len() {
271 for j in 0..ids.len() {
272 if i != j {
273 absorb_sub_sup(ids[i], ids[j], pool, tbox);
274 }
275 }
276 }
277 }
278 Axiom::DisjointClasses(ids) => {
279 emit_pairwise_disjoint(ids, pool, tbox);
280 }
281 Axiom::DisjointUnion { class, members } => {
282 let class_concept = pool.atomic(*class);
283 let union_concept = pool.or(members.iter().copied());
284 absorb_sub_sup(class_concept, union_concept, pool, tbox);
286 absorb_sub_sup(union_concept, class_concept, pool, tbox);
287 emit_pairwise_disjoint(members, pool, tbox);
289 }
290 Axiom::ObjectPropertyDomain { role, domain } => {
291 let top = pool.top();
293 let some_r_top = pool.some(*role, top);
294 absorb_sub_sup(some_r_top, *domain, pool, tbox);
295 }
296 Axiom::ObjectPropertyRange { role, range } => {
297 let all_r = pool.all(*role, *range);
299 tbox.residual_gcis.push(all_r);
300 }
301 _ => {
302 }
305 }
306}
307
308fn emit_pairwise_disjoint(ids: &[ConceptId], pool: &mut ConceptPool, tbox: &mut AbsorbedTBox) {
309 for i in 0..ids.len() {
310 for j in (i + 1)..ids.len() {
311 let not_cj = pool.not(ids[j]);
312 let not_cj_nnf = to_nnf(not_cj, pool);
313 absorb_sub_sup(ids[i], not_cj_nnf, pool, tbox);
314 }
315 }
316}
317
318fn absorb_sub_sup(sub: ConceptId, sup: ConceptId, pool: &mut ConceptPool, tbox: &mut AbsorbedTBox) {
320 let neg_sub = pool.not(sub);
321 let neg_sub_nnf = to_nnf(neg_sub, pool);
322 let disjunction = pool.or([neg_sub_nnf, sup]);
323 absorb_gci(disjunction, pool, tbox);
324}
325
326fn absorb_gci(phi: ConceptId, pool: &mut ConceptPool, tbox: &mut AbsorbedTBox) {
329 let disjuncts: Vec<ConceptId> = match pool.get(phi) {
330 ConceptExpr::Or(args) => args.to_vec(),
331 _ => vec![phi],
332 };
333
334 let mut chosen: Option<(usize, Trigger)> = None;
336 for (i, &d) in disjuncts.iter().enumerate() {
337 if let Some(t) = as_trigger(d, pool) {
338 chosen = Some((i, t));
339 break;
340 }
341 }
342
343 if let Some((pos, trigger)) = chosen {
344 let rest: Vec<ConceptId> = disjuncts
345 .iter()
346 .enumerate()
347 .filter_map(|(i, &c)| (i != pos).then_some(c))
348 .collect();
349 let conclusion = pool.or(rest);
352 match trigger {
353 Trigger::Class(trigger) => tbox.concept_rules.push(ConceptRule {
354 trigger,
355 conclusion,
356 }),
357 Trigger::Individual(individual) => tbox.nominal_rules.push(NominalRule {
358 individual,
359 conclusion,
360 }),
361 }
362 } else {
363 tbox.residual_gcis.push(phi);
364 }
365}
366
367enum Trigger {
369 Class(ClassId),
370 Individual(IndividualId),
371}
372
373fn as_trigger(cid: ConceptId, pool: &ConceptPool) -> Option<Trigger> {
375 if let ConceptExpr::Not(inner) = pool.get(cid) {
376 match pool.get(*inner) {
377 ConceptExpr::Atomic(c) => Some(Trigger::Class(*c)),
378 ConceptExpr::Nominal(i) => Some(Trigger::Individual(*i)),
379 _ => None,
380 }
381 } else {
382 None
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 #![allow(clippy::many_single_char_names)]
389
390 use super::*;
391 use crate::Vocabulary;
392 use crate::ir::{Role, RoleId};
393 use crate::nnf_axioms;
394 use crate::ontology::InternalOntology;
395
396 fn fresh(class_names: &[&str]) -> InternalOntology {
397 let mut o = InternalOntology::new();
398 for n in class_names {
399 o.vocabulary.intern_class(n);
400 }
401 o
402 }
403
404 fn cid(o: &InternalOntology, name: &str) -> ClassId {
405 o.vocabulary.class_id(name).expect("class missing")
406 }
407
408 fn atom(o: &mut InternalOntology, name: &str) -> ConceptId {
409 let c = cid(o, name);
410 o.concepts.atomic(c)
411 }
412
413 fn run(o: &mut InternalOntology) -> AbsorbedTBox {
416 let nnf = nnf_axioms(o);
417 absorb(&nnf, &mut o.concepts)
418 }
419
420 #[test]
421 fn atomic_sub_class_of_yields_one_rule() {
422 let mut o = fresh(&["A", "B"]);
424 let a = atom(&mut o, "A");
425 let b = atom(&mut o, "B");
426 o.axioms.push(Axiom::SubClassOf { sub: a, sup: b });
427 let t = run(&mut o);
428 assert_eq!(t.concept_rules.len(), 1);
429 assert!(t.residual_gcis.is_empty());
430 assert_eq!(t.concept_rules[0].trigger, cid(&o, "A"));
431 assert_eq!(t.concept_rules[0].conclusion, b);
432 }
433
434 #[test]
435 fn sub_class_of_with_conjunctive_conclusion() {
436 let mut o = fresh(&["A", "B", "C"]);
438 let a = atom(&mut o, "A");
439 let b = atom(&mut o, "B");
440 let cc = atom(&mut o, "C");
441 let and_bc = o.concepts.and([b, cc]);
442 o.axioms.push(Axiom::SubClassOf {
443 sub: a,
444 sup: and_bc,
445 });
446 let t = run(&mut o);
447 assert_eq!(t.concept_rules.len(), 1);
448 assert_eq!(t.concept_rules[0].trigger, cid(&o, "A"));
449 assert_eq!(t.concept_rules[0].conclusion, and_bc);
450 }
451
452 #[test]
453 fn complex_lhs_with_atomic_rhs_absorbs_via_double_negation() {
454 let mut o = fresh(&["A", "B", "C"]);
458 let a = atom(&mut o, "A");
459 let b = atom(&mut o, "B");
460 let cc = atom(&mut o, "C");
461 let and_bc = o.concepts.and([b, cc]);
462 o.axioms.push(Axiom::SubClassOf {
463 sub: and_bc,
464 sup: a,
465 });
466 let t = run(&mut o);
467 assert_eq!(t.concept_rules.len(), 1);
468 assert!(t.residual_gcis.is_empty());
469 let trigger = t.concept_rules[0].trigger;
471 assert!(trigger == cid(&o, "B") || trigger == cid(&o, "C"));
472 }
473
474 #[test]
475 fn pure_existential_gci_is_residual() {
476 let mut o = fresh(&["A"]);
478 let a = atom(&mut o, "A");
479 let r = Role::named(RoleId::new(0));
480 let some_a = o.concepts.some(r, a);
481 let top = o.concepts.top();
482 o.axioms.push(Axiom::SubClassOf {
483 sub: top,
484 sup: some_a,
485 });
486 let t = run(&mut o);
487 assert!(t.concept_rules.is_empty());
488 assert_eq!(t.residual_gcis.len(), 1);
489 assert_eq!(t.residual_gcis[0], some_a);
490 }
491
492 #[test]
493 fn equivalent_classes_creates_pairwise_rules() {
494 let mut o = fresh(&["A", "B"]);
496 let a = atom(&mut o, "A");
497 let b = atom(&mut o, "B");
498 o.axioms.push(Axiom::EquivalentClasses(vec![a, b]));
499 let t = run(&mut o);
500 assert_eq!(t.concept_rules.len(), 2);
501 let triggers: Vec<ClassId> = t.concept_rules.iter().map(|r| r.trigger).collect();
503 assert!(triggers.contains(&cid(&o, "A")));
504 assert!(triggers.contains(&cid(&o, "B")));
505 }
506
507 #[test]
508 fn disjoint_classes_yields_not_atom_conclusion() {
509 let mut o = fresh(&["A", "B"]);
511 let a = atom(&mut o, "A");
512 let b = atom(&mut o, "B");
513 o.axioms.push(Axiom::DisjointClasses(vec![a, b]));
514 let t = run(&mut o);
515 assert_eq!(t.concept_rules.len(), 1);
516 let rule = &t.concept_rules[0];
517 let trigger = rule.trigger;
520 assert!(trigger == cid(&o, "A") || trigger == cid(&o, "B"));
521 let other = if trigger == cid(&o, "A") {
523 cid(&o, "B")
524 } else {
525 cid(&o, "A")
526 };
527 let expected_other_atom = o.concepts.atomic(other);
528 let expected_conclusion = o.concepts.not(expected_other_atom);
529 assert_eq!(rule.conclusion, expected_conclusion);
530 }
531
532 #[test]
533 fn disjoint_classes_three_way_yields_three_pairwise_rules() {
534 let mut o = fresh(&["A", "B", "C"]);
536 let a = atom(&mut o, "A");
537 let b = atom(&mut o, "B");
538 let cc = atom(&mut o, "C");
539 o.axioms.push(Axiom::DisjointClasses(vec![a, b, cc]));
540 let t = run(&mut o);
541 assert_eq!(t.concept_rules.len(), 3);
542 }
543
544 #[test]
545 fn disjoint_union_emits_subsumption_and_pairwise_disjoint_rules() {
546 let mut o = fresh(&["P", "C1", "C2"]);
551 let c1 = atom(&mut o, "C1");
552 let c2 = atom(&mut o, "C2");
553 o.axioms.push(Axiom::DisjointUnion {
554 class: cid(&o, "P"),
555 members: vec![c1, c2],
556 });
557 let t = run(&mut o);
558 assert_eq!(t.concept_rules.len(), 2);
562 assert_eq!(t.residual_gcis.len(), 1);
563 }
564
565 #[test]
566 fn object_property_range_becomes_unguarded_role_rule() {
567 let mut o = fresh(&["A"]);
571 let a = atom(&mut o, "A");
572 let r = Role::named(RoleId::new(0));
573 o.axioms
574 .push(Axiom::ObjectPropertyRange { role: r, range: a });
575 let t = run(&mut o);
576 assert!(t.concept_rules.is_empty());
577 assert!(t.residual_gcis.is_empty());
578 assert_eq!(t.role_rules.len(), 1);
579 let rr = t.role_rules[0];
580 assert_eq!(rr.role, crate::Role::Named(r.role_id()));
581 assert_eq!(rr.guard, None);
582 assert_eq!(rr.target_label, a);
583 }
584
585 #[test]
586 fn sub_class_of_all_becomes_guarded_role_rule() {
587 let mut o = fresh(&["A", "B"]);
588 let a = atom(&mut o, "A");
589 let b = atom(&mut o, "B");
590 let r = Role::named(RoleId::new(0));
591 let all_r_b = o.concepts.all(r, b);
592 o.axioms.push(Axiom::SubClassOf {
593 sub: a,
594 sup: all_r_b,
595 });
596 let t = run(&mut o);
597 assert!(t.concept_rules.is_empty());
598 assert_eq!(t.role_rules.len(), 1);
599 let rr = t.role_rules[0];
600 assert_eq!(rr.role, crate::Role::Named(r.role_id()));
601 assert_eq!(rr.guard, Some(cid(&o, "A")));
602 assert_eq!(rr.target_label, b);
603 }
604
605 #[test]
606 fn nominal_sub_class_yields_nominal_rule() {
607 let mut o = fresh(&["B"]);
609 let b = atom(&mut o, "B");
610 let ind = o.vocabulary.intern_individual("a");
611 let nom = o.concepts.nominal(ind);
612 o.axioms.push(Axiom::SubClassOf { sub: nom, sup: b });
613 let t = run(&mut o);
614 assert!(t.concept_rules.is_empty());
615 assert_eq!(t.nominal_rules.len(), 1);
616 assert_eq!(t.nominal_rules[0].individual, ind);
617 assert_eq!(t.nominal_rules[0].conclusion, b);
618 }
619
620 #[test]
621 fn unrelated_axioms_pass_through_without_contribution() {
622 let mut o = fresh(&["A"]);
624 let _ = atom(&mut o, "A");
625 let r = Role::named(RoleId::new(0));
626 let _ = Vocabulary::new(); o.axioms.push(Axiom::TransitiveRole(r));
628 let i = o.vocabulary.intern_individual("a");
629 let a = o.concepts.atomic(cid(&o, "A"));
630 o.axioms.push(Axiom::ClassAssertion {
631 class: a,
632 individual: i,
633 });
634 let t = run(&mut o);
635 assert!(t.concept_rules.is_empty());
636 assert!(t.residual_gcis.is_empty());
637 }
638
639 #[test]
640 fn sub_class_of_top_to_atom_is_residual() {
641 let mut o = fresh(&["A"]);
643 let a = atom(&mut o, "A");
644 let top = o.concepts.top();
645 o.axioms.push(Axiom::SubClassOf { sub: top, sup: a });
646 let t = run(&mut o);
647 assert!(t.concept_rules.is_empty());
648 assert_eq!(t.residual_gcis.len(), 1);
649 assert_eq!(t.residual_gcis[0], a);
650 }
651
652 #[test]
653 fn finalize_indexes_concept_rules_by_trigger() {
654 let mut o = fresh(&["A", "B", "C", "D", "E"]);
656 let a = atom(&mut o, "A");
657 let b = atom(&mut o, "B");
658 let cc = atom(&mut o, "C");
659 let d = atom(&mut o, "D");
660 let e = atom(&mut o, "E");
661 o.axioms.push(Axiom::SubClassOf { sub: a, sup: b });
662 o.axioms.push(Axiom::SubClassOf { sub: a, sup: cc });
663 o.axioms.push(Axiom::SubClassOf { sub: d, sup: e });
664 let t = run(&mut o);
665 assert_eq!(t.concept_rules.len(), 3);
666 let a_id = cid(&o, "A");
668 let d_id = cid(&o, "D");
669 let from_a = t.concept_rules_by_trigger.get(&a_id).expect("A indexed");
670 assert_eq!(from_a.len(), 2);
671 assert!(from_a.contains(&b));
672 assert!(from_a.contains(&cc));
673 let from_d = t.concept_rules_by_trigger.get(&d_id).expect("D indexed");
674 assert_eq!(from_d, &vec![e]);
675 assert!(!t.concept_rules_by_trigger.contains_key(&cid(&o, "B")));
677 }
678
679 #[test]
680 fn finalize_indexes_nominal_rules_by_individual() {
681 let mut o = fresh(&["B", "C", "D"]);
683 let b = atom(&mut o, "B");
684 let cc = atom(&mut o, "C");
685 let d = atom(&mut o, "D");
686 let ind_a = o.vocabulary.intern_individual("a");
687 let ind_b = o.vocabulary.intern_individual("b");
688 let nom_a = o.concepts.nominal(ind_a);
689 let nom_b = o.concepts.nominal(ind_b);
690 o.axioms.push(Axiom::SubClassOf { sub: nom_a, sup: b });
691 o.axioms.push(Axiom::SubClassOf {
692 sub: nom_a,
693 sup: cc,
694 });
695 o.axioms.push(Axiom::SubClassOf { sub: nom_b, sup: d });
696 let t = run(&mut o);
697 assert_eq!(t.nominal_rules.len(), 3);
698 let from_a = t
699 .nominal_rules_by_individual
700 .get(&ind_a)
701 .expect("a indexed");
702 assert_eq!(from_a.len(), 2);
703 assert!(from_a.contains(&b));
704 assert!(from_a.contains(&cc));
705 assert_eq!(t.nominal_rules_by_individual.get(&ind_b), Some(&vec![d]));
706 }
707
708 #[test]
709 fn finalize_partitions_role_rules_by_guard() {
710 let mut o = fresh(&["A", "B", "C"]);
713 let a = atom(&mut o, "A");
714 let b = atom(&mut o, "B");
715 let cc = atom(&mut o, "C");
716 let r = Role::named(RoleId::new(0));
717 let all_r_b = o.concepts.all(r, b);
718 o.axioms.push(Axiom::SubClassOf {
719 sub: a,
720 sup: all_r_b,
721 });
722 o.axioms
723 .push(Axiom::ObjectPropertyRange { role: r, range: cc });
724 let t = run(&mut o);
725 assert_eq!(t.role_rules.len(), 2);
726 assert_eq!(t.unguarded_role_rules.len(), 1);
727 assert_eq!(t.unguarded_role_rules[0].target_label, cc);
728 let a_id = cid(&o, "A");
729 let guarded = t
730 .guarded_role_rules_by_guard
731 .get(&a_id)
732 .expect("guarded on A");
733 assert_eq!(guarded.len(), 1);
734 assert_eq!(guarded[0].target_label, b);
735 }
736
737 #[test]
738 fn finalize_is_idempotent() {
739 let mut o = fresh(&["A", "B"]);
740 let a = atom(&mut o, "A");
741 let b = atom(&mut o, "B");
742 o.axioms.push(Axiom::SubClassOf { sub: a, sup: b });
743 let mut t = run(&mut o);
744 let before = t.clone();
745 t.finalize();
746 t.finalize();
747 assert_eq!(t, before);
748 }
749}