1use crate::error::{ReasonerError, Result};
4use crate::input::ReasonerInput;
5use crate::result::{ClassificationResult, RealizationResult};
6use crate::{classify, realize, ReasonerId};
7use horned_owl::model::ClassExpression;
8use ontologos_bridge::{core_to_triples_all, merge_triples_into_ontology};
9use ontologos_core::Ontology;
10use ontologos_parser::load_ontology;
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13use std::time::Instant;
14use strixonomy_catalog::ClassHierarchy;
15use strixonomy_owl::{class_expression_to_turtle_value, parse_class_expression};
16
17pub const DL_QUERY_CLASS_IRI: &str = "urn:strixonomy:dl-query#Q";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
21#[serde(rename_all = "snake_case")]
22pub enum DlQueryMode {
23 #[default]
24 Inferred,
25 Asserted,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct DlQueryResult {
30 pub expression: String,
31 pub normalized: String,
32 pub query_class_iri: String,
33 pub subclasses: Vec<String>,
34 pub superclasses: Vec<String>,
35 pub equivalents: Vec<String>,
36 pub instances: Vec<String>,
37 pub profile: String,
38 pub mode: DlQueryMode,
39 pub duration_ms: u64,
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub warnings: Vec<String>,
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub diagnostics: Vec<String>,
44}
45
46pub fn run_dl_query(
52 profile: ReasonerId,
53 input: &ReasonerInput,
54 expression: &str,
55 namespaces: &BTreeMap<String, String>,
56 mode: DlQueryMode,
57) -> Result<DlQueryResult> {
58 let started = Instant::now();
59 let parsed = parse_class_expression(expression, namespaces)
60 .map_err(|e| ReasonerError::Classify(format!("Manchester parse failed: {e}")))?;
61 let diagnostics: Vec<String> = parsed.diagnostics.iter().map(|d| d.message.clone()).collect();
62
63 let named_iri = named_class_iri(&parsed.expression);
64 let is_named = named_iri.is_some();
65 let (query_iri, query_input, mut warnings) = if let Some(iri) = named_iri {
66 (iri, input.clone_shallow(), Vec::new())
67 } else {
68 let (augmented, w) = inject_temp_equivalent(input, &parsed.expression, namespaces)?;
69 (DL_QUERY_CLASS_IRI.to_string(), augmented, w)
70 };
71
72 let (hierarchy, profile_used, realization, equiv_clusters) = match mode {
73 DlQueryMode::Asserted => {
74 (query_input.asserted_hierarchy.clone(), "asserted".to_string(), None, Vec::new())
75 }
76 DlQueryMode::Inferred => {
77 let classification = classify(profile, &query_input, true)?;
78 let realization = match realize(profile, &query_input) {
79 Ok(result) => Some(result),
80 Err(err) => {
81 warnings.push(format!(
83 "realization unavailable; Instances may be incomplete: {err}"
84 ));
85 None
86 }
87 };
88 collect_inferred(&classification, realization)
89 }
90 };
91
92 let mut subclasses = collect_descendants(&hierarchy, &query_iri);
93 let mut superclasses = collect_ancestors(&hierarchy, &query_iri);
94 let mut equivalents =
95 collect_equivalents(&hierarchy, &query_input.ontology, &query_iri, &equiv_clusters);
96 subclasses.retain(|iri| iri != &query_iri && iri != DL_QUERY_CLASS_IRI);
97 superclasses.retain(|iri| {
98 iri != &query_iri
99 && iri != DL_QUERY_CLASS_IRI
100 && iri != "http://www.w3.org/2002/07/owl#Thing"
101 });
102 equivalents.retain(|iri| iri != &query_iri && iri != DL_QUERY_CLASS_IRI);
103
104 let equiv_set: BTreeSet<_> = equivalents.iter().cloned().collect();
106 subclasses.retain(|iri| !equiv_set.contains(iri));
107 superclasses.retain(|iri| !equiv_set.contains(iri));
108
109 let mut instances = Vec::new();
110 if let Some(realization) = realization {
111 for entry in realization.individuals {
112 if entry.types.iter().any(|t| t == &query_iri) {
113 instances.push(entry.individual_iri);
114 }
115 }
116 } else if mode == DlQueryMode::Asserted {
117 if is_named {
118 instances = asserted_instances_of_class(&query_input, &hierarchy, &query_iri);
119 } else {
120 warnings.push(
121 "asserted mode cannot materialize instances for anonymous class expressions; use inferred mode"
122 .to_string(),
123 );
124 }
125 }
126
127 instances.sort();
128 instances.dedup();
129
130 Ok(DlQueryResult {
131 expression: expression.to_string(),
132 normalized: parsed.normalized,
133 query_class_iri: query_iri,
134 subclasses,
135 superclasses,
136 equivalents,
137 instances,
138 profile: profile_used,
139 mode,
140 duration_ms: started.elapsed().as_millis() as u64,
141 warnings,
142 diagnostics,
143 })
144}
145
146impl ReasonerInput {
147 fn clone_shallow(&self) -> Self {
149 Self {
150 workspace: self.workspace.clone(),
151 content_hash: self.content_hash.clone(),
152 ontology: self.ontology.clone(),
153 asserted_hierarchy: self.asserted_hierarchy.clone(),
154 document_overrides: self.document_overrides.clone(),
155 }
156 }
157}
158
159fn collect_inferred(
160 classification: &ClassificationResult,
161 realization: Option<RealizationResult>,
162) -> (ClassHierarchy, String, Option<RealizationResult>, Vec<Vec<String>>) {
163 (
164 classification.inferred.combined.clone(),
165 classification.profile_used.clone(),
166 realization,
167 classification.equivalences.clone(),
168 )
169}
170
171fn asserted_instances_of_class(
174 input: &ReasonerInput,
175 hierarchy: &ClassHierarchy,
176 class_iri: &str,
177) -> Vec<String> {
178 let mut class_iris: BTreeSet<String> =
179 collect_descendants(hierarchy, class_iri).into_iter().collect();
180 class_iris.insert(class_iri.to_string());
181 close_under_asserted_equivalents(&input.ontology, &mut class_iris);
182 let seeds: Vec<String> = class_iris.iter().cloned().collect();
184 for seed in seeds {
185 for child in collect_descendants(hierarchy, &seed) {
186 class_iris.insert(child);
187 }
188 }
189 close_under_asserted_equivalents(&input.ontology, &mut class_iris);
190
191 let mut out = BTreeSet::new();
192 for iri in &class_iris {
193 let Some(class_id) = input.ontology.lookup_entity(iri) else {
194 continue;
195 };
196 for ind_id in input.ontology.individuals_of(class_id) {
197 if let Ok(ind_iri) = crate::result::entity_iri(&input.ontology, *ind_id) {
198 out.insert(ind_iri);
199 }
200 }
201 }
202
203 if out.is_empty() {
205 use ontologos_core::Axiom;
206 for (_id, axiom) in input.ontology.axioms().iter() {
207 let Axiom::ClassAssertion { individual, class } = axiom else {
208 continue;
209 };
210 let Ok(class_s) = crate::result::entity_iri(&input.ontology, *class) else {
211 continue;
212 };
213 if !class_iris.contains(&class_s) {
214 continue;
215 }
216 if let Ok(ind_s) = crate::result::entity_iri(&input.ontology, *individual) {
217 out.insert(ind_s);
218 }
219 }
220 }
221
222 out.into_iter().collect()
223}
224
225fn close_under_asserted_equivalents(ontology: &Ontology, class_iris: &mut BTreeSet<String>) {
226 let seeds: Vec<String> = class_iris.iter().cloned().collect();
227 for seed in seeds {
228 let Some(id) = ontology.lookup_entity(&seed) else {
229 continue;
230 };
231 let Some(equivs) = ontology.equivalents_of(id) else {
232 continue;
233 };
234 for eid in equivs {
235 if let Ok(iri) = crate::result::entity_iri(ontology, *eid) {
236 class_iris.insert(iri);
237 }
238 }
239 }
240}
241
242fn named_class_iri(expr: &ClassExpression<horned_owl::model::RcStr>) -> Option<String> {
243 match expr {
244 ClassExpression::Class(c) => Some(c.to_string()),
245 _ => None,
246 }
247}
248
249fn inject_temp_equivalent(
250 input: &ReasonerInput,
251 expr: &ClassExpression<horned_owl::model::RcStr>,
252 namespaces: &BTreeMap<String, String>,
253) -> Result<(ReasonerInput, Vec<String>)> {
254 let ce_turtle = class_expression_to_turtle_value(expr, namespaces, 0).map_err(|e| {
255 ReasonerError::Classify(format!("failed to serialize class expression: {e}"))
256 })?;
257 let supplement = build_query_supplement(&ce_turtle, namespaces);
258 let loaded = load_ontology_from_temp_text(&supplement)?;
259 let mut ontology = input.ontology.clone();
260 let triples =
261 core_to_triples_all(&loaded).map_err(|e| ReasonerError::Ontology(e.to_string()))?;
262 merge_triples_into_ontology(&mut ontology, &triples, &[])
263 .map_err(|e| ReasonerError::Ontology(e.to_string()))?;
264 let asserted_hierarchy = crate::hierarchy::asserted_hierarchy_from_ontology(&ontology);
265 Ok((
266 ReasonerInput {
267 workspace: input.workspace.clone(),
268 content_hash: format!("{}:dl-query", input.content_hash),
269 ontology,
270 asserted_hierarchy,
271 document_overrides: input.document_overrides.clone(),
272 },
273 vec!["evaluated via temporary equivalent class (not written to disk)".to_string()],
274 ))
275}
276
277fn build_query_supplement(ce_turtle: &str, namespaces: &BTreeMap<String, String>) -> String {
278 let mut out = String::new();
279 out.push_str("@prefix owl: <http://www.w3.org/2002/07/owl#> .\n");
280 out.push_str("@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n");
281 out.push_str("@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n");
282 out.push_str("@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n");
283 for (prefix, iri) in namespaces {
284 if matches!(prefix.as_str(), "owl" | "rdf" | "rdfs" | "xsd") {
285 continue;
286 }
287 out.push_str(&format!("@prefix {prefix}: <{iri}> .\n"));
288 }
289 out.push_str(&format!(
290 "<{DL_QUERY_CLASS_IRI}> a owl:Class ;\n owl:equivalentClass {ce_turtle} .\n"
291 ));
292 out
293}
294
295fn load_ontology_from_temp_text(text: &str) -> Result<ontologos_core::Ontology> {
296 let tmp = tempfile::Builder::new()
297 .suffix(".ttl")
298 .tempfile()
299 .map_err(|e| ReasonerError::Ontology(e.to_string()))?;
300 std::fs::write(tmp.path(), text).map_err(|e| ReasonerError::Ontology(e.to_string()))?;
301 load_ontology(tmp.path()).map_err(|e| ReasonerError::Ontology(e.to_string()))
302}
303
304fn collect_descendants(hierarchy: &ClassHierarchy, root: &str) -> Vec<String> {
305 let mut out = BTreeSet::new();
306 let mut stack = vec![root.to_string()];
307 let mut seen = BTreeSet::new();
308 while let Some(current) = stack.pop() {
309 if !seen.insert(current.clone()) {
310 continue;
311 }
312 if let Some(children) = hierarchy.children.get(¤t) {
313 for child in children {
314 if child != root {
315 out.insert(child.clone());
316 }
317 stack.push(child.clone());
318 }
319 }
320 }
321 out.into_iter().collect()
322}
323
324fn collect_ancestors(hierarchy: &ClassHierarchy, root: &str) -> Vec<String> {
325 let mut out = BTreeSet::new();
326 let mut stack = vec![root.to_string()];
327 let mut seen = BTreeSet::new();
328 while let Some(current) = stack.pop() {
329 if !seen.insert(current.clone()) {
330 continue;
331 }
332 if let Some(parents) = hierarchy.parents.get(¤t) {
333 for parent in parents {
334 if parent != root {
335 out.insert(parent.clone());
336 }
337 stack.push(parent.clone());
338 }
339 }
340 }
341 out.into_iter().collect()
342}
343
344fn collect_equivalents(
346 hierarchy: &ClassHierarchy,
347 ontology: &Ontology,
348 root: &str,
349 inferred_clusters: &[Vec<String>],
350) -> Vec<String> {
351 let descendants: BTreeSet<_> = collect_descendants(hierarchy, root).into_iter().collect();
352 let ancestors: BTreeSet<_> = collect_ancestors(hierarchy, root).into_iter().collect();
353 let mut out: BTreeSet<String> = descendants.intersection(&ancestors).cloned().collect();
354
355 if let Some(id) = ontology.lookup_entity(root) {
356 if let Some(equivs) = ontology.equivalents_of(id) {
357 for eid in equivs {
358 if let Ok(iri) = crate::result::entity_iri(ontology, *eid) {
359 out.insert(iri);
360 }
361 }
362 }
363 }
364
365 for cluster in inferred_clusters {
366 if cluster.iter().any(|iri| iri == root) {
367 for iri in cluster {
368 if iri != root {
369 out.insert(iri.clone());
370 }
371 }
372 }
373 }
374
375 out.into_iter().collect()
376}