Skip to main content

ontologos_rl/
report.rs

1use std::collections::{BTreeMap, HashSet};
2
3use ontologos_core::{AxiomId, EntityId, InferenceTrace};
4use serde::Serialize;
5
6/// OWL RL rule that produced an inference.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
8#[serde(rename_all = "snake_case")]
9pub enum RlRule {
10    /// Equivalent classes imply mutual `SubClassOf`.
11    EqClassSub,
12    /// Equivalent properties imply mutual `SubObjectPropertyOf`.
13    EqPropSub,
14    /// Property characteristics propagate along `subPropertyOf`.
15    CharPropagate,
16    /// Existential restriction propagates along `subPropertyOf`.
17    ExistentialSubProp,
18    /// Class with `∃P.D` subsumed by class with `∃Q.D` when `P ⊑ Q`.
19    ExistentialSubsumption,
20    /// Domain typing from property assertions.
21    TypeDomain,
22    /// Range typing from property assertions.
23    TypeRange,
24    /// Class assertion propagates along `subClassOf`.
25    TypeSubclass,
26    /// Class assertion propagates along class equivalence.
27    TypeEquivalent,
28    /// Property assertion propagates along `subPropertyOf`.
29    PropSub,
30    /// Property assertion via inverse.
31    PropInverse,
32    /// Property assertion via symmetry.
33    PropSymmetric,
34    /// Transitive closure on property assertions.
35    PropTransitive,
36    /// `sameAs` propagation on class assertions.
37    SameAsClass,
38    /// `sameAs` propagation on property assertions.
39    SameAsProp,
40    /// Reflexive property self-assertion.
41    PropReflexive,
42}
43
44impl RlRule {
45    #[must_use]
46    pub fn as_str(self) -> &'static str {
47        match self {
48            Self::EqClassSub => "eq_class_sub",
49            Self::EqPropSub => "eq_prop_sub",
50            Self::CharPropagate => "char_propagate",
51            Self::ExistentialSubProp => "existential_sub_prop",
52            Self::ExistentialSubsumption => "existential_subsumption",
53            Self::TypeDomain => "type_domain",
54            Self::TypeRange => "type_range",
55            Self::TypeSubclass => "type_subclass",
56            Self::TypeEquivalent => "type_equivalent",
57            Self::PropSub => "prop_sub",
58            Self::PropInverse => "prop_inverse",
59            Self::PropSymmetric => "prop_symmetric",
60            Self::PropTransitive => "prop_transitive",
61            Self::SameAsClass => "same_as_class",
62            Self::SameAsProp => "same_as_prop",
63            Self::PropReflexive => "prop_reflexive",
64        }
65    }
66}
67
68/// A single recorded inference (legacy alias; prefer [`TraceStep`]).
69#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
70pub struct InferenceRecord {
71    pub rule: RlRule,
72    pub premises: Vec<AxiomId>,
73    pub conclusion: AxiomId,
74}
75
76/// Summary of RL saturation over an ontology (includes prior RDFS pass).
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78pub struct MaterializationReport {
79    pub initial_axiom_count: usize,
80    pub final_axiom_count: usize,
81    pub rdfs_inferred: usize,
82    pub inferred_by_rule: BTreeMap<RlRule, usize>,
83    #[serde(skip_serializing_if = "trace_is_empty")]
84    pub trace: InferenceTrace,
85    #[serde(skip_serializing_if = "Vec::is_empty")]
86    pub clashes: Vec<String>,
87    /// Canonical disjoint-clash keys already reported (not serialized).
88    #[serde(skip)]
89    pub disjoint_clash_keys: HashSet<(EntityId, EntityId, EntityId)>,
90    /// Canonical sameAs/differentFrom clash pairs already reported (not serialized).
91    #[serde(skip)]
92    pub same_as_clash_keys: HashSet<(EntityId, EntityId)>,
93}
94
95fn trace_is_empty(trace: &InferenceTrace) -> bool {
96    trace.steps.is_empty()
97}
98
99impl MaterializationReport {
100    #[must_use]
101    pub fn inferred_total(&self) -> usize {
102        self.final_axiom_count
103            .saturating_sub(self.initial_axiom_count)
104    }
105}