Skip to main content

strixonomy_reasoner/
swrl_run.rs

1//! Optional SWRL-aware classification: materialize DLSafe rules then classify.
2
3use crate::error::{ReasonerError, Result};
4use crate::input::ReasonerInput;
5use crate::result::{
6    build_inferred_hierarchy, new_inferences, taxonomy_equivalence_clusters, taxonomy_to_iri_edges,
7    unsatisfiable_iris, ClassificationResult, ReasonerWarning,
8};
9use ontologos_core::{EntityKind, Ontology, SwrlAtom, SwrlDArg, SwrlIArg, SwrlRule};
10use std::time::Instant;
11
12const SWRL_RULE_PRED: &str = "http://ontocode.dev/ns#swrlRule";
13
14/// Classify with SWRL materialization when the ontology contains SWRL rules.
15pub fn classify_with_swrl(input: &ReasonerInput) -> Result<ClassificationResult> {
16    let started = Instant::now();
17    let (ontology, mut warnings) = prepare_swrl_ontology(input)?;
18    let (taxonomy, _) = ontologos_swrl::classify_with_swrl(&ontology)
19        .map_err(|e| ReasonerError::Classify(format!("SWRL classify: {e}")))?;
20
21    let iri_edges = taxonomy_to_iri_edges(&ontology, &taxonomy).map_err(ReasonerError::Classify)?;
22    let equivalences =
23        taxonomy_equivalence_clusters(&ontology, &taxonomy).map_err(ReasonerError::Classify)?;
24    let reported = unsatisfiable_iris(&ontology, &taxonomy).map_err(ReasonerError::Classify)?;
25    let inferred = build_inferred_hierarchy(&iri_edges, &reported, &input.asserted_hierarchy);
26    let unsatisfiable = inferred.unsatisfiable.clone();
27    let new_inferences = new_inferences(&input.asserted_hierarchy, &inferred.edges);
28
29    // Consistency must run on the *materialized* ontology (#358), not the pre-SWRL input.
30    let mut consistent = unsatisfiable.is_empty();
31    if let Ok(detail) = crate::abox::check_full_consistency(
32        &ontology,
33        crate::adapter::ReasonerId::Dl,
34        &unsatisfiable,
35    ) {
36        consistent = detail.consistent;
37        for clash in &detail.abox_clashes {
38            warnings.push(ReasonerWarning {
39                code: "abox_clash".into(),
40                message: clash.clone(),
41                suggested_profile: None,
42            });
43        }
44        if !detail.complete {
45            warnings.push(ReasonerWarning {
46                code: "consistency_incomplete".into(),
47                message: "consistency check did not complete (budget or cancel)".into(),
48                suggested_profile: None,
49            });
50        }
51    }
52
53    Ok(ClassificationResult {
54        profile_used: "swrl".to_string(),
55        consistent,
56        unsatisfiable,
57        inferred,
58        new_inferences,
59        warnings,
60        duration_ms: started.elapsed().as_millis() as u64,
61        subsumption_count: taxonomy.subsumption_count(),
62        inferred_axiom_count: taxonomy.subsumption_count(),
63        equivalences,
64    })
65}
66
67/// Clone input ontology, inject authored rules, and materialize SWRL inferences.
68///
69/// Returns the post-materialization ontology plus inject/materialize warnings.
70pub fn prepare_swrl_ontology(input: &ReasonerInput) -> Result<(Ontology, Vec<ReasonerWarning>)> {
71    let mut ontology = input.ontology.clone();
72    let mut warnings = Vec::new();
73    // Prefer rules already injected at WorkspaceInputLoader time; re-inject from live
74    // overrides only when the store is still empty (e.g. load path missed Turtle).
75    if ontology.swrl_rules().is_empty() {
76        let (authored, inject_warnings) = inject_authored_swrl_from_input(&mut ontology, input);
77        warnings.extend(inject_warnings);
78        if authored > 0 {
79            warnings.push(ReasonerWarning {
80                code: "swrl_authored_injected".into(),
81                message: format!(
82                    "injected {authored} Strixonomy-authored SWRL rule(s) from strixonomy:swrlRule annotations"
83                ),
84                suggested_profile: None,
85            });
86        }
87    }
88    let report = ontologos_swrl::materialize_swrl_rules(&mut ontology)
89        .map_err(|e| ReasonerError::Classify(format!("SWRL materialization: {e}")))?;
90    if report.inferences_added > 0 || report.rules_found > 0 {
91        warnings.push(ReasonerWarning {
92            code: "swrl_materialized".into(),
93            message: format!(
94                "SWRL: {} rule(s), {} inferred axiom(s) materialized",
95                report.rules_found, report.inferences_added
96            ),
97            suggested_profile: None,
98        });
99    }
100    Ok((ontology, warnings))
101}
102
103/// True when Ontologos already has SWRL rules on the ontology.
104pub fn ontology_has_swrl_rules(ontology: &Ontology) -> bool {
105    !ontology.swrl_rules().is_empty()
106}
107
108/// True when classify should take the SWRL path.
109pub fn input_has_swrl_rules(input: &ReasonerInput) -> bool {
110    ontology_has_swrl_rules(&input.ontology) || input_mentions_authored_swrl(input)
111}
112
113fn input_mentions_authored_swrl(input: &ReasonerInput) -> bool {
114    input.document_overrides.values().any(|t| t.contains(SWRL_RULE_PRED))
115}
116
117/// Inject authored SWRL from live document overrides into the Ontologos store.
118pub fn inject_authored_swrl_from_input(
119    ontology: &mut Ontology,
120    input: &ReasonerInput,
121) -> (usize, Vec<ReasonerWarning>) {
122    let mut injected = 0usize;
123    let mut warnings = Vec::new();
124    for text in input.document_overrides.values() {
125        let (n, w) = inject_swrl_from_turtle(ontology, text);
126        injected += n;
127        warnings.extend(w);
128    }
129    (injected, warnings)
130}
131
132/// Parse `strixonomy:swrlRule` JSON literals from Turtle and push convertible rules.
133///
134/// Returns `(injected_count, warnings)`. Rules with BuiltIn/DataRange atoms are skipped
135/// with an explicit warning (#357) instead of silently disappearing.
136pub fn inject_swrl_from_turtle(
137    ontology: &mut Ontology,
138    text: &str,
139) -> (usize, Vec<ReasonerWarning>) {
140    let mut injected = 0usize;
141    let mut warnings = Vec::new();
142    for rule in strixonomy_swrl::rules_from_turtle_document(text) {
143        if !rule.enabled {
144            continue;
145        }
146        let rule_id = rule.id.as_deref().unwrap_or("<anonymous>");
147        match convert_authored_rule(ontology, &rule) {
148            Ok(converted) => {
149                if ontology.push_swrl_rule(converted).is_ok() {
150                    injected += 1;
151                }
152            }
153            Err(e) => {
154                warnings.push(ReasonerWarning {
155                    code: "swrl_rule_skipped".into(),
156                    message: format!("enabled SWRL rule {rule_id} was not injected: {e}"),
157                    suggested_profile: None,
158                });
159            }
160        }
161    }
162    (injected, warnings)
163}
164
165fn convert_authored_rule(
166    ontology: &mut Ontology,
167    rule: &strixonomy_swrl::SwrlRule,
168) -> Result<SwrlRule> {
169    let mut body = Vec::new();
170    for atom in &rule.body {
171        body.push(convert_atom(ontology, atom)?);
172    }
173    let mut head = Vec::new();
174    for atom in &rule.head {
175        head.push(convert_atom(ontology, atom)?);
176    }
177    Ok(SwrlRule { body, head })
178}
179
180fn convert_atom(ontology: &mut Ontology, atom: &strixonomy_swrl::SwrlAtom) -> Result<SwrlAtom> {
181    use strixonomy_swrl::SwrlAtom as A;
182    match atom {
183        A::Class { class, arg } => Ok(SwrlAtom::Class {
184            class: ontology
185                .entity_id(class, EntityKind::Class)
186                .map_err(|e| ReasonerError::Classify(e.to_string()))?,
187            arg: convert_iarg(ontology, arg)?,
188        }),
189        A::ObjectProperty { property, subject, object } => Ok(SwrlAtom::ObjectProperty {
190            property: ontology
191                .entity_id(property, EntityKind::ObjectProperty)
192                .map_err(|e| ReasonerError::Classify(e.to_string()))?,
193            subject: convert_iarg(ontology, subject)?,
194            object: convert_iarg(ontology, object)?,
195        }),
196        A::DataProperty { property, subject, value } => Ok(SwrlAtom::DataProperty {
197            property: ontology
198                .entity_id(property, EntityKind::DataProperty)
199                .map_err(|e| ReasonerError::Classify(e.to_string()))?,
200            subject: convert_iarg(ontology, subject)?,
201            value: convert_darg(ontology, value)?,
202        }),
203        A::SameIndividual { left, right } => Ok(SwrlAtom::SameIndividual(
204            convert_iarg(ontology, left)?,
205            convert_iarg(ontology, right)?,
206        )),
207        A::DifferentIndividuals { left, right } => Ok(SwrlAtom::DifferentIndividuals(
208            convert_iarg(ontology, left)?,
209            convert_iarg(ontology, right)?,
210        )),
211        A::BuiltIn { .. } => Err(ReasonerError::Classify(
212            "BuiltIn SWRL atoms are not executable via Ontologos materialization".into(),
213        )),
214        A::DataRange { .. } => Err(ReasonerError::Classify(
215            "DataRange SWRL atoms are not mapped for Ontologos injection".into(),
216        )),
217    }
218}
219
220fn convert_iarg(ontology: &mut Ontology, arg: &strixonomy_swrl::SwrlIArg) -> Result<SwrlIArg> {
221    match arg {
222        strixonomy_swrl::SwrlIArg::Variable(v) => Ok(SwrlIArg::Variable(v.clone())),
223        strixonomy_swrl::SwrlIArg::Individual(iri) => Ok(SwrlIArg::Individual(
224            ontology
225                .entity_id(iri, EntityKind::Individual)
226                .map_err(|e| ReasonerError::Classify(e.to_string()))?,
227        )),
228    }
229}
230
231fn convert_darg(ontology: &mut Ontology, arg: &strixonomy_swrl::SwrlDArg) -> Result<SwrlDArg> {
232    match arg {
233        strixonomy_swrl::SwrlDArg::Variable(v) => Ok(SwrlDArg::Variable(v.clone())),
234        strixonomy_swrl::SwrlDArg::Literal { lexical, datatype } => {
235            let datatype = match datatype {
236                Some(iri) => Some(
237                    ontology
238                        .entity_id(iri, EntityKind::Datatype)
239                        .map_err(|e| ReasonerError::Classify(e.to_string()))?,
240                ),
241                None => None,
242            };
243            Ok(SwrlDArg::Literal { lexical: lexical.clone(), datatype })
244        }
245    }
246}